From 7db848f153f083b5efafc4c3bae177eeafb99f8b Mon Sep 17 00:00:00 2001 From: bugficks Date: Tue, 15 Jan 2019 13:29:56 +0100 Subject: [PATCH 001/228] [Feature request] MySQL SSL CA in datasource connector https://github.com/grafana/grafana/issues/8570 --- pkg/tsdb/mysql/mysql.go | 44 ++++++++++++ .../datasource/mysql/partials/config.html | 68 ++++++++++++++++++- 2 files changed, 110 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 35b03e489a0..e713b87e265 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -6,6 +6,10 @@ import ( "reflect" "strconv" "strings" + "errors" + + "crypto/x509" + "crypto/tls" "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" @@ -32,6 +36,46 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin datasource.Url, datasource.Database, ) + + var tlsSkipVerify, tlsAuth, tlsAuthWithCACert bool + if datasource.JsonData != nil { + tlsAuth = datasource.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = datasource.JsonData.Get("tlsAuthWithCACert").MustBool(false) + tlsSkipVerify = datasource.JsonData.Get("tlsSkipVerify").MustBool(false) + } + + if tlsAuth || tlsAuthWithCACert { + + secureJsonData := datasource.SecureJsonData.Decrypt() + tlsConfig := tls.Config { + InsecureSkipVerify: tlsSkipVerify, + } + + if tlsAuthWithCACert && len(secureJsonData["tlsCACert"]) > 0 { + + caPool := x509.NewCertPool() + if ok := caPool.AppendCertsFromPEM([]byte(secureJsonData["tlsCACert"])); !ok { + return nil, errors.New("Failed to parse TLS CA PEM certificate") + } + + tlsConfig.RootCAs = caPool + } + + if tlsAuth { + certs, err := tls.X509KeyPair([]byte(secureJsonData["tlsClientCert"]), []byte(secureJsonData["tlsClientKey"])) + if err != nil { + return nil, err + } + clientCert := make([]tls.Certificate, 0, 1) + clientCert = append(clientCert, certs) + + tlsConfig.Certificates = clientCert + } + + mysql.RegisterTLSConfig(datasource.Name, &tlsConfig) + cnnstr += "&tls=" + datasource.Name + } + logger.Debug("getEngine", "connection", cnnstr) config := tsdb.SqlQueryEndpointConfiguration{ diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html index a35633c626a..5f3ba5c1286 100644 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ b/public/app/plugins/datasource/mysql/partials/config.html @@ -1,4 +1,3 @@ -

MySQL Connection

@@ -22,6 +21,72 @@
+ +
+
+ + +
+
+ +
+
+ +
+
+
TLS Auth Details
+ TLS Certs are encrypted and stored in the Grafana database. +
+
+
+
+ +
+
+ +
+ +
+ + reset +
+
+
+ +
+
+
+ +
+
+ +
+
+ + reset +
+
+ +
+
+ +
+
+ +
+
+ + reset +
+
+
+
Connection limits @@ -84,4 +149,3 @@

- From f31fe495e977cd9fe1c585e25221a423ff9a7c71 Mon Sep 17 00:00:00 2001 From: bugficks Date: Tue, 15 Jan 2019 13:54:25 +0100 Subject: [PATCH 002/228] fix go fmt --- pkg/tsdb/mysql/mysql.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index e713b87e265..82e7cac27f0 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -2,14 +2,14 @@ package mysql import ( "database/sql" + "errors" "fmt" "reflect" "strconv" "strings" - "errors" - "crypto/x509" "crypto/tls" + "crypto/x509" "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" @@ -47,7 +47,7 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin if tlsAuth || tlsAuthWithCACert { secureJsonData := datasource.SecureJsonData.Decrypt() - tlsConfig := tls.Config { + tlsConfig := tls.Config{ InsecureSkipVerify: tlsSkipVerify, } From 7df5e3cebf06b39c0007bca76c9e86254fc7bc5a Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 28 Jan 2019 19:37:19 +0100 Subject: [PATCH 003/228] extract tls auth settings directive from datasource http settings directive --- public/app/features/all.ts | 1 + .../datasources/partials/http_settings.html | 52 +------------- .../partials/tls_auth_settings.html | 62 +++++++++++++++++ .../settings/TlsAuthSettingsCtrl.ts | 10 +++ .../datasource/mysql/partials/config.html | 68 +++---------------- 5 files changed, 84 insertions(+), 109 deletions(-) create mode 100644 public/app/features/datasources/partials/tls_auth_settings.html create mode 100644 public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts diff --git a/public/app/features/all.ts b/public/app/features/all.ts index 83146596ea0..d5e684e4a4e 100644 --- a/public/app/features/all.ts +++ b/public/app/features/all.ts @@ -12,3 +12,4 @@ import './manage-dashboards'; import './teams/CreateTeamCtrl'; import './profile/all'; import './datasources/settings/HttpSettingsCtrl'; +import './datasources/settings/TlsAuthSettingsCtrl'; diff --git a/public/app/features/datasources/partials/http_settings.html b/public/app/features/datasources/partials/http_settings.html index 521e2d3cdc6..b6f2c4fc0dd 100644 --- a/public/app/features/datasources/partials/http_settings.html +++ b/public/app/features/datasources/partials/http_settings.html @@ -101,53 +101,5 @@ -
-
-
TLS Auth Details
- TLS Certs are encrypted and stored in the Grafana database. -
-
-
-
- -
-
- -
- -
- - reset -
-
-
- -
-
-
- -
-
- -
-
- - reset -
-
- -
-
- -
-
- -
-
- - reset -
-
-
-
- + + \ No newline at end of file diff --git a/public/app/features/datasources/partials/tls_auth_settings.html b/public/app/features/datasources/partials/tls_auth_settings.html new file mode 100644 index 00000000000..c852e8ec70c --- /dev/null +++ b/public/app/features/datasources/partials/tls_auth_settings.html @@ -0,0 +1,62 @@ +
+
+
TLS Auth Details
+ TLS Certs are encrypted and stored in the Grafana database. +
+
+
+
+
+ +
+ +
+ + reset +
+
+
+ +
+
+
+
+ +
+
+ + reset +
+
+ +
+
+
+ +
+
+ + reset +
+
+
+
diff --git a/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts new file mode 100644 index 00000000000..7c21fab404c --- /dev/null +++ b/public/app/features/datasources/settings/TlsAuthSettingsCtrl.ts @@ -0,0 +1,10 @@ +import { coreModule } from 'app/core/core'; + +coreModule.directive('datasourceTlsAuthSettings', () => { + return { + scope: { + current: '=', + }, + templateUrl: 'public/app/features/datasources/partials/tls_auth_settings.html', + }; +}); diff --git a/public/app/plugins/datasource/mysql/partials/config.html b/public/app/plugins/datasource/mysql/partials/config.html index 5f3ba5c1286..8221a06e1ee 100644 --- a/public/app/plugins/datasource/mysql/partials/config.html +++ b/public/app/plugins/datasource/mysql/partials/config.html @@ -24,70 +24,20 @@
- - + +
- +
-
-
-
TLS Auth Details
- TLS Certs are encrypted and stored in the Grafana database. -
-
-
-
- -
-
- -
- -
- - reset -
-
-
- -
-
-
- -
-
- -
-
- - reset -
-
- -
-
- -
-
- -
-
- - reset -
-
-
-
- + + Connection limits From f157c19e16cdc970542867ec77eb5a61fe5f11ad Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 28 Jan 2019 19:38:56 +0100 Subject: [PATCH 004/228] extract parsing of datasource tls config to method --- pkg/models/datasource_cache.go | 48 +++++++++++++++++++++------------- pkg/tsdb/mysql/mysql.go | 43 ++++-------------------------- 2 files changed, 35 insertions(+), 56 deletions(-) diff --git a/pkg/models/datasource_cache.go b/pkg/models/datasource_cache.go index 66ba66e4d39..1c895514ace 100644 --- a/pkg/models/datasource_cache.go +++ b/pkg/models/datasource_cache.go @@ -46,19 +46,16 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { return t.Transport, nil } - var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool - if ds.JsonData != nil { - tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false) - tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) - tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false) + tlsConfig, err := ds.GetTLSConfig() + if err != nil { + return nil, err } + tlsConfig.Renegotiation = tls.RenegotiateFreelyAsClient + transport := &http.Transport{ - TLSClientConfig: &tls.Config{ - InsecureSkipVerify: tlsSkipVerify, - Renegotiation: tls.RenegotiateFreelyAsClient, - }, - Proxy: http.ProxyFromEnvironment, + TLSClientConfig: tlsConfig, + Proxy: http.ProxyFromEnvironment, Dial: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, @@ -70,6 +67,26 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { IdleConnTimeout: 90 * time.Second, } + ptc.cache[ds.Id] = cachedTransport{ + Transport: transport, + updated: ds.Updated, + } + + return transport, nil +} + +func (ds *DataSource) GetTLSConfig() (*tls.Config, error) { + var tlsSkipVerify, tlsClientAuth, tlsAuthWithCACert bool + if ds.JsonData != nil { + tlsClientAuth = ds.JsonData.Get("tlsAuth").MustBool(false) + tlsAuthWithCACert = ds.JsonData.Get("tlsAuthWithCACert").MustBool(false) + tlsSkipVerify = ds.JsonData.Get("tlsSkipVerify").MustBool(false) + } + + tlsConfig := &tls.Config{ + InsecureSkipVerify: tlsSkipVerify, + } + if tlsClientAuth || tlsAuthWithCACert { decrypted := ds.SecureJsonData.Decrypt() if tlsAuthWithCACert && len(decrypted["tlsCACert"]) > 0 { @@ -78,7 +95,7 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { if !ok { return nil, errors.New("Failed to parse TLS CA PEM certificate") } - transport.TLSClientConfig.RootCAs = caPool + tlsConfig.RootCAs = caPool } if tlsClientAuth { @@ -86,14 +103,9 @@ func (ds *DataSource) GetHttpTransport() (*http.Transport, error) { if err != nil { return nil, err } - transport.TLSClientConfig.Certificates = []tls.Certificate{cert} + tlsConfig.Certificates = []tls.Certificate{cert} } } - ptc.cache[ds.Id] = cachedTransport{ - Transport: transport, - updated: ds.Updated, - } - - return transport, nil + return tlsConfig, nil } diff --git a/pkg/tsdb/mysql/mysql.go b/pkg/tsdb/mysql/mysql.go index 82e7cac27f0..d451150f1de 100644 --- a/pkg/tsdb/mysql/mysql.go +++ b/pkg/tsdb/mysql/mysql.go @@ -2,15 +2,11 @@ package mysql import ( "database/sql" - "errors" "fmt" "reflect" "strconv" "strings" - "crypto/tls" - "crypto/x509" - "github.com/go-sql-driver/mysql" "github.com/go-xorm/core" "github.com/grafana/grafana/pkg/log" @@ -37,42 +33,13 @@ func newMysqlQueryEndpoint(datasource *models.DataSource) (tsdb.TsdbQueryEndpoin datasource.Database, ) - var tlsSkipVerify, tlsAuth, tlsAuthWithCACert bool - if datasource.JsonData != nil { - tlsAuth = datasource.JsonData.Get("tlsAuth").MustBool(false) - tlsAuthWithCACert = datasource.JsonData.Get("tlsAuthWithCACert").MustBool(false) - tlsSkipVerify = datasource.JsonData.Get("tlsSkipVerify").MustBool(false) + tlsConfig, err := datasource.GetTLSConfig() + if err != nil { + return nil, err } - if tlsAuth || tlsAuthWithCACert { - - secureJsonData := datasource.SecureJsonData.Decrypt() - tlsConfig := tls.Config{ - InsecureSkipVerify: tlsSkipVerify, - } - - if tlsAuthWithCACert && len(secureJsonData["tlsCACert"]) > 0 { - - caPool := x509.NewCertPool() - if ok := caPool.AppendCertsFromPEM([]byte(secureJsonData["tlsCACert"])); !ok { - return nil, errors.New("Failed to parse TLS CA PEM certificate") - } - - tlsConfig.RootCAs = caPool - } - - if tlsAuth { - certs, err := tls.X509KeyPair([]byte(secureJsonData["tlsClientCert"]), []byte(secureJsonData["tlsClientKey"])) - if err != nil { - return nil, err - } - clientCert := make([]tls.Certificate, 0, 1) - clientCert = append(clientCert, certs) - - tlsConfig.Certificates = clientCert - } - - mysql.RegisterTLSConfig(datasource.Name, &tlsConfig) + if tlsConfig.RootCAs != nil || len(tlsConfig.Certificates) > 0 { + mysql.RegisterTLSConfig(datasource.Name, tlsConfig) cnnstr += "&tls=" + datasource.Name } From ce2209585c1e1e885e1f02c29d004e8abde61c84 Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:32:40 +0300 Subject: [PATCH 005/228] Remove version.ts --- .../config_ctrl.ts | 2 +- .../version.test.ts | 53 ------------------- .../version.ts | 34 ------------ 3 files changed, 1 insertion(+), 88 deletions(-) delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts index 98fe5a87a56..4ee5c94fad6 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/config_ctrl.ts @@ -1,6 +1,6 @@ import AzureLogAnalyticsDatasource from './azure_log_analytics/azure_log_analytics_datasource'; import config from 'app/core/config'; -import { isVersionGtOrEq } from './version'; +import { isVersionGtOrEq } from 'app/core/utils/version'; export class AzureMonitorConfigCtrl { static templateUrl = 'public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/config.html'; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts deleted file mode 100644 index 17a6ce9bb0b..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.test.ts +++ /dev/null @@ -1,53 +0,0 @@ -import { SemVersion, isVersionGtOrEq } from './version'; - -describe('SemVersion', () => { - let version = '1.0.0-alpha.1'; - - describe('parsing', () => { - it('should parse version properly', () => { - const semver = new SemVersion(version); - expect(semver.major).toBe(1); - expect(semver.minor).toBe(0); - expect(semver.patch).toBe(0); - expect(semver.meta).toBe('alpha.1'); - }); - }); - - describe('comparing', () => { - beforeEach(() => { - version = '3.4.5'; - }); - - it('should detect greater version properly', () => { - const semver = new SemVersion(version); - const cases = [ - { value: '3.4.5', expected: true }, - { value: '3.4.4', expected: true }, - { value: '3.4.6', expected: false }, - { value: '4', expected: false }, - { value: '3.5', expected: false }, - ]; - cases.forEach(testCase => { - expect(semver.isGtOrEq(testCase.value)).toBe(testCase.expected); - }); - }); - }); - - describe('isVersionGtOrEq', () => { - it('should compare versions properly (a >= b)', () => { - const cases = [ - { values: ['3.4.5', '3.4.5'], expected: true }, - { values: ['3.4.5', '3.4.4'], expected: true }, - { values: ['3.4.5', '3.4.6'], expected: false }, - { values: ['3.4', '3.4.0'], expected: true }, - { values: ['3', '3.0.0'], expected: true }, - { values: ['3.1.1-beta1', '3.1'], expected: true }, - { values: ['3.4.5', '4'], expected: false }, - { values: ['3.4.5', '3.5'], expected: false }, - ]; - cases.forEach(testCase => { - expect(isVersionGtOrEq(testCase.values[0], testCase.values[1])).toBe(testCase.expected); - }); - }); - }); -}); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts deleted file mode 100644 index 1131e1d2ab8..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/version.ts +++ /dev/null @@ -1,34 +0,0 @@ -import _ from 'lodash'; - -const versionPattern = /^(\d+)(?:\.(\d+))?(?:\.(\d+))?(?:-([0-9A-Za-z\.]+))?/; - -export class SemVersion { - major: number; - minor: number; - patch: number; - meta: string; - - constructor(version: string) { - const match = versionPattern.exec(version); - if (match) { - this.major = Number(match[1]); - this.minor = Number(match[2] || 0); - this.patch = Number(match[3] || 0); - this.meta = match[4]; - } - } - - isGtOrEq(version: string): boolean { - const compared = new SemVersion(version); - return !(this.major < compared.major || this.minor < compared.minor || this.patch < compared.patch); - } - - isValid(): boolean { - return _.isNumber(this.major); - } -} - -export function isVersionGtOrEq(a: string, b: string): boolean { - const aSemver = new SemVersion(a); - return aSemver.isGtOrEq(b); -} From cf60ae79c31d6c0745b47e77e62e2d0605a19b73 Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:47:17 +0300 Subject: [PATCH 006/228] Move prism to app/features/explore --- .../editor => features/explore}/slate-plugins/prism/index.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename public/app/{plugins/datasource/grafana-azure-monitor-datasource/editor => features/explore}/slate-plugins/prism/index.tsx (100%) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx b/public/app/features/explore/slate-plugins/prism/index.tsx similarity index 100% rename from public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/prism/index.tsx rename to public/app/features/explore/slate-plugins/prism/index.tsx From bdd59de877f677d8831f64d438a146408097faed Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:47:33 +0300 Subject: [PATCH 007/228] Remove newline && runner plugins --- .../editor/slate-plugins/newline.ts | 35 ------------------- .../editor/slate-plugins/runner.ts | 14 -------- 2 files changed, 49 deletions(-) delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts deleted file mode 100644 index d484d93a542..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/newline.ts +++ /dev/null @@ -1,35 +0,0 @@ -function getIndent(text) { - let offset = text.length - text.trimLeft().length; - if (offset) { - let indent = text[0]; - while (--offset) { - indent += text[0]; - } - return indent; - } - return ''; -} - -export default function NewlinePlugin() { - return { - onKeyDown(event, change) { - const { value } = change; - if (!value.isCollapsed) { - return undefined; - } - - if (event.key === 'Enter' && !event.shiftKey) { - event.preventDefault(); - - const { startBlock } = value; - const currentLineText = startBlock.text; - const indent = getIndent(currentLineText); - - return change - .splitBlock() - .insertText(indent) - .focus(); - } - }, - }; -} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts deleted file mode 100644 index 068bd9f0ad1..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/slate-plugins/runner.ts +++ /dev/null @@ -1,14 +0,0 @@ -export default function RunnerPlugin({ handler }) { - return { - onKeyDown(event) { - // Handle enter - if (handler && event.key === 'Enter' && event.shiftKey) { - // Submit on Enter - event.preventDefault(); - handler(event); - return true; - } - return undefined; - }, - }; -} From 9a3f4def98fcb89855ec278c924f1897d1b7357e Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:49:04 +0300 Subject: [PATCH 008/228] Use slate-plugins from app/features/explore --- .../editor/query_field.tsx | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 1c883a40c31..f93912f069e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -1,12 +1,9 @@ -import PluginPrism from './slate-plugins/prism'; -// import PluginPrism from 'slate-prism'; -// import Prism from 'prismjs'; +import PluginPrism from 'app/features/explore/slate-plugins/prism'; import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import ClearPlugin from 'app/features/explore/slate-plugins/clear'; -// Custom plugins (new line on Enter and run on Shift+Enter) -import NewlinePlugin from './slate-plugins/newline'; -import RunnerPlugin from './slate-plugins/runner'; +import NewlinePlugin from 'app/features/explore/slate-plugins/newline'; +import RunnerPlugin from 'app/features/explore/slate-plugins/runner'; import Typeahead from './typeahead'; From 6d03766acecab8914d344a929708a22470838a43 Mon Sep 17 00:00:00 2001 From: corpglory-dev Date: Fri, 1 Feb 2019 14:54:38 +0300 Subject: [PATCH 009/228] Remove extra newline --- .../grafana-azure-monitor-datasource/editor/query_field.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index f93912f069e..400126f7e55 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -1,5 +1,4 @@ import PluginPrism from 'app/features/explore/slate-plugins/prism'; - import BracesPlugin from 'app/features/explore/slate-plugins/braces'; import ClearPlugin from 'app/features/explore/slate-plugins/clear'; import NewlinePlugin from 'app/features/explore/slate-plugins/newline'; From 60f700a1d217593ee1bca29d0be12328b21e0fde Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 Feb 2019 19:23:19 +0100 Subject: [PATCH 010/228] wip: dashboard react --- .../dashboard/containers/DashboardCtrl.ts | 6 - .../dashboard/containers/DashboardPage.tsx | 138 ++++++++++++++++++ .../features/dashboard/state/initDashboard.ts | 5 + public/app/routes/routes.ts | 9 +- 4 files changed, 149 insertions(+), 9 deletions(-) create mode 100644 public/app/features/dashboard/containers/DashboardPage.tsx create mode 100644 public/app/features/dashboard/state/initDashboard.ts diff --git a/public/app/features/dashboard/containers/DashboardCtrl.ts b/public/app/features/dashboard/containers/DashboardCtrl.ts index 74795315504..0151f8f7331 100644 --- a/public/app/features/dashboard/containers/DashboardCtrl.ts +++ b/public/app/features/dashboard/containers/DashboardCtrl.ts @@ -31,12 +31,6 @@ export class DashboardCtrl { // temp hack due to way dashboards are loaded // can't use controllerAs on route yet $scope.ctrl = this; - - // TODO: break out settings view to separate view & controller - this.editTab = 0; - - // funcs called from React component bindings and needs this binding - this.getPanelContainer = this.getPanelContainer.bind(this); } setupDashboard(data) { diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx new file mode 100644 index 00000000000..54eed34fc29 --- /dev/null +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -0,0 +1,138 @@ +// Libraries +import React, { Component } from 'react'; +import { hot } from 'react-hot-loader'; +import { connect } from 'react-redux'; + +// Utils & Services +import locationUtil from 'app/core/utils/location_util'; +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { createErrorNotification } from 'app/core/copy/appNotification'; + +// Components +import { LoadingPlaceholder } from '@grafana/ui'; + +// Redux +import { updateLocation } from 'app/core/actions'; +import { notifyApp } from 'app/core/actions'; + +// Types +import { StoreState } from 'app/types'; +import { DashboardModel } from 'app/features/dashboard/state'; + +interface Props { + panelId: string; + urlUid?: string; + urlSlug?: string; + urlType?: string; + $scope: any; + $injector: any; + updateLocation: typeof updateLocation; + notifyApp: typeof notifyApp; +} + +interface State { + dashboard: DashboardModel | null; + notFound: boolean; +} + +export class DashboardPage extends Component { + state: State = { + dashboard: null, + notFound: false, + }; + + async componentDidMount() { + const { $injector, urlUid, urlType, urlSlug } = this.props; + + // handle old urls with no uid + if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { + this.redirectToNewUrl(); + return; + } + + const loaderSrv = $injector.get('dashboardLoaderSrv'); + const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + + try { + this.initDashboard(dashDTO); + } catch (err) { + this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); + console.log(err); + } + } + + redirectToNewUrl() { + getBackendSrv() + .getDashboardBySlug(this.props.urlSlug) + .then(res => { + if (res) { + const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + this.props.updateLocation(url); + } + }); + } + + initDashboard(dashDTO: any) { + const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + + // init services + this.timeSrv.init(dashboard); + this.annotationsSrv.init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + this.variableSrv + .init(dashboard) + // template values failes are non fatal + .catch(this.onInitFailed.bind(this, 'Templating init failed', false)) + // continue + .finally(() => { + this.dashboard = dashboard; + this.dashboard.processRepeats(); + this.dashboard.updateSubmenuVisibility(); + this.dashboard.autoFitPanels(window.innerHeight); + + this.unsavedChangesSrv.init(dashboard, this.$scope); + + // TODO refactor ViewStateSrv + this.$scope.dashboard = dashboard; + this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); + + this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); + this.setWindowTitleAndTheme(); + + appEvents.emit('dashboard-initialized', dashboard); + }) + .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); + + this.setState({ dashboard }); + } + + render() { + const { notFound, dashboard } = this.state; + + if (notFound) { + return
Dashboard not found
; + } + + if (!dashboard) { + return ; + } + + return
title: {dashboard.title}
; + } +} + +const mapStateToProps = (state: StoreState) => ({ + urlUid: state.location.routeParams.uid, + urlSlug: state.location.routeParams.slug, + urlType: state.location.routeParams.type, + panelId: state.location.query.panelId, +}); + +const mapDispatchToProps = { + updateLocation, + notifyApp, +}; + +export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts new file mode 100644 index 00000000000..3b2307b3ccc --- /dev/null +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -0,0 +1,5 @@ + + +export function initDashboard(dashboard: DashboardModel, $injector: any, $scope: any) { + +} diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index 0f4c09a9c77..cdd9ed89a08 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -20,6 +20,7 @@ import DataSourceDashboards from 'app/features/datasources/DataSourceDashboards' import DataSourceSettingsPage from '../features/datasources/settings/DataSourceSettingsPage'; import OrgDetailsPage from '../features/org/OrgDetailsPage'; import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage'; +import DashboardPage from '../features/dashboard/containers/DashboardPage'; import config from 'app/core/config'; /** @ngInject */ @@ -34,10 +35,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { pageClass: 'page-dashboard', }) .when('/d/:uid/:slug', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/d/:uid', { templateUrl: 'public/app/partials/dashboard.html', From d86e773c756806bb826af50c347709bce265a65f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 Feb 2019 22:43:19 +0100 Subject: [PATCH 011/228] wip: minor progress --- .../dashboard/containers/DashboardPage.tsx | 128 +++++++----------- .../app/features/dashboard/state/actions.ts | 41 +++--- .../features/dashboard/state/initDashboard.ts | 67 ++++++++- .../features/dashboard/state/reducers.test.ts | 4 +- .../app/features/dashboard/state/reducers.ts | 33 +++-- public/app/types/dashboard.ts | 17 ++- 6 files changed, 171 insertions(+), 119 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 54eed34fc29..0e3e1058660 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -3,21 +3,16 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -// Utils & Services -import locationUtil from 'app/core/utils/location_util'; -import { getBackendSrv } from 'app/core/services/backend_srv'; -import { createErrorNotification } from 'app/core/copy/appNotification'; - // Components import { LoadingPlaceholder } from '@grafana/ui'; // Redux -import { updateLocation } from 'app/core/actions'; -import { notifyApp } from 'app/core/actions'; +import { initDashboard } from '../state/initDashboard'; // Types import { StoreState } from 'app/types'; import { DashboardModel } from 'app/features/dashboard/state'; +import { DashboardLoadingState } from 'app/types/dashboard'; interface Props { panelId: string; @@ -26,8 +21,9 @@ interface Props { urlType?: string; $scope: any; $injector: any; - updateLocation: typeof updateLocation; - notifyApp: typeof notifyApp; + initDashboard: typeof initDashboard; + loadingState: DashboardLoadingState; + dashboard: DashboardModel; } interface State { @@ -42,81 +38,54 @@ export class DashboardPage extends Component { }; async componentDidMount() { - const { $injector, urlUid, urlType, urlSlug } = this.props; + this.props.initDashboard({ + injector: this.props.$injector, + scope: this.props.$scope, + urlSlug: this.props.urlSlug, + urlUid: this.props.urlUid, + urlType: this.props.urlType, + }) - // handle old urls with no uid - if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { - this.redirectToNewUrl(); - return; - } - - const loaderSrv = $injector.get('dashboardLoaderSrv'); - const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - - try { - this.initDashboard(dashDTO); - } catch (err) { - this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); - console.log(err); - } + // const { $injector, urlUid, urlType, urlSlug } = this.props; + // + // // handle old urls with no uid + // if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { + // this.redirectToNewUrl(); + // return; + // } + // + // const loaderSrv = $injector.get('dashboardLoaderSrv'); + // const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + // + // try { + // this.initDashboard(dashDTO); + // } catch (err) { + // this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); + // console.log(err); + // } } - redirectToNewUrl() { - getBackendSrv() - .getDashboardBySlug(this.props.urlSlug) - .then(res => { - if (res) { - const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - this.props.updateLocation(url); - } - }); - } - - initDashboard(dashDTO: any) { - const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); - - // init services - this.timeSrv.init(dashboard); - this.annotationsSrv.init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - this.variableSrv - .init(dashboard) - // template values failes are non fatal - .catch(this.onInitFailed.bind(this, 'Templating init failed', false)) - // continue - .finally(() => { - this.dashboard = dashboard; - this.dashboard.processRepeats(); - this.dashboard.updateSubmenuVisibility(); - this.dashboard.autoFitPanels(window.innerHeight); - - this.unsavedChangesSrv.init(dashboard, this.$scope); - - // TODO refactor ViewStateSrv - this.$scope.dashboard = dashboard; - this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); - - this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); - this.setWindowTitleAndTheme(); - - appEvents.emit('dashboard-initialized', dashboard); - }) - .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); - - this.setState({ dashboard }); - } + // redirectToNewUrl() { + // getBackendSrv() + // .getDashboardBySlug(this.props.urlSlug) + // .then(res => { + // if (res) { + // const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + // this.props.updateLocation(url); + // } + // }); + // } + // + // initDashboard(dashDTO: any) { + // const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + // this.setState({ dashboard }); + // } render() { - const { notFound, dashboard } = this.state; - - if (notFound) { - return
Dashboard not found
; - } + const { loadingState, dashboard } = this.props; if (!dashboard) { - return ; + return ; } return
title: {dashboard.title}
; @@ -128,11 +97,12 @@ const mapStateToProps = (state: StoreState) => ({ urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, panelId: state.location.query.panelId, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard as DashboardModel, }); const mapDispatchToProps = { - updateLocation, - notifyApp, + initDashboard }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 4dcf0a925b7..1bb29dc3ad5 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -1,8 +1,18 @@ +// Libaries import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; + +// Services & Utils import { getBackendSrv } from 'app/core/services/backend_srv'; -import appEvents from 'app/core/app_events'; +import { actionCreatorFactory } from 'app/core/redux'; +import { ActionOf } from 'app/core/redux/actionCreatorFactory'; +import { createSuccessNotification } from 'app/core/copy/appNotification'; + +// Actions import { loadPluginDashboards } from '../../plugins/state/actions'; +import { notifyApp } from 'app/core/actions'; + +// Types import { DashboardAcl, DashboardAclDTO, @@ -10,30 +20,14 @@ import { DashboardAclUpdateDTO, NewDashboardAclItem, } from 'app/types/acl'; +import { DashboardLoadingState } from 'app/types/dashboard'; -export enum ActionTypes { - LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS', - LoadStarredDashboards = 'LOAD_STARRED_DASHBOARDS', -} +export const loadDashboardPermissions = actionCreatorFactory('LOAD_DASHBOARD_PERMISSIONS').create(); +export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_LOADING_STATE').create(); -export interface LoadDashboardPermissionsAction { - type: ActionTypes.LoadDashboardPermissions; - payload: DashboardAcl[]; -} +export type Action = ActionOf; -export interface LoadStarredDashboardsAction { - type: ActionTypes.LoadStarredDashboards; - payload: DashboardAcl[]; -} - -export type Action = LoadDashboardPermissionsAction | LoadStarredDashboardsAction; - -type ThunkResult = ThunkAction; - -export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({ - type: ActionTypes.LoadDashboardPermissions, - payload: items, -}); +export type ThunkResult = ThunkAction; export function getDashboardPermissions(id: number): ThunkResult { return async dispatch => { @@ -124,7 +118,7 @@ export function addDashboardPermission(dashboardId: number, newItem: NewDashboar export function importDashboard(data, dashboardTitle: string): ThunkResult { return async dispatch => { await getBackendSrv().post('/api/dashboards/import', data); - appEvents.emit('alert-success', ['Dashboard Imported', dashboardTitle]); + dispatch(notifyApp(createSuccessNotification('Dashboard Imported', dashboardTitle))); dispatch(loadPluginDashboards()); }; } @@ -135,3 +129,4 @@ export function removeDashboard(uri: string): ThunkResult { dispatch(loadPluginDashboards()); }; } + diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 3b2307b3ccc..124d03eee4a 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,5 +1,70 @@ +// Libaries +import { StoreState } from 'app/types'; +import { ThunkAction } from 'redux-thunk'; +// Services & Utils +import { getBackendSrv } from 'app/core/services/backend_srv'; +import { createErrorNotification } from 'app/core/copy/appNotification'; -export function initDashboard(dashboard: DashboardModel, $injector: any, $scope: any) { +// Actions +import { updateLocation } from 'app/core/actions'; +import { notifyApp } from 'app/core/actions'; +import locationUtil from 'app/core/utils/location_util'; +import { setDashboardLoadingState, ThunkResult } from './actions'; +// Types +import { DashboardLoadingState } from 'app/types/dashboard'; +import { DashboardModel } from './DashboardModel'; + +export interface InitDashboardArgs { + injector: any; + scope: any; + urlUid?: string; + urlSlug?: string; + urlType?: string; +} + +export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { + return async dispatch => { + const loaderSrv = injector.get('dashboardLoaderSrv'); + + dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + + try { + // fetch dashboard from api + const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + // set initializing state + dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing)); + // create model + const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + // init services + + injector.get('timeSrv').init(dashboard); + injector.get('annotationsSrv').init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + injector.get('variableSrv').init(dashboard) + .catch(err => { + dispatch(notifyApp(createErrorNotification('Templating init failed'))); + }) + .finally(() => { + + dashboard.processRepeats(); + dashboard.updateSubmenuVisibility(); + dashboard.autoFitPanels(window.innerHeight); + + injector.get('unsavedChangesSrv').init(dashboard, scope); + + scope.dashboard = dashboard; + injector.get('dashboardViewStateSrv').create(scope); + injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); + }) + .catch(err => { + dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + }); + } catch (err) { + dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + } + }; } diff --git a/public/app/features/dashboard/state/reducers.test.ts b/public/app/features/dashboard/state/reducers.test.ts index ced8866aad8..ea3353ce741 100644 --- a/public/app/features/dashboard/state/reducers.test.ts +++ b/public/app/features/dashboard/state/reducers.test.ts @@ -1,4 +1,4 @@ -import { Action, ActionTypes } from './actions'; +import { Action } from './actions'; import { OrgRole, PermissionLevel, DashboardState } from 'app/types'; import { initialState, dashboardReducer } from './reducers'; @@ -8,7 +8,7 @@ describe('dashboard reducer', () => { beforeEach(() => { const action: Action = { - type: ActionTypes.LoadDashboardPermissions, + type: 'LOAD_DASHBOARD_PERMISSIONS', payload: [ { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View }, { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit }, diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 8a79a6c9f77..bd13446b090 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -1,21 +1,30 @@ -import { DashboardState } from 'app/types'; -import { Action, ActionTypes } from './actions'; +import { DashboardState, DashboardLoadingState } from 'app/types/dashboard'; +import { loadDashboardPermissions, setDashboardLoadingState } from './actions'; +import { reducerFactory } from 'app/core/redux'; import { processAclItems } from 'app/core/utils/acl'; export const initialState: DashboardState = { + loadingState: DashboardLoadingState.NotStarted, + dashboard: null, permissions: [], }; -export const dashboardReducer = (state = initialState, action: Action): DashboardState => { - switch (action.type) { - case ActionTypes.LoadDashboardPermissions: - return { - ...state, - permissions: processAclItems(action.payload), - }; - } - return state; -}; +export const dashboardReducer = reducerFactory(initialState) + .addMapper({ + filter: loadDashboardPermissions, + mapper: (state, action) => ({ + ...state, + permissions: processAclItems(action.payload), + }), + }) + .addMapper({ + filter: setDashboardLoadingState, + mapper: (state, action) => ({ + ...state, + loadingState: action.payload + }), + }) + .create() export default { dashboard: dashboardReducer, diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index d33405c985e..df9a2e53548 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,5 +1,18 @@ import { DashboardAcl } from './acl'; -export interface DashboardState { - permissions: DashboardAcl[]; +export interface Dashboard { +} + +export enum DashboardLoadingState { + NotStarted = 'Not started', + Fetching = 'Fetching', + Initializing = 'Initializing', + Error = 'Error', + Done = 'Done', +} + +export interface DashboardState { + dashboard: Dashboard | null; + loadingState: DashboardLoadingState; + permissions: DashboardAcl[] | null; } From 83937f59c008343e7e1d000a088807bccb476e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sat, 2 Feb 2019 23:01:48 +0100 Subject: [PATCH 012/228] wip: dashboard in react starting to work --- .../dashboard/containers/DashboardPage.tsx | 40 +-------- .../app/features/dashboard/state/actions.ts | 3 +- .../features/dashboard/state/initDashboard.ts | 86 +++++++++++-------- .../app/features/dashboard/state/reducers.ts | 11 ++- public/app/types/dashboard.ts | 4 +- 5 files changed, 66 insertions(+), 78 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 0e3e1058660..c0d5c4d4730 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; // Components import { LoadingPlaceholder } from '@grafana/ui'; +import { DashboardGrid } from '../dashgrid/DashboardGrid'; // Redux import { initDashboard } from '../state/initDashboard'; @@ -45,42 +46,8 @@ export class DashboardPage extends Component { urlUid: this.props.urlUid, urlType: this.props.urlType, }) - - // const { $injector, urlUid, urlType, urlSlug } = this.props; - // - // // handle old urls with no uid - // if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { - // this.redirectToNewUrl(); - // return; - // } - // - // const loaderSrv = $injector.get('dashboardLoaderSrv'); - // const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - // - // try { - // this.initDashboard(dashDTO); - // } catch (err) { - // this.props.notifyApp(createErrorNotification('Failed to init dashboard', err.toString())); - // console.log(err); - // } } - // redirectToNewUrl() { - // getBackendSrv() - // .getDashboardBySlug(this.props.urlSlug) - // .then(res => { - // if (res) { - // const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - // this.props.updateLocation(url); - // } - // }); - // } - // - // initDashboard(dashDTO: any) { - // const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); - // this.setState({ dashboard }); - // } - render() { const { loadingState, dashboard } = this.props; @@ -88,7 +55,8 @@ export class DashboardPage extends Component { return ; } - return
title: {dashboard.title}
; + console.log(dashboard); + return } } @@ -98,7 +66,7 @@ const mapStateToProps = (state: StoreState) => ({ urlType: state.location.routeParams.type, panelId: state.location.query.panelId, loadingState: state.dashboard.loadingState, - dashboard: state.dashboard as DashboardModel, + dashboard: state.dashboard.model as DashboardModel, }); const mapDispatchToProps = { diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 1bb29dc3ad5..14721cdbe96 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -20,10 +20,11 @@ import { DashboardAclUpdateDTO, NewDashboardAclItem, } from 'app/types/acl'; -import { DashboardLoadingState } from 'app/types/dashboard'; +import { DashboardLoadingState, MutableDashboard } from 'app/types/dashboard'; export const loadDashboardPermissions = actionCreatorFactory('LOAD_DASHBOARD_PERMISSIONS').create(); export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_LOADING_STATE').create(); +export const setDashboardModel = actionCreatorFactory('SET_DASHBOARD_MODEL').create(); export type Action = ActionOf; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 124d03eee4a..d20f9ae1cf8 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,16 +1,11 @@ -// Libaries -import { StoreState } from 'app/types'; -import { ThunkAction } from 'redux-thunk'; - // Services & Utils -import { getBackendSrv } from 'app/core/services/backend_srv'; import { createErrorNotification } from 'app/core/copy/appNotification'; // Actions import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; import locationUtil from 'app/core/utils/location_util'; -import { setDashboardLoadingState, ThunkResult } from './actions'; +import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './actions'; // Types import { DashboardLoadingState } from 'app/types/dashboard'; @@ -30,41 +25,58 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + let dashDTO = null; + try { // fetch dashboard from api - const dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - // set initializing state - dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing)); - // create model - const dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); - // init services - - injector.get('timeSrv').init(dashboard); - injector.get('annotationsSrv').init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - injector.get('variableSrv').init(dashboard) - .catch(err => { - dispatch(notifyApp(createErrorNotification('Templating init failed'))); - }) - .finally(() => { - - dashboard.processRepeats(); - dashboard.updateSubmenuVisibility(); - dashboard.autoFitPanels(window.innerHeight); - - injector.get('unsavedChangesSrv').init(dashboard, scope); - - scope.dashboard = dashboard; - injector.get('dashboardViewStateSrv').create(scope); - injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); - }) - .catch(err => { - dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); - }); + dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + console.log(err); + return; } + + // set initializing state + dispatch(setDashboardLoadingState(DashboardLoadingState.Initializing)); + + // create model + let dashboard: DashboardModel; + try { + dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); + } catch (err) { + dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + console.log(err); + return; + } + + // init services + injector.get('timeSrv').init(dashboard); + injector.get('annotationsSrv').init(dashboard); + + // template values service needs to initialize completely before + // the rest of the dashboard can load + try { + await injector.get('variableSrv').init(dashboard); + } catch (err) { + dispatch(notifyApp(createErrorNotification('Templating init failed', err.toString()))); + console.log(err); + } + + try { + dashboard.processRepeats(); + dashboard.updateSubmenuVisibility(); + dashboard.autoFitPanels(window.innerHeight); + + injector.get('unsavedChangesSrv').init(dashboard, scope); + + scope.dashboard = dashboard; + injector.get('dashboardViewStateSrv').create(scope); + injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); + } catch (err) { + dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); + console.log(err); + } + + dispatch(setDashboardModel(dashboard)); }; } diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index bd13446b090..5cfc879a1a4 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -1,11 +1,11 @@ import { DashboardState, DashboardLoadingState } from 'app/types/dashboard'; -import { loadDashboardPermissions, setDashboardLoadingState } from './actions'; +import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel } from './actions'; import { reducerFactory } from 'app/core/redux'; import { processAclItems } from 'app/core/utils/acl'; export const initialState: DashboardState = { loadingState: DashboardLoadingState.NotStarted, - dashboard: null, + model: null, permissions: [], }; @@ -24,6 +24,13 @@ export const dashboardReducer = reducerFactory(initialState) loadingState: action.payload }), }) + .addMapper({ + filter: setDashboardModel, + mapper: (state, action) => ({ + ...state, + model: action.payload + }), + }) .create() export default { diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index df9a2e53548..bdea5b04bac 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,6 +1,6 @@ import { DashboardAcl } from './acl'; -export interface Dashboard { +export interface MutableDashboard { } export enum DashboardLoadingState { @@ -12,7 +12,7 @@ export enum DashboardLoadingState { } export interface DashboardState { - dashboard: Dashboard | null; + model: MutableDashboard | null; loadingState: DashboardLoadingState; permissions: DashboardAcl[] | null; } From 8dec74689d2361febda5647d29f3ff5f46e0007c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 10:55:58 +0100 Subject: [PATCH 013/228] Dashboard settings starting to work --- public/app/core/app_events.ts | 3 +- .../dashboard/components/DashNav/DashNav.tsx | 144 ++++++++++++++++++ .../dashboard/components/DashNav/index.ts | 2 + .../DashboardSettings/DashboardSettings.tsx | 36 +++++ .../components/DashboardSettings/index.ts | 1 + .../dashboard/containers/DashboardPage.tsx | 100 ++++++++++-- .../features/dashboard/state/initDashboard.ts | 1 + .../app/features/dashboard/state/reducers.ts | 2 +- .../sass/components/_dashboard_settings.scss | 3 + 9 files changed, 276 insertions(+), 16 deletions(-) create mode 100644 public/app/features/dashboard/components/DashNav/DashNav.tsx create mode 100644 public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx diff --git a/public/app/core/app_events.ts b/public/app/core/app_events.ts index 6af7913167b..1951fd87001 100644 --- a/public/app/core/app_events.ts +++ b/public/app/core/app_events.ts @@ -1,4 +1,5 @@ import { Emitter } from './utils/emitter'; -const appEvents = new Emitter(); +export const appEvents = new Emitter(); + export default appEvents; diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx new file mode 100644 index 00000000000..e1fb70e5d68 --- /dev/null +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -0,0 +1,144 @@ +// Libaries +import React, { PureComponent } from 'react'; +import { connect } from 'react-redux'; + +// Utils & Services +import { appEvents } from 'app/core/app_events'; + +// State +import { updateLocation } from 'app/core/actions'; + +// Types +import { DashboardModel } from '../../state/DashboardModel'; + +export interface Props { + dashboard: DashboardModel | null; + updateLocation: typeof updateLocation; +} + +export class DashNav extends PureComponent { + onOpenSearch = () => { + appEvents.emit('show-dash-search'); + }; + + onAddPanel = () => {}; + onOpenSettings = () => { + this.props.updateLocation({ + query: { + editview: 'settings', + }, + partial: true, + }) + }; + + renderLoadingState() { + return ( + + ); + } + + render() { + let { dashboard } = this.props; + + if (!dashboard) { + return this.renderLoadingState(); + } + + const haveFolder = dashboard.meta.folderId > 0; + const { canEdit, canSave, folderTitle, showSettings } = dashboard.meta; + + return ( +
+ + +
+ {/* + + */} + +
+ {canEdit && ( + + )} + + {showSettings && ( + + )} + + { + // + // + // + // + // + // + // + // + // + // + // + //
+ // + // + // + // + // + // + } +
+
+ ); + } +} + +const mapStateToProps = () => ({ +}); + +const mapDispatchToProps = { + updateLocation +}; + +export default connect(mapStateToProps, mapDispatchToProps)(DashNav); diff --git a/public/app/features/dashboard/components/DashNav/index.ts b/public/app/features/dashboard/components/DashNav/index.ts index 854e32b24d2..cfa9003cd8a 100644 --- a/public/app/features/dashboard/components/DashNav/index.ts +++ b/public/app/features/dashboard/components/DashNav/index.ts @@ -1 +1,3 @@ export { DashNavCtrl } from './DashNavCtrl'; +import DashNav from './DashNav'; +export { DashNav }; diff --git a/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx new file mode 100644 index 00000000000..8a92c0d69eb --- /dev/null +++ b/public/app/features/dashboard/components/DashboardSettings/DashboardSettings.tsx @@ -0,0 +1,36 @@ +// Libaries +import React, { PureComponent } from 'react'; + +// Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; + +// Types +import { DashboardModel } from '../../state/DashboardModel'; + +export interface Props { + dashboard: DashboardModel | null; +} + +export class DashboardSettings extends PureComponent { + element: HTMLElement; + angularCmp: AngularComponent; + + componentDidMount() { + const loader = getAngularLoader(); + + const template = ''; + const scopeProps = { dashboard: this.props.dashboard }; + + this.angularCmp = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.angularCmp) { + this.angularCmp.destroy(); + } + } + + render() { + return
this.element = element} />; + } +} diff --git a/public/app/features/dashboard/components/DashboardSettings/index.ts b/public/app/features/dashboard/components/DashboardSettings/index.ts index f81b8cdbc67..0a89feada33 100644 --- a/public/app/features/dashboard/components/DashboardSettings/index.ts +++ b/public/app/features/dashboard/components/DashboardSettings/index.ts @@ -1 +1,2 @@ export { SettingsCtrl } from './SettingsCtrl'; +export { DashboardSettings } from './DashboardSettings'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index c0d5c4d4730..9b088b4735f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -1,14 +1,19 @@ // Libraries -import React, { Component } from 'react'; +import $ from 'jquery'; +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; +import classNames from 'classnames'; // Components import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; +import { DashNav } from '../components/DashNav'; +import { DashboardSettings } from '../components/DashboardSettings'; // Redux import { initDashboard } from '../state/initDashboard'; +import { setDashboardModel } from '../state/actions'; // Types import { StoreState } from 'app/types'; @@ -20,22 +25,23 @@ interface Props { urlUid?: string; urlSlug?: string; urlType?: string; + editview: string; $scope: any; $injector: any; initDashboard: typeof initDashboard; + setDashboardModel: typeof setDashboardModel; loadingState: DashboardLoadingState; dashboard: DashboardModel; } interface State { - dashboard: DashboardModel | null; - notFound: boolean; + isSettingsOpening: boolean; } -export class DashboardPage extends Component { +export class DashboardPage extends PureComponent { state: State = { - dashboard: null, - notFound: false, + isSettingsOpening: false, + isSettingsOpen: false, }; async componentDidMount() { @@ -45,18 +51,82 @@ export class DashboardPage extends Component { urlSlug: this.props.urlSlug, urlUid: this.props.urlUid, urlType: this.props.urlType, - }) + }); + } + + componentDidUpdate(prevProps: Props) { + const { dashboard, editview } = this.props; + + // when dashboard has loaded subscribe to somme events + if (prevProps.dashboard === null && dashboard) { + dashboard.events.on('view-mode-changed', this.onViewModeChanged); + + // set initial fullscreen class state + this.setPanelFullscreenClass(); + } + + if (!prevProps.editview && editview) { + this.setState({ isSettingsOpening: true }); + setTimeout(() => { + this.setState({ isSettingsOpening: false}); + }, 10); + } + } + + onViewModeChanged = () => { + this.setPanelFullscreenClass(); + }; + + setPanelFullscreenClass() { + $('body').toggleClass('panel-in-fullscreen', this.props.dashboard.meta.fullscreen === true); + } + + componentWillUnmount() { + if (this.props.dashboard) { + this.props.dashboard.destroy(); + this.props.setDashboardModel(null); + } + } + + renderLoadingState() { + return ; + } + + renderDashboard() { + const { dashboard, editview } = this.props; + + const classes = classNames({ + 'dashboard-container': true, + 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled + }); + + return ( +
+ {dashboard && editview && } + +
+ +
+
+ ); } render() { - const { loadingState, dashboard } = this.props; + const { dashboard, editview } = this.props; + const { isSettingsOpening } = this.state; - if (!dashboard) { - return ; - } + const classes = classNames({ + 'dashboard-page--settings-opening': isSettingsOpening, + 'dashboard-page--settings-open': !isSettingsOpening && editview, + }); - console.log(dashboard); - return + return ( +
+ + {!dashboard && this.renderLoadingState()} + {dashboard && this.renderDashboard()} +
+ ); } } @@ -65,12 +135,14 @@ const mapStateToProps = (state: StoreState) => ({ urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, panelId: state.location.query.panelId, + editview: state.location.query.editview, loadingState: state.dashboard.loadingState, dashboard: state.dashboard.model as DashboardModel, }); const mapDispatchToProps = { - initDashboard + initDashboard, + setDashboardModel }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index d20f9ae1cf8..10d7164fbff 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -67,6 +67,7 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini dashboard.updateSubmenuVisibility(); dashboard.autoFitPanels(window.innerHeight); + // init unsaved changes tracking injector.get('unsavedChangesSrv').init(dashboard, scope); scope.dashboard = dashboard; diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 5cfc879a1a4..2f4e5df5c14 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -31,7 +31,7 @@ export const dashboardReducer = reducerFactory(initialState) model: action.payload }), }) - .create() + .create(); export default { dashboard: dashboardReducer, diff --git a/public/sass/components/_dashboard_settings.scss b/public/sass/components/_dashboard_settings.scss index 5e17e025196..38883b7c80e 100644 --- a/public/sass/components/_dashboard_settings.scss +++ b/public/sass/components/_dashboard_settings.scss @@ -16,6 +16,9 @@ opacity: 1; transition: opacity 300ms ease-in-out; } + .dashboard-container { + display: none; + } } .dashboard-settings__content { From cba2ca55319cfb7a3bb009548b8f87c839a7e4b9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 12:29:47 +0100 Subject: [PATCH 014/228] Url state -> dashboard model state sync starting to work --- .../dashboard/components/DashNav/DashNav.tsx | 3 + .../components/DashNav/template.html | 2 - .../dashboard/containers/DashboardPage.tsx | 124 +++++++++++++----- .../dashboard/dashgrid/DashboardGrid.tsx | 41 ++++-- .../services/DashboardViewStateSrv.ts | 30 ----- public/app/routes/GrafanaCtrl.ts | 2 + public/app/types/dashboard.ts | 4 + public/views/index-template.html | 2 +- 8 files changed, 128 insertions(+), 80 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index e1fb70e5d68..f9df483bf5f 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -13,6 +13,9 @@ import { DashboardModel } from '../../state/DashboardModel'; export interface Props { dashboard: DashboardModel | null; + editview: string; + isEditing: boolean; + isFullscreen: boolean; updateLocation: typeof updateLocation; } diff --git a/public/app/features/dashboard/components/DashNav/template.html b/public/app/features/dashboard/components/DashNav/template.html index e50a8cd0bff..7e53267cbfd 100644 --- a/public/app/features/dashboard/components/DashNav/template.html +++ b/public/app/features/dashboard/components/DashNav/template.html @@ -55,7 +55,5 @@
- - diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 9b088b4735f..281916acb14 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -5,6 +5,9 @@ import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import classNames from 'classnames'; +// Services & Utils +import { createErrorNotification } from 'app/core/copy/appNotification'; + // Components import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; @@ -14,34 +17,45 @@ import { DashboardSettings } from '../components/DashboardSettings'; // Redux import { initDashboard } from '../state/initDashboard'; import { setDashboardModel } from '../state/actions'; +import { updateLocation } from 'app/core/actions'; +import { notifyApp } from 'app/core/actions'; // Types import { StoreState } from 'app/types'; -import { DashboardModel } from 'app/features/dashboard/state'; +import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; import { DashboardLoadingState } from 'app/types/dashboard'; interface Props { - panelId: string; urlUid?: string; urlSlug?: string; urlType?: string; - editview: string; + editview?: string; + urlPanelId?: string; $scope: any; $injector: any; - initDashboard: typeof initDashboard; - setDashboardModel: typeof setDashboardModel; + urlEdit: boolean; + urlFullscreen: boolean; loadingState: DashboardLoadingState; dashboard: DashboardModel; + initDashboard: typeof initDashboard; + setDashboardModel: typeof setDashboardModel; + notifyApp: typeof notifyApp; + updateLocation: typeof updateLocation; } interface State { isSettingsOpening: boolean; + isEditing: boolean; + isFullscreen: boolean; + fullscreenPanel: PanelModel | null; } export class DashboardPage extends PureComponent { state: State = { isSettingsOpening: false, - isSettingsOpen: false, + isEditing: false, + isFullscreen: false, + fullscreenPanel: null, }; async componentDidMount() { @@ -55,30 +69,66 @@ export class DashboardPage extends PureComponent { } componentDidUpdate(prevProps: Props) { - const { dashboard, editview } = this.props; + const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props; - // when dashboard has loaded subscribe to somme events - if (prevProps.dashboard === null && dashboard) { - dashboard.events.on('view-mode-changed', this.onViewModeChanged); - - // set initial fullscreen class state - this.setPanelFullscreenClass(); + if (!dashboard) { + return; } + // handle animation states when opening dashboard settings if (!prevProps.editview && editview) { this.setState({ isSettingsOpening: true }); setTimeout(() => { - this.setState({ isSettingsOpening: false}); + this.setState({ isSettingsOpening: false }); }, 10); } + + // // when dashboard has loaded subscribe to somme events + // if (prevProps.dashboard === null) { + // // set initial fullscreen class state + // this.setPanelFullscreenClass(); + // } + + // Sync url state with model + if (urlFullscreen !== dashboard.meta.isFullscreen || urlEdit !== dashboard.meta.isEditing) { + // entering fullscreen/edit mode + if (urlPanelId) { + const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + + if (panel) { + dashboard.setViewMode(panel, urlFullscreen, urlEdit); + this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel }); + } else { + this.handleFullscreenPanelNotFound(urlPanelId); + } + } else { + // handle leaving fullscreen mode + if (this.state.fullscreenPanel) { + dashboard.setViewMode(this.state.fullscreenPanel, urlFullscreen, urlEdit); + } + this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: null }); + } + + this.setPanelFullscreenClass(urlFullscreen); + } } - onViewModeChanged = () => { - this.setPanelFullscreenClass(); - }; + handleFullscreenPanelNotFound(urlPanelId: string) { + // Panel not found + this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`)); + // Clear url state + this.props.updateLocation({ + query: { + edit: null, + fullscreen: null, + panelId: null, + }, + partial: true + }); + } - setPanelFullscreenClass() { - $('body').toggleClass('panel-in-fullscreen', this.props.dashboard.meta.fullscreen === true); + setPanelFullscreenClass(isFullscreen: boolean) { + $('body').toggleClass('panel-in-fullscreen', isFullscreen); } componentWillUnmount() { @@ -94,10 +144,11 @@ export class DashboardPage extends PureComponent { renderDashboard() { const { dashboard, editview } = this.props; + const { isEditing, isFullscreen } = this.state; const classes = classNames({ 'dashboard-container': true, - 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled + 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, }); return ( @@ -105,7 +156,7 @@ export class DashboardPage extends PureComponent { {dashboard && editview && }
- +
); @@ -113,7 +164,7 @@ export class DashboardPage extends PureComponent { render() { const { dashboard, editview } = this.props; - const { isSettingsOpening } = this.state; + const { isSettingsOpening, isEditing, isFullscreen } = this.state; const classes = classNames({ 'dashboard-page--settings-opening': isSettingsOpening, @@ -122,7 +173,7 @@ export class DashboardPage extends PureComponent { return (
- + {!dashboard && this.renderLoadingState()} {dashboard && this.renderDashboard()}
@@ -130,19 +181,26 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => ({ - urlUid: state.location.routeParams.uid, - urlSlug: state.location.routeParams.slug, - urlType: state.location.routeParams.type, - panelId: state.location.query.panelId, - editview: state.location.query.editview, - loadingState: state.dashboard.loadingState, - dashboard: state.dashboard.model as DashboardModel, -}); +const mapStateToProps = (state: StoreState) => { + console.log('state location', state.location.query); + return { + urlUid: state.location.routeParams.uid, + urlSlug: state.location.routeParams.slug, + urlType: state.location.routeParams.type, + editview: state.location.query.editview, + urlPanelId: state.location.query.panelId, + urlFullscreen: state.location.query.fullscreen === true, + urlEdit: state.location.query.edit === true, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard.model as DashboardModel, + }; +}; const mapDispatchToProps = { initDashboard, - setDashboardModel + setDashboardModel, + notifyApp, + updateLocation, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(DashboardPage)); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 658bfad3816..27f699ff3e6 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -1,11 +1,14 @@ -import React from 'react'; +// Libaries +import React, { PureComponent } from 'react'; import { hot } from 'react-hot-loader'; import ReactGridLayout, { ItemCallback } from 'react-grid-layout'; +import classNames from 'classnames'; +import sizeMe from 'react-sizeme'; + +// Types import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core/constants'; import { DashboardPanel } from './DashboardPanel'; import { DashboardModel, PanelModel } from '../state'; -import classNames from 'classnames'; -import sizeMe from 'react-sizeme'; let lastGridWidth = 1200; let ignoreNextWidthChange = false; @@ -76,19 +79,18 @@ function GridWrapper({ const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper); -export interface DashboardGridProps { +export interface Props { dashboard: DashboardModel; + isEditing: boolean; + isFullscreen: boolean; } -export class DashboardGrid extends React.Component { +export class DashboardGrid extends PureComponent { gridToPanelMap: any; panelMap: { [id: string]: PanelModel }; - constructor(props: DashboardGridProps) { - super(props); - - // subscribe to dashboard events - const dashboard = this.props.dashboard; + componentDidMount() { + const { dashboard } = this.props; dashboard.on('panel-added', this.triggerForceUpdate); dashboard.on('panel-removed', this.triggerForceUpdate); dashboard.on('repeats-processed', this.triggerForceUpdate); @@ -97,6 +99,16 @@ export class DashboardGrid extends React.Component { dashboard.on('row-expanded', this.triggerForceUpdate); } + componentWillUnmount() { + const { dashboard } = this.props; + dashboard.off('panel-added', this.triggerForceUpdate); + dashboard.off('panel-removed', this.triggerForceUpdate); + dashboard.off('repeats-processed', this.triggerForceUpdate); + dashboard.off('view-mode-changed', this.onViewModeChanged); + dashboard.off('row-collapsed', this.triggerForceUpdate); + dashboard.off('row-expanded', this.triggerForceUpdate); + } + buildLayout() { const layout = []; this.panelMap = {}; @@ -151,7 +163,6 @@ export class DashboardGrid extends React.Component { onViewModeChanged = () => { ignoreNextWidthChange = true; - this.forceUpdate(); } updateGridPos = (item: ReactGridLayout.Layout, layout: ReactGridLayout.Layout[]) => { @@ -197,18 +208,20 @@ export class DashboardGrid extends React.Component { } render() { + const { dashboard, isFullscreen } = this.props; + return ( {this.renderPanels()} diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts index fc38c3b241f..f5a68d6f647 100644 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ b/public/app/features/dashboard/services/DashboardViewStateSrv.ts @@ -98,8 +98,6 @@ export class DashboardViewStateSrv { if (fromRouteUpdated !== true) { this.$location.search(this.serializeToUrl()); } - - this.syncState(); } toggleCollapsedPanelRow(panelId) { @@ -115,34 +113,6 @@ export class DashboardViewStateSrv { } } - syncState() { - if (this.state.fullscreen) { - const panel = this.dashboard.getPanelById(this.state.panelId); - - if (!panel) { - this.state.fullscreen = null; - this.state.panelId = null; - this.state.edit = null; - - this.update(this.state); - - setTimeout(() => { - appEvents.emit('alert-error', ['Error', 'Panel not found']); - }, 100); - - return; - } - - if (!panel.fullscreen) { - this.enterFullscreen(panel); - } else if (this.dashboard.meta.isEditing !== this.state.edit) { - this.dashboard.setViewMode(panel, this.state.fullscreen, this.state.edit); - } - } else if (this.fullscreenPanel) { - this.leaveFullscreen(); - } - } - leaveFullscreen() { const panel = this.fullscreenPanel; diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 70bdf49e5e4..817e6452f44 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -165,6 +165,8 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop for (const drop of Drop.drops) { drop.destroy(); } + + appEvents.emit('hide-dash-search'); }); // handle kiosk mode diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index bdea5b04bac..713cd28efb1 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -1,6 +1,10 @@ import { DashboardAcl } from './acl'; export interface MutableDashboard { + meta: { + fullscreen: boolean; + isEditing: boolean; + } } export enum DashboardLoadingState { diff --git a/public/views/index-template.html b/public/views/index-template.html index a1c955d45d6..770ab74eccc 100644 --- a/public/views/index-template.html +++ b/public/views/index-template.html @@ -189,7 +189,7 @@ - +
From 2cb1733c59af3643bda1b03ca187c230120f9c16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 14:53:42 +0100 Subject: [PATCH 015/228] wip: progress --- .../dashboard/components/DashNav/DashNav.tsx | 99 ++++++++++++------- .../dashboard/containers/DashboardPage.tsx | 16 ++- .../app/features/dashboard/state/actions.ts | 3 +- .../features/dashboard/state/initDashboard.ts | 60 ++++++++--- public/app/routes/routes.ts | 24 +++-- public/app/types/dashboard.ts | 2 +- 6 files changed, 138 insertions(+), 66 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index f9df483bf5f..79d4da94aca 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -16,6 +16,7 @@ export interface Props { editview: string; isEditing: boolean; isFullscreen: boolean; + $injector: any; updateLocation: typeof updateLocation; } @@ -25,13 +26,29 @@ export class DashNav extends PureComponent { }; onAddPanel = () => {}; + + onClose = () => { + this.props.updateLocation({ + query: { editview: null, panelId: null, edit: null, fullscreen: null }, + partial: true, + }); + }; + onOpenSettings = () => { this.props.updateLocation({ - query: { - editview: 'settings', - }, + query: { editview: 'settings' }, partial: true, - }) + }); + }; + + onStarDashboard = () => { + const { $injector, dashboard } = this.props; + const dashboardSrv = $injector.get('dashboardSrv'); + + dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then(newState => { + dashboard.meta.isStarred = newState; + this.forceUpdate(); + }); }; renderLoadingState() { @@ -48,15 +65,16 @@ export class DashNav extends PureComponent { ); } + render() { - let { dashboard } = this.props; + const { dashboard, isFullscreen, editview } = this.props; if (!dashboard) { return this.renderLoadingState(); } const haveFolder = dashboard.meta.folderId > 0; - const { canEdit, canSave, folderTitle, showSettings } = dashboard.meta; + const { canEdit, canStar, canSave, folderTitle, showSettings, isStarred } = dashboard.meta; return (
@@ -95,53 +113,66 @@ export class DashNav extends PureComponent { )} + {canStar && ( + + )} + { - // // // + // + // - // - // // // - // - // + // + // // // - //
- // - // - // - // - // - // + // + // + // + // + // } + {(isFullscreen || editview) && ( +
+ +
+ )}
); } } -const mapStateToProps = () => ({ -}); +const mapStateToProps = () => ({}); const mapDispatchToProps = { - updateLocation + updateLocation, }; export default connect(mapStateToProps, mapDispatchToProps)(DashNav); diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 281916acb14..9f0f1cdff5f 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -60,8 +60,8 @@ export class DashboardPage extends PureComponent { async componentDidMount() { this.props.initDashboard({ - injector: this.props.$injector, - scope: this.props.$scope, + $injector: this.props.$injector, + $scope: this.props.$scope, urlSlug: this.props.urlSlug, urlUid: this.props.urlUid, urlType: this.props.urlType, @@ -123,7 +123,7 @@ export class DashboardPage extends PureComponent { fullscreen: null, panelId: null, }, - partial: true + partial: true, }); } @@ -163,7 +163,7 @@ export class DashboardPage extends PureComponent { } render() { - const { dashboard, editview } = this.props; + const { dashboard, editview, $injector } = this.props; const { isSettingsOpening, isEditing, isFullscreen } = this.state; const classes = classNames({ @@ -173,7 +173,13 @@ export class DashboardPage extends PureComponent { return (
- + {!dashboard && this.renderLoadingState()} {dashboard && this.renderDashboard()}
diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index 14721cdbe96..bc57b8e5f10 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -1,5 +1,4 @@ // Libaries -import { StoreState } from 'app/types'; import { ThunkAction } from 'redux-thunk'; // Services & Utils @@ -13,6 +12,7 @@ import { loadPluginDashboards } from '../../plugins/state/actions'; import { notifyApp } from 'app/core/actions'; // Types +import { StoreState } from 'app/types'; import { DashboardAcl, DashboardAclDTO, @@ -27,7 +27,6 @@ export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_MODEL').create(); export type Action = ActionOf; - export type ThunkResult = ThunkAction; export function getDashboardPermissions(id: number): ThunkResult { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 10d7164fbff..f7e23238b7c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,5 +1,6 @@ // Services & Utils import { createErrorNotification } from 'app/core/copy/appNotification'; +import { getBackendSrv } from 'app/core/services/backend_srv'; // Actions import { updateLocation } from 'app/core/actions'; @@ -12,24 +13,53 @@ import { DashboardLoadingState } from 'app/types/dashboard'; import { DashboardModel } from './DashboardModel'; export interface InitDashboardArgs { - injector: any; - scope: any; + $injector: any; + $scope: any; urlUid?: string; urlSlug?: string; urlType?: string; } -export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { - return async dispatch => { - const loaderSrv = injector.get('dashboardLoaderSrv'); +async function redirectToNewUrl(slug: string, dispatch: any) { + const res = await getBackendSrv().getDashboardBySlug(slug); - dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + if (res) { + const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); + dispatch(updateLocation(url)); + } +} + +export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { + return async dispatch => { + // handle old urls with no uid + if (!urlUid && urlSlug) { + redirectToNewUrl(urlSlug, dispatch); + return; + } let dashDTO = null; + // set fetching state + dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + try { - // fetch dashboard from api - dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + // if no uid or slug, load home dashboard + if (!urlUid && !urlSlug) { + dashDTO = await getBackendSrv().get('/api/dashboards/home'); + + if (dashDTO.redirectUri) { + const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri); + dispatch(updateLocation({ path: newUrl })); + return; + } else { + dashDTO.meta.canSave = false; + dashDTO.meta.canShare = false; + dashDTO.meta.canStar = false; + } + } else { + const loaderSrv = $injector.get('dashboardLoaderSrv'); + dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + } } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); console.log(err); @@ -50,13 +80,13 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini } // init services - injector.get('timeSrv').init(dashboard); - injector.get('annotationsSrv').init(dashboard); + $injector.get('timeSrv').init(dashboard); + $injector.get('annotationsSrv').init(dashboard); // template values service needs to initialize completely before // the rest of the dashboard can load try { - await injector.get('variableSrv').init(dashboard); + await $injector.get('variableSrv').init(dashboard); } catch (err) { dispatch(notifyApp(createErrorNotification('Templating init failed', err.toString()))); console.log(err); @@ -68,11 +98,11 @@ export function initDashboard({ injector, scope, urlUid, urlSlug, urlType }: Ini dashboard.autoFitPanels(window.innerHeight); // init unsaved changes tracking - injector.get('unsavedChangesSrv').init(dashboard, scope); + $injector.get('unsavedChangesSrv').init(dashboard, $scope); - scope.dashboard = dashboard; - injector.get('dashboardViewStateSrv').create(scope); - injector.get('keybindingSrv').setupDashboardBindings(scope, dashboard); + $scope.dashboard = dashboard; + $injector.get('dashboardViewStateSrv').create($scope); + $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard); } catch (err) { dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); console.log(err); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index cdd9ed89a08..abe347d689a 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -29,10 +29,12 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { $routeProvider .when('/', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/d/:uid/:slug', { template: '', @@ -43,16 +45,20 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { }, }) .when('/d/:uid', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/dashboard/:type/:slug', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'LoadDashboardCtrl', - reloadOnSearch: false, + template: '', pageClass: 'page-dashboard', + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, }) .when('/d-solo/:uid/:slug', { template: '', diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 713cd28efb1..9b1e750e859 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -4,7 +4,7 @@ export interface MutableDashboard { meta: { fullscreen: boolean; isEditing: boolean; - } + }; } export enum DashboardLoadingState { From 09efa24f281c69998865efc627fa278a50187e72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 15:29:14 +0100 Subject: [PATCH 016/228] Added more buttons in dashboard nav --- .../dashboard/components/DashNav/DashNav.tsx | 124 +++++++++++------- .../dashboard/containers/DashboardPage.tsx | 17 ++- .../features/dashboard/state/initDashboard.ts | 2 +- 3 files changed, 95 insertions(+), 48 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 79d4da94aca..e82fa0ba75e 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -12,7 +12,7 @@ import { updateLocation } from 'app/core/actions'; import { DashboardModel } from '../../state/DashboardModel'; export interface Props { - dashboard: DashboardModel | null; + dashboard: DashboardModel; editview: string; isEditing: boolean; isFullscreen: boolean; @@ -25,7 +25,20 @@ export class DashNav extends PureComponent { appEvents.emit('show-dash-search'); }; - onAddPanel = () => {}; + onAddPanel = () => { + const { dashboard } = this.props; + + // Return if the "Add panel" exists already + if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') { + return; + } + + dashboard.addPanel({ + type: 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + title: 'Panel Title', + }); + }; onClose = () => { this.props.updateLocation({ @@ -34,6 +47,16 @@ export class DashNav extends PureComponent { }); }; + onToggleTVMode = () => { + appEvents.emit('toggle-kiosk-mode'); + }; + + onSave = () => { + const { $injector } = this.props; + const dashboardSrv = $injector.get('dashboardSrv'); + dashboardSrv.saveDashboard(); + }; + onOpenSettings = () => { this.props.updateLocation({ query: { editview: 'settings' }, @@ -51,30 +74,25 @@ export class DashNav extends PureComponent { }); }; - renderLoadingState() { - return ( - - ); - } + onOpenShare = () => { + const $rootScope = this.props.$injector.get('$rootScope'); + const modalScope = $rootScope.$new(); + modalScope.tabIndex = 0; + modalScope.dashboard = this.props.dashboard; + appEvents.emit('show-modal', { + src: 'public/app/features/dashboard/components/ShareModal/template.html', + scope: modalScope, + }); + }; render() { const { dashboard, isFullscreen, editview } = this.props; - - if (!dashboard) { - return this.renderLoadingState(); - } + const { canEdit, canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; + const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; - const { canEdit, canStar, canSave, folderTitle, showSettings, isStarred } = dashboard.meta; + const snapshotUrl = snapshot && snapshot.originalUrl; return (
@@ -124,34 +142,50 @@ export class DashNav extends PureComponent { )} + {canShare && ( + + )} + + {canSave && ( + + )} + + {snapshotUrl && ( + + + + )} + +
+ +
+ { - // - // - // - // - // - // - // - // - // - // - //
- // - // - // // - // } + {(isFullscreen || editview) && (
-
+
+ +
+ + { + // + } + + {(isFullscreen || editview) && ( +
- - { - // - } - - {(isFullscreen || editview) && ( -
- -
- )} -
+ )} ); } diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 929b984a93f..8d96a2eec73 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -1,20 +1,25 @@ +// Libaries import moment from 'moment'; import _ from 'lodash'; -import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; +// Constants +import { DEFAULT_ANNOTATION_COLOR } from '@grafana/ui'; import { GRID_COLUMN_COUNT, REPEAT_DIR_VERTICAL, GRID_CELL_HEIGHT, GRID_CELL_VMARGIN } from 'app/core/constants'; + +// Utils & Services import { Emitter } from 'app/core/utils/emitter'; import { contextSrv } from 'app/core/services/context_srv'; import sortByKeys from 'app/core/utils/sort_by_keys'; +// Types import { PanelModel } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; import { TimeRange } from '@grafana/ui/src'; export class DashboardModel { id: any; - uid: any; - title: any; + uid: string; + title: string; autoUpdate: any; description: any; tags: any; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 19727cd8ab0..01092617e2f 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -7,6 +7,7 @@ import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; import locationUtil from 'app/core/utils/location_util'; import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './actions'; +import { removePanel } from '../utils/panel'; // Types import { DashboardLoadingState } from 'app/types/dashboard'; @@ -102,7 +103,15 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I $scope.dashboard = dashboard; $injector.get('dashboardViewStateSrv').create($scope); - $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard); + + // dashboard keybindings should not live in core, this needs a bigger refactoring + // So declaring this here so it can depend on the removePanel util function + // Long term onRemovePanel should be handled via react prop callback + const onRemovePanel = (panelId: number) => { + removePanel(dashboard, dashboard.getPanelById(panelId), true); + }; + + $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard, onRemovePanel); } catch (err) { dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); console.log(err); diff --git a/public/app/features/panel/panel_ctrl.ts b/public/app/features/panel/panel_ctrl.ts index 6799b209147..41810ab21c0 100644 --- a/public/app/features/panel/panel_ctrl.ts +++ b/public/app/features/panel/panel_ctrl.ts @@ -7,6 +7,7 @@ import { Emitter } from 'app/core/core'; import getFactors from 'app/core/utils/factors'; import { duplicatePanel, + removePanel, copyPanel as copyPanelUtil, editPanelJson as editPanelJsonUtil, sharePanel as sharePanelUtil, @@ -213,9 +214,7 @@ export class PanelCtrl { } removePanel() { - this.publishAppEvent('panel-remove', { - panelId: this.panel.id, - }); + removePanel(this.dashboard, this.panel, true); } editPanelJson() { From d7151e5c887082746dbd42cfcdcb4550a50cee30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 18:25:13 +0100 Subject: [PATCH 018/228] improving dash nav react comp --- .../dashboard/components/DashNav/DashNav.tsx | 47 ++++++++++++------- .../components/DashNav/DashNavButton.tsx | 22 +++++++++ .../features/dashboard/state/initDashboard.ts | 2 +- 3 files changed, 53 insertions(+), 18 deletions(-) create mode 100644 public/app/features/dashboard/components/DashNav/DashNavButton.tsx diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 4513d15ebb7..d6ee9ae1f68 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -5,6 +5,9 @@ import { connect } from 'react-redux'; // Utils & Services import { appEvents } from 'app/core/app_events'; +// Components +import { DashNavButton } from './DashNavButton'; + // State import { updateLocation } from 'app/core/actions'; @@ -41,10 +44,17 @@ export class DashNav extends PureComponent { }; onClose = () => { - this.props.updateLocation({ - query: { editview: null, panelId: null, edit: null, fullscreen: null }, - partial: true, - }); + if (this.props.editview) { + this.props.updateLocation({ + query: { editview: null }, + partial: true, + }); + } else { + this.props.updateLocation({ + query: { panelId: null, edit: null, fullscreen: null }, + partial: true, + }); + } }; onToggleTVMode = () => { @@ -116,19 +126,12 @@ export class DashNav extends PureComponent {
{canEdit && ( - - )} - - {showSettings && ( - + )} {canStar && ( @@ -171,6 +174,16 @@ export class DashNav extends PureComponent { )} + + {showSettings && ( + + )}
diff --git a/public/app/features/dashboard/components/DashNav/DashNavButton.tsx b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx new file mode 100644 index 00000000000..1a98bf961dc --- /dev/null +++ b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx @@ -0,0 +1,22 @@ +// Libraries +import React, { FunctionComponent } from 'react'; + +// Components +import { Tooltip } from '@grafana/ui'; + +interface Props { + icon: string; + tooltip: string; + classSuffix: string; + onClick: () => void; +} + +export const DashNavButton: FunctionComponent = ({ icon, tooltip, classSuffix, onClick }) => { + return ( + + + + ); +}; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 01092617e2f..612ce46b422 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -33,7 +33,7 @@ async function redirectToNewUrl(slug: string, dispatch: any) { export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { return async dispatch => { // handle old urls with no uid - if (!urlUid && urlSlug) { + if (!urlUid && urlSlug && !urlType) { redirectToNewUrl(urlSlug, dispatch); return; } From 1f3fafb198fc47e143e81afaa5bf497e1059f401 Mon Sep 17 00:00:00 2001 From: Paresh Date: Sun, 3 Feb 2019 13:07:33 -0600 Subject: [PATCH 019/228] mssql: pass timerange for template variable queries --- .../app/plugins/datasource/mssql/datasource.ts | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/datasource/mssql/datasource.ts b/public/app/plugins/datasource/mssql/datasource.ts index 23aa5504d3e..1ede9cc3d1e 100644 --- a/public/app/plugins/datasource/mssql/datasource.ts +++ b/public/app/plugins/datasource/mssql/datasource.ts @@ -107,13 +107,24 @@ export class MssqlDatasource { format: 'table', }; + const data = { + queries: [interpolatedQuery], + }; + + if (optionalOptions && optionalOptions.range) { + if (optionalOptions.range.from) { + data['from'] = optionalOptions.range.from.valueOf().toString(); + } + if (optionalOptions.range.to) { + data['to'] = optionalOptions.range.to.valueOf().toString(); + } + } + return this.backendSrv .datasourceRequest({ url: '/api/tsdb/query', method: 'POST', - data: { - queries: [interpolatedQuery], - }, + data: data, }) .then(data => this.responseParser.parseMetricFindQueryResult(refId, data)); } From 0324de37d2caa63fac56746ad0dbcea53bea83a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 20:38:13 +0100 Subject: [PATCH 020/228] refactorings and cleanup --- .../dashboard/components/DashNav/DashNav.tsx | 71 ++++++++----------- .../components/DashNav/DashNavButton.tsx | 21 ++++-- .../features/dashboard/state/initDashboard.ts | 28 ++++++-- 3 files changed, 67 insertions(+), 53 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index d6ee9ae1f68..559e3e1d66f 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -135,61 +135,49 @@ export class DashNav extends PureComponent { )} {canStar && ( - + /> )} {canShare && ( - + /> )} {canSave && ( - + )} {snapshotUrl && ( - - - + /> )} {showSettings && ( - + )}
- +
{ @@ -198,13 +186,12 @@ export class DashNav extends PureComponent { {(isFullscreen || editview) && (
- + />
)} diff --git a/public/app/features/dashboard/components/DashNav/DashNavButton.tsx b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx index 1a98bf961dc..505baaf1f5d 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavButton.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNavButton.tsx @@ -8,15 +8,26 @@ interface Props { icon: string; tooltip: string; classSuffix: string; - onClick: () => void; + onClick?: () => void; + href?: string; } -export const DashNavButton: FunctionComponent = ({ icon, tooltip, classSuffix, onClick }) => { +export const DashNavButton: FunctionComponent = ({ icon, tooltip, classSuffix, onClick, href }) => { + if (onClick) { + return ( + + + + ); + } + return ( - - + ); }; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 612ce46b422..a39a7fce285 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -1,6 +1,11 @@ // Services & Utils import { createErrorNotification } from 'app/core/copy/appNotification'; import { getBackendSrv } from 'app/core/services/backend_srv'; +import { DashboardSrv } from 'app/features/dashboard/services/DashboardSrv'; +import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; +import { AnnotationsSrv } from 'app/features/annotations/annotations_srv'; +import { VariableSrv } from 'app/features/templating/variable_srv'; +import { KeybindingSrv } from 'app/core/services/keybindingSrv'; // Actions import { updateLocation } from 'app/core/actions'; @@ -81,13 +86,21 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I } // init services - $injector.get('timeSrv').init(dashboard); - $injector.get('annotationsSrv').init(dashboard); + const timeSrv: TimeSrv = $injector.get('timeSrv'); + const annotationsSrv: AnnotationsSrv = $injector.get('annotationsSrv'); + const variableSrv: VariableSrv = $injector.get('variableSrv'); + const keybindingSrv: KeybindingSrv = $injector.get('keybindingSrv'); + const unsavedChangesSrv = $injector.get('unsavedChangesSrv'); + const viewStateSrv = $injector.get('dashboardViewStateSrv'); + const dashboardSrv: DashboardSrv = $injector.get('dashboardSrv'); + + timeSrv.init(dashboard); + annotationsSrv.init(dashboard); // template values service needs to initialize completely before // the rest of the dashboard can load try { - await $injector.get('variableSrv').init(dashboard); + await variableSrv.init(dashboard); } catch (err) { dispatch(notifyApp(createErrorNotification('Templating init failed'))); console.log(err); @@ -99,10 +112,10 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I dashboard.autoFitPanels(window.innerHeight); // init unsaved changes tracking - $injector.get('unsavedChangesSrv').init(dashboard, $scope); + unsavedChangesSrv.init(dashboard, $scope); $scope.dashboard = dashboard; - $injector.get('dashboardViewStateSrv').create($scope); + viewStateSrv.create($scope); // dashboard keybindings should not live in core, this needs a bigger refactoring // So declaring this here so it can depend on the removePanel util function @@ -111,12 +124,15 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I removePanel(dashboard, dashboard.getPanelById(panelId), true); }; - $injector.get('keybindingSrv').setupDashboardBindings($scope, dashboard, onRemovePanel); + keybindingSrv.setupDashboardBindings($scope, dashboard, onRemovePanel); } catch (err) { dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); console.log(err); } + // legacy srv state + dashboardSrv.setCurrent(dashboard); + // set model in redux (even though it's mutable) dispatch(setDashboardModel(dashboard)); }; } From 883f7a164b455d3f5f9b7ff23252d37e6a1a9da3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Feb 2019 21:06:07 +0100 Subject: [PATCH 021/228] added time picker --- .../dashboard/components/DashNav/DashNav.tsx | 35 +++++++++++++++---- .../dashboard/containers/DashboardPage.tsx | 25 ++++++------- public/sass/components/_navbar.scss | 2 +- 3 files changed, 40 insertions(+), 22 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 559e3e1d66f..66edb149433 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -3,6 +3,7 @@ import React, { PureComponent } from 'react'; import { connect } from 'react-redux'; // Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import { appEvents } from 'app/core/app_events'; // Components @@ -24,6 +25,24 @@ export interface Props { } export class DashNav extends PureComponent { + timePickerEl: HTMLElement; + timepickerCmp: AngularComponent; + + componentDidMount() { + const loader = getAngularLoader(); + + const template = ''; + const scopeProps = { dashboard: this.props.dashboard }; + + this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template); + } + + componentWillUnmount() { + if (this.timepickerCmp) { + this.timepickerCmp.destroy(); + } + } + onOpenSearch = () => { appEvents.emit('show-dash-search'); }; @@ -98,7 +117,7 @@ export class DashNav extends PureComponent { render() { const { dashboard, isFullscreen, editview } = this.props; - const { canEdit, canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; + const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; @@ -125,7 +144,7 @@ export class DashNav extends PureComponent { */}
- {canEdit && ( + {canSave && ( { )} {showSettings && ( - + )}
@@ -180,9 +203,7 @@ export class DashNav extends PureComponent { /> - { - // - } +
(this.timePickerEl = element)} /> {(isFullscreen || editview) && (
diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index d86d5aea221..be12657a829 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -200,20 +200,17 @@ export class DashboardPage extends PureComponent { } } -const mapStateToProps = (state: StoreState) => { - console.log('state location', state.location.query); - return { - urlUid: state.location.routeParams.uid, - urlSlug: state.location.routeParams.slug, - urlType: state.location.routeParams.type, - editview: state.location.query.editview, - urlPanelId: state.location.query.panelId, - urlFullscreen: state.location.query.fullscreen === true, - urlEdit: state.location.query.edit === true, - loadingState: state.dashboard.loadingState, - dashboard: state.dashboard.model as DashboardModel, - }; -}; +const mapStateToProps = (state: StoreState) => ({ + urlUid: state.location.routeParams.uid, + urlSlug: state.location.routeParams.slug, + urlType: state.location.routeParams.type, + editview: state.location.query.editview, + urlPanelId: state.location.query.panelId, + urlFullscreen: state.location.query.fullscreen === true, + urlEdit: state.location.query.edit === true, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard.model as DashboardModel, +}); const mapDispatchToProps = { initDashboard, diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index b3733b694fc..0744ed0dfc7 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -102,7 +102,7 @@ display: flex; align-items: center; justify-content: flex-end; - margin-right: $spacer; + margin-left: 10px; &--close { display: none; From 217468074f6f8f420554cbed203db97921324e17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 11:19:45 +0100 Subject: [PATCH 022/228] added submenu, made sure submenu visibility is always up to date --- .../app/features/annotations/editor_ctrl.ts | 7 +++- .../DashLinks/DashLinksEditorCtrl.ts | 4 ++- .../components/DashNav/DashNavCtrl.ts | 2 -- .../dashboard/components/SubMenu/SubMenu.tsx | 36 +++++++++++++++++++ .../dashboard/components/SubMenu/index.ts | 1 + .../dashboard/containers/DashboardPage.tsx | 2 ++ .../dashboard/services/DashboardSrv.ts | 7 ++-- .../app/features/explore/ExploreToolbar.tsx | 4 +-- public/sass/components/_navbar.scss | 3 +- 9 files changed, 56 insertions(+), 10 deletions(-) create mode 100644 public/app/features/dashboard/components/SubMenu/SubMenu.tsx diff --git a/public/app/features/annotations/editor_ctrl.ts b/public/app/features/annotations/editor_ctrl.ts index 18b00793ff8..c12e442f6d3 100644 --- a/public/app/features/annotations/editor_ctrl.ts +++ b/public/app/features/annotations/editor_ctrl.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import coreModule from 'app/core/core_module'; +import { DashboardModel } from 'app/features/dashboard/state'; export class AnnotationsEditorCtrl { mode: any; @@ -10,6 +11,7 @@ export class AnnotationsEditorCtrl { currentAnnotation: any; currentDatasource: any; currentIsNew: any; + dashboard: DashboardModel; annotationDefaults: any = { name: '', @@ -26,9 +28,10 @@ export class AnnotationsEditorCtrl { constructor($scope, private datasourceSrv) { $scope.ctrl = this; + this.dashboard = $scope.dashboard; this.mode = 'list'; this.datasources = datasourceSrv.getAnnotationSources(); - this.annotations = $scope.dashboard.annotations.list; + this.annotations = this.dashboard.annotations.list; this.reset(); this.onColorChange = this.onColorChange.bind(this); @@ -78,11 +81,13 @@ export class AnnotationsEditorCtrl { this.annotations.push(this.currentAnnotation); this.reset(); this.mode = 'list'; + this.dashboard.updateSubmenuVisibility(); } removeAnnotation(annotation) { const index = _.indexOf(this.annotations, annotation); this.annotations.splice(index, 1); + this.dashboard.updateSubmenuVisibility(); } onColorChange(newColor) { diff --git a/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts b/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts index 398ad757bf3..339c8e7de4c 100644 --- a/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts +++ b/public/app/features/dashboard/components/DashLinks/DashLinksEditorCtrl.ts @@ -1,5 +1,6 @@ import angular from 'angular'; import _ from 'lodash'; +import { DashboardModel } from 'app/features/dashboard/state'; export let iconMap = { 'external link': 'fa-external-link', @@ -12,7 +13,7 @@ export let iconMap = { }; export class DashLinksEditorCtrl { - dashboard: any; + dashboard: DashboardModel; iconMap: any; mode: any; link: any; @@ -40,6 +41,7 @@ export class DashLinksEditorCtrl { addLink() { this.dashboard.links.push(this.link); this.mode = 'list'; + this.dashboard.updateSubmenuVisibility(); } editLink(link) { diff --git a/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts b/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts index e75c1468a1f..fbf84d354e3 100644 --- a/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts +++ b/public/app/features/dashboard/components/DashNav/DashNavCtrl.ts @@ -10,8 +10,6 @@ export class DashNavCtrl { /** @ngInject */ constructor(private $scope, private dashboardSrv, private $location, public playlistSrv) { - appEvents.on('save-dashboard', this.saveDashboard.bind(this), $scope); - if (this.dashboard.meta.isSnapshot) { const meta = this.dashboard.meta; this.titleTooltip = 'Created:  ' + moment(meta.created).calendar(); diff --git a/public/app/features/dashboard/components/SubMenu/SubMenu.tsx b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx new file mode 100644 index 00000000000..caef8f2de38 --- /dev/null +++ b/public/app/features/dashboard/components/SubMenu/SubMenu.tsx @@ -0,0 +1,36 @@ +// Libaries +import React, { PureComponent } from 'react'; + +// Utils & Services +import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; + +// Types +import { DashboardModel } from '../../state/DashboardModel'; + +export interface Props { + dashboard: DashboardModel | null; +} + +export class SubMenu extends PureComponent { + element: HTMLElement; + angularCmp: AngularComponent; + + componentDidMount() { + const loader = getAngularLoader(); + + const template = ''; + const scopeProps = { dashboard: this.props.dashboard }; + + this.angularCmp = loader.load(this.element, scopeProps, template); + } + + componentWillUnmount() { + if (this.angularCmp) { + this.angularCmp.destroy(); + } + } + + render() { + return
this.element = element} />; + } +} diff --git a/public/app/features/dashboard/components/SubMenu/index.ts b/public/app/features/dashboard/components/SubMenu/index.ts index 1790aa66782..ca113ab75d6 100644 --- a/public/app/features/dashboard/components/SubMenu/index.ts +++ b/public/app/features/dashboard/components/SubMenu/index.ts @@ -1 +1,2 @@ export { SubMenuCtrl } from './SubMenuCtrl'; +export { SubMenu } from './SubMenu'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index be12657a829..e01998f65a9 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -12,6 +12,7 @@ import { createErrorNotification } from 'app/core/copy/appNotification'; import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashNav } from '../components/DashNav'; +import { SubMenu } from '../components/SubMenu'; import { DashboardSettings } from '../components/DashboardSettings'; // Redux @@ -192,6 +193,7 @@ export class DashboardPage extends PureComponent { {dashboard && editview && }
+ {dashboard.meta.submenuEnabled && }
diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 03aeb34ed36..7d3dfb68cd8 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -1,12 +1,15 @@ import coreModule from 'app/core/core_module'; -import { DashboardModel } from '../state/DashboardModel'; +import { appEvents } from 'app/core/app_events'; import locationUtil from 'app/core/utils/location_util'; +import { DashboardModel } from '../state/DashboardModel'; export class DashboardSrv { dash: any; /** @ngInject */ - constructor(private backendSrv, private $rootScope, private $location) {} + constructor(private backendSrv, private $rootScope, private $location) { + appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); + } create(dashboard, meta) { return new DashboardModel(dashboard, meta); diff --git a/public/app/features/explore/ExploreToolbar.tsx b/public/app/features/explore/ExploreToolbar.tsx index 35f06d11c81..228cfb147e8 100644 --- a/public/app/features/explore/ExploreToolbar.tsx +++ b/public/app/features/explore/ExploreToolbar.tsx @@ -97,10 +97,10 @@ export class UnConnectedExploreToolbar extends PureComponent {
{exploreId === 'left' && ( - + Explore - + )}
diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index 0744ed0dfc7..0cfa314a985 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -83,8 +83,7 @@ font-size: 19px; line-height: 8px; opacity: 0.75; - margin-right: 8px; - // icon hidden on smaller screens + margin-right: 13px; display: none; } From f2d2712a9547ac8bcd6ef8b69f85d41d2d19dd51 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 30 Jan 2019 14:21:51 +0300 Subject: [PATCH 023/228] azuremonitor: add more builtin functions and operators --- .../editor/KustoQueryField.tsx | 26 +- .../editor/editor_component.tsx | 2 +- .../editor/kusto.ts | 114 ------ .../editor/kusto/kusto.ts | 355 ++++++++++++++++++ 4 files changed, 379 insertions(+), 118 deletions(-) delete mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts create mode 100644 public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 849cf62efe0..fa79d4bdb99 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -6,7 +6,7 @@ import QueryField from './query_field'; import debounce from 'app/features/explore/utils/debounce'; import { getNextCharacter } from 'app/features/explore/utils/dom'; -import { FUNCTIONS, KEYWORDS } from './kusto'; +import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto/kusto'; // import '../sass/editor.base.scss'; @@ -260,10 +260,20 @@ export default class KustoQueryField extends QueryField { label: 'Keywords', items: KEYWORDS.map(wrapText) }, + { + prefixMatch: true, + label: 'Operators', + items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + }, { prefixMatch: true, label: 'Functions', - items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; }) + items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Macros', + items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) } ]; } @@ -276,10 +286,20 @@ export default class KustoQueryField extends QueryField { label: 'Keywords', items: KEYWORDS.map(wrapText) }, + { + prefixMatch: true, + label: 'Operators', + items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + }, { prefixMatch: true, label: 'Functions', - items: FUNCTIONS.map((s: any) => { s.type = 'function'; return s; }) + items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Macros', + items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) } ]; } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx index da7db58567f..59e4ab12c81 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx @@ -1,5 +1,5 @@ import KustoQueryField from './KustoQueryField'; -import Kusto from './kusto'; +import Kusto from './kusto/kusto'; import React, { Component } from 'react'; import coreModule from 'app/core/core_module'; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts deleted file mode 100644 index 647ebb8024a..00000000000 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto.ts +++ /dev/null @@ -1,114 +0,0 @@ -export const FUNCTIONS = [ - { text: 'countof', display: 'countof()', hint: '' }, - { text: 'bin', display: 'bin()', hint: '' }, - { text: 'extentid', display: 'extentid()', hint: '' }, - { text: 'extract', display: 'extract()', hint: '' }, - { text: 'extractjson', display: 'extractjson()', hint: '' }, - { text: 'floor', display: 'floor()', hint: '' }, - { text: 'iif', display: 'iif()', hint: '' }, - { text: 'isnull', display: 'isnull()', hint: '' }, - { text: 'isnotnull', display: 'isnotnull()', hint: '' }, - { text: 'notnull', display: 'notnull()', hint: '' }, - { text: 'isempty', display: 'isempty()', hint: '' }, - { text: 'isnotempty', display: 'isnotempty()', hint: '' }, - { text: 'notempty', display: 'notempty()', hint: '' }, - { text: 'now', display: 'now()', hint: '' }, - { text: 're2', display: 're2()', hint: '' }, - { text: 'strcat', display: 'strcat()', hint: '' }, - { text: 'strlen', display: 'strlen()', hint: '' }, - { text: 'toupper', display: 'toupper()', hint: '' }, - { text: 'tostring', display: 'tostring()', hint: '' }, - { text: 'count', display: 'count()', hint: '' }, - { text: 'cnt', display: 'cnt()', hint: '' }, - { text: 'sum', display: 'sum()', hint: '' }, - { text: 'min', display: 'min()', hint: '' }, - { text: 'max', display: 'max()', hint: '' }, - { text: 'avg', display: 'avg()', hint: '' }, - { - text: '$__timeFilter', - display: '$__timeFilter()', - hint: 'Macro that uses the selected timerange in Grafana to filter the query.', - }, - { - text: '$__escapeMulti', - display: '$__escapeMulti()', - hint: 'Macro to escape multi-value template variables that contain illegal characters.', - }, - { text: '$__contains', display: '$__contains()', hint: 'Macro for multi-value template variables.' }, -]; - -export const KEYWORDS = [ - 'by', - 'on', - 'contains', - 'notcontains', - 'containscs', - 'notcontainscs', - 'startswith', - 'has', - 'matches', - 'regex', - 'true', - 'false', - 'and', - 'or', - 'typeof', - 'int', - 'string', - 'date', - 'datetime', - 'time', - 'long', - 'real', - '​boolean', - 'bool', - // add some more keywords - 'where', - 'order', -]; - -// Kusto operators -// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~']; - -export const DURATION = ['SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS']; - -const tokenizer = { - comment: { - pattern: /(^|[^\\:])\/\/.*/, - lookbehind: true, - greedy: true, - }, - 'function-context': { - pattern: /[a-z0-9_]+\([^)]*\)?/i, - inside: {}, - }, - duration: { - pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'), - alias: 'number', - }, - builtin: new RegExp(`\\b(?:${FUNCTIONS.map(f => f.text).join('|')})(?=\\s*\\()`, 'i'), - string: { - pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, - greedy: true, - }, - keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|\\*)\\b`, 'i'), - boolean: /\b(?:true|false)\b/, - number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i, - operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/, - punctuation: /[{};(),.:]/, - variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/, -}; - -tokenizer['function-context'].inside = { - argument: { - pattern: /[a-z0-9_]+(?=:)/i, - alias: 'symbol', - }, - duration: tokenizer.duration, - number: tokenizer.number, - builtin: tokenizer.builtin, - string: tokenizer.string, - variable: tokenizer.variable, -}; - -export default tokenizer; diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts new file mode 100644 index 00000000000..e2a1142597b --- /dev/null +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/kusto/kusto.ts @@ -0,0 +1,355 @@ +/* tslint:disable:max-line-length */ +export const operatorTokens = [ + { text: "!between", hint: "Matches the input that is outside the inclusive range." }, + { text: "as", hint: "Binds a name to the operator's input tabular expression." }, + { text: "between", hint: "Matches the input that is inside the inclusive range." }, + { text: "consume", hint: "The `consume` operator consumes the tabular data stream handed to it. It is\r\nmostly used for triggering the query side-effect without actually returning\r\nthe results back to the caller." }, + { text: "count", hint: "Returns the number of records in the input record set." }, + { text: "datatable", hint: "Returns a table whose schema and values are defined in the query itself." }, + { text: "distinct", hint: "Produces a table with the distinct combination of the provided columns of the input table." }, + { text: "evaluate", hint: "Invokes a service-side query extension (plugin)." }, + { text: "extend", hint: "Create calculated columns and append them to the result set." }, + { text: "externaldata", hint: "Returns a table whose schema is defined in the query itself, and whose data is read from an external raw file." }, + { text: "facet", hint: "Returns a set of tables, one for each specified column.\r\nEach table specifies the list of values taken by its column.\r\nAn additional table can be created by using the `with` clause." }, + { text: "find", hint: "Finds rows that match a predicate across a set of tables." }, + { text: "fork", hint: "Runs multiple consumer operators in parallel." }, + { text: "getschema", hint: "Produce a table that represents a tabular schema of the input." }, + { text: "in", hint: "Filters a recordset based on the provided set of values." }, + { text: "invoke", hint: "Invokes lambda that receives the source of `invoke` as tabular parameter argument." }, + { text: "join", hint: "Merge the rows of two tables to form a new table by matching values of the specified column(s) from each table." }, + { text: "limit", hint: "Return up to the specified number of rows." }, + { text: "make-series", hint: "Create series of specified aggregated values along specified axis." }, + { text: "mvexpand", hint: "Expands multi-value array or property bag." }, + { text: "order", hint: "Sort the rows of the input table into order by one or more columns." }, + { text: "parse", hint: "Evaluates a string expression and parses its value into one or more calculated columns." }, + { text: "print", hint: "Evaluates one or more scalar expressions and inserts the results (as a single-row table with as many columns as there are expressions) into the output." }, + { text: "project", hint: "Select the columns to include, rename or drop, and insert new computed columns." }, + { text: "project-away", hint: "Select what columns to exclude from the input." }, + { text: "project-rename", hint: "Renames columns in the result output." }, + { text: "range", hint: "Generates a single-column table of values." }, + { text: "reduce", hint: "Groups a set of strings together based on values similarity." }, + { text: "render", hint: "Instructs the user agent to render the results of the query in a particular way." }, + { text: "sample", hint: "Returns up to the specified number of random rows from the input table." }, + { text: "sample-distinct", hint: "Returns a single column that contains up to the specified number of distinct values of the requested column." }, + { text: "search", hint: "The search operator provides a multi-table/multi-column search experience." }, + { text: "serialize", hint: "Marks that order of the input row set is safe for window functions usage." }, + { text: "sort", hint: "Sort the rows of the input table into order by one or more columns." }, + { text: "summarize", hint: "Produces a table that aggregates the content of the input table." }, + { text: "take", hint: "Return up to the specified number of rows." }, + { text: "top", hint: "Returns the first *N* records sorted by the specified columns." }, + { text: "top-hitters", hint: "Returns an approximation of the first *N* results (assuming skewed distribution of the input)." }, + { text: "top-nested", hint: "Produces hierarchical top results, where each level is a drill-down based on previous level values." }, + { text: "union", hint: "Takes two or more tables and returns the rows of all of them." }, + { text: "where", hint: "Filters a table to the subset of rows that satisfy a predicate." }, +]; + +export const functionTokens = [ + { text: "abs", hint: "Calculates the absolute value of the input." }, + { text: "acos", hint: "Returns the angle whose cosine is the specified number (the inverse operation of [`cos()`](cosfunction.md)) ." }, + { text: "ago", hint: "Subtracts the given timespan from the current UTC clock time." }, + { text: "any", hint: "Returns random non-empty value from the specified expression values." }, + { text: "arg_max", hint: "Finds a row in the group that maximizes *ExprToMaximize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "arg_min", hint: "Finds a row in the group that minimizes *ExprToMinimize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "argmax", hint: "Finds a row in the group that maximizes *ExprToMaximize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "argmin", hint: "Finds a row in the group that minimizes *ExprToMinimize*, and returns the value of *ExprToReturn* (or `*` to return the entire row)." }, + { text: "array_concat", hint: "Concatenates a number of dynamic arrays to a single array." }, + { text: "array_length", hint: "Calculates the number of elements in a dynamic array." }, + { text: "array_slice", hint: "Extracts a slice of a dynamic array." }, + { text: "array_split", hint: "Splits an array to multiple arrays according to the split indices and packs the generated array in a dynamic array." }, + { text: "asin", hint: "Returns the angle whose sine is the specified number (the inverse operation of [`sin()`](sinfunction.md)) ." }, + { text: "assert", hint: "Checks for a condition; if the condition is false, outputs error messages and fails the query." }, + { text: "atan", hint: "Returns the angle whose tangent is the specified number (the inverse operation of [`tan()`](tanfunction.md)) ." }, + { text: "atan2", hint: "Calculates the angle, in radians, between the positive x-axis and the ray from the origin to the point (y, x)." }, + { text: "avg", hint: "Calculates the average of *Expr* across the group." }, + { text: "avgif", hint: "Calculates the [average](avg-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`." }, + { text: "bag_keys", hint: "Enumerates all the root keys in a dynamic property-bag object." }, + { text: "base64_decodestring", hint: "Decodes a base64 string to a UTF-8 string" }, + { text: "base64_encodestring", hint: "Encodes a string as base64 string" }, + { text: "beta_cdf", hint: "Returns the standard cumulative beta distribution function." }, + { text: "beta_inv", hint: "Returns the inverse of the beta cumulative probability beta density function." }, + { text: "beta_pdf", hint: "Returns the probability density beta function." }, + { text: "bin", hint: "Rounds values down to an integer multiple of a given bin size." }, + { text: "bin_at", hint: "Rounds values down to a fixed-size \'bin\', with control over the bin's starting point.\r\n(See also [`bin function`](./binfunction.md).)" }, + { text: "bin_auto", hint: "Rounds values down to a fixed-size \'bin\', with control over the bin size and starting point provided by a query property." }, + { text: "binary_and", hint: "Returns a result of the bitwise `and` operation between two values." }, + { text: "binary_not", hint: "Returns a bitwise negation of the input value." }, + { text: "binary_or", hint: "Returns a result of the bitwise `or` operation of the two values." }, + { text: "binary_shift_left", hint: "Returns binary shift left operation on a pair of numbers." }, + { text: "binary_shift_right", hint: "Returns binary shift right operation on a pair of numbers." }, + { text: "binary_xor", hint: "Returns a result of the bitwise `xor` operation of the two values." }, + { text: "buildschema", hint: "Returns the minimal schema that admits all values of *DynamicExpr*." }, + { text: "case", hint: "Evaluates a list of predicates and returns the first result expression whose predicate is satisfied." }, + { text: "ceiling", hint: "Calculates the smallest integer greater than, or equal to, the specified numeric expression." }, + { text: "cluster", hint: "Changes the reference of the query to a remote cluster." }, + { text: "coalesce", hint: "Evaluates a list of expressions and returns the first non-null (or non-empty for string) expression." }, + { text: "cos", hint: "Returns the cosine function." }, + { text: "cot", hint: "Calculates the trigonometric cotangent of the specified angle, in radians." }, + { text: "count", hint: "Returns a count of the records per summarization group (or in total if summarization is done without grouping)." }, + { text: "countif", hint: "Returns a count of rows for which *Predicate* evaluates to `true`." }, + { text: "countof", hint: "Counts occurrences of a substring in a string. Plain string matches may overlap; regex matches do not." }, + { text: "current_principal", hint: "Returns the current principal running this query." }, + { text: "cursor_after", hint: "A predicate over the records of a table to compare their ingestion time\r\nagainst a database cursor." }, + { text: "cursor_before_or_at", hint: "A predicate over the records of a table to compare their ingestion time\r\nagainst a database cursor." }, + { text: "database", hint: "Changes the reference of the query to a specific database within the cluster scope." }, + { text: "datetime_add", hint: "Calculates a new [datetime](./scalar-data-types/datetime.md) from a specified datepart multiplied by a specified amount, added to a specified [datetime](./scalar-data-types/datetime.md)." }, + { text: "datetime_diff", hint: "Calculates calendarian difference between two [datetime](./scalar-data-types/datetime.md) values." }, + { text: "datetime_part", hint: "Extracts the requested date part as an integer value." }, + { text: "dayofmonth", hint: "Returns the integer number representing the day number of the given month" }, + { text: "dayofweek", hint: "Returns the integer number of days since the preceding Sunday, as a `timespan`." }, + { text: "dayofyear", hint: "Returns the integer number represents the day number of the given year." }, + { text: "dcount", hint: "Returns an estimate of the number of distinct values of *Expr* in the group." }, + { text: "dcount_hll", hint: "Calculates the dcount from hll results (which was generated by [hll](hll-aggfunction.md) or [hll_merge](hll-merge-aggfunction.md))." }, + { text: "dcountif", hint: "Returns an estimate of the number of distinct values of *Expr* of rows for which *Predicate* evaluates to `true`." }, + { text: "degrees", hint: "Converts angle value in radians into value in degrees, using formula `degrees = (180 / PI ) * angle_in_radians`" }, + { text: "distance", hint: "Returns the distance between two points in meters." }, + { text: "endofday", hint: "Returns the end of the day containing the date, shifted by an offset, if provided." }, + { text: "endofmonth", hint: "Returns the end of the month containing the date, shifted by an offset, if provided." }, + { text: "endofweek", hint: "Returns the end of the week containing the date, shifted by an offset, if provided." }, + { text: "endofyear", hint: "Returns the end of the year containing the date, shifted by an offset, if provided." }, + { text: "estimate_data_size", hint: "Returns an estimated data size of the selected columns of the tabular expression." }, + { text: "exp", hint: "The base-e exponential function of x, which is e raised to the power x: e^x." }, + { text: "exp10", hint: "The base-10 exponential function of x, which is 10 raised to the power x: 10^x. \r\n**Syntax**" }, + { text: "exp2", hint: "The base-2 exponential function of x, which is 2 raised to the power x: 2^x." }, + { text: "extent_id", hint: "Returns a unique identifier that identifies the data shard (\"extent\") that the current record resides in." }, + { text: "extent_tags", hint: "Returns a dynamic array with the [tags](../management/extents-overview.md#extent-tagging) of the data shard (\"extent\") that the current record resides in." }, + { text: "extract", hint: "Get a match for a [regular expression](./re2.md) from a text string." }, + { text: "extract_all", hint: "Get all matches for a [regular expression](./re2.md) from a text string." }, + { text: "extractjson", hint: "Get a specified element out of a JSON text using a path expression." }, + { text: "floor", hint: "An alias for [`bin()`](binfunction.md)." }, + { text: "format_datetime", hint: "Formats a datetime parameter based on the format pattern parameter." }, + { text: "format_timespan", hint: "Formats a timespan parameter based on the format pattern parameter." }, + { text: "gamma", hint: "Computes [gamma function](https://en.wikipedia.org/wiki/Gamma_function)" }, + { text: "getmonth", hint: "Get the month number (1-12) from a datetime." }, + { text: "gettype", hint: "Returns the runtime type of its single argument." }, + { text: "getyear", hint: "Returns the year part of the `datetime` argument." }, + { text: "hash", hint: "Returns a hash value for the input value." }, + { text: "hash_sha256", hint: "Returns a sha256 hash value for the input value." }, + { text: "hll", hint: "Calculates the Intermediate results of [dcount](dcount-aggfunction.md) across the group." }, + { text: "hll_merge", hint: "Merges hll results (scalar version of the aggregate version [`hll_merge()`](hll-merge-aggfunction.md))." }, + { text: "hourofday", hint: "Returns the integer number representing the hour number of the given date" }, + { text: "iff", hint: "Evaluates the first argument (the predicate), and returns the value of either the second or third arguments, depending on whether the predicate evaluated to `true` (second) or `false` (third)." }, + { text: "iif", hint: "Evaluates the first argument (the predicate), and returns the value of either the second or third arguments, depending on whether the predicate evaluated to `true` (second) or `false` (third)." }, + { text: "indexof", hint: "Function reports the zero-based index of the first occurrence of a specified string within input string." }, + { text: "ingestion_time", hint: "Retrieves the record's `$IngestionTime` hidden `datetime` column, or null." }, + { text: "iscolumnexists", hint: "Returns a boolean value indicating if the given string argument exists in the schema produced by the preceding tabular operator." }, + { text: "isempty", hint: "Returns `true` if the argument is an empty string or is null." }, + { text: "isfinite", hint: "Returns whether input is a finite value (is neither infinite nor NaN)." }, + { text: "isinf", hint: "Returns whether input is an infinite (positive or negative) value." }, + { text: "isnan", hint: "Returns whether input is Not-a-Number (NaN) value." }, + { text: "isnotempty", hint: "Returns `true` if the argument is not an empty string nor it is a null." }, + { text: "isnotnull", hint: "Returns `true` if the argument is not null." }, + { text: "isnull", hint: "Evaluates its sole argument and returns a `bool` value indicating if the argument evaluates to a null value." }, + { text: "log", hint: "Returns the natural logarithm function." }, + { text: "log10", hint: "Returns the common (base-10) logarithm function." }, + { text: "log2", hint: "Returns the base-2 logarithm function." }, + { text: "loggamma", hint: "Computes log of absolute value of the [gamma function](https://en.wikipedia.org/wiki/Gamma_function)" }, + { text: "make_datetime", hint: "Creates a [datetime](./scalar-data-types/datetime.md) scalar value from the specified date and time." }, + { text: "make_dictionary", hint: "Returns a `dynamic` (JSON) property-bag (dictionary) of all the values of *Expr* in the group." }, + { text: "make_string", hint: "Returns the string generated by the Unicode characters." }, + { text: "make_timespan", hint: "Creates a [timespan](./scalar-data-types/timespan.md) scalar value from the specified time period." }, + { text: "makelist", hint: "Returns a `dynamic` (JSON) array of all the values of *Expr* in the group." }, + { text: "makeset", hint: "Returns a `dynamic` (JSON) array of the set of distinct values that *Expr* takes in the group." }, + { text: "materialize", hint: "Allows caching a sub-query result during the time of query execution in a way that other subqueries can reference the partial result." }, + { text: "max", hint: "Returns the maximum value across the group." }, + { text: "max_of", hint: "Returns the maximum value of several evaluated numeric expressions." }, + { text: "merge_tdigests", hint: "Merges tdigest results (scalar version of the aggregate version [`merge_tdigests()`](merge-tdigests-aggfunction.md))." }, + { text: "min", hint: "Returns the minimum value agross the group." }, + { text: "min_of", hint: "Returns the minimum value of several evaluated numeric expressions." }, + { text: "monthofyear", hint: "Returns the integer number represents the month number of the given year." }, + { text: "next", hint: "Returns the value of a column in a row that it at some offset following the\r\ncurrent row in a [serialized row set](./windowsfunctions.md#serialized-row-set)." }, + { text: "not", hint: "Reverses the value of its `bool` argument." }, + { text: "now", hint: "Returns the current UTC clock time, optionally offset by a given timespan.\r\nThis function can be used multiple times in a statement and the clock time being referenced will be the same for all instances." }, + { text: "pack", hint: "Creates a `dynamic` object (property bag) from a list of names and values." }, + { text: "pack_all", hint: "Creates a `dynamic` object (property bag) from all the columns of the tabular expression." }, + { text: "pack_array", hint: "Packs all input values into a dynamic array." }, + { text: "parse_ipv4", hint: "Converts input to integer (signed 64-bit) number representation." }, + { text: "parse_json", hint: "Interprets a `string` as a [JSON value](https://json.org/)) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md). \r\nIt is superior to using [extractjson() function](./extractjsonfunction.md)\r\nwhen you need to extract more than one element of a JSON compound object." }, + { text: "parse_path", hint: "Parses a file path `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object that contains the following parts of the path: \r\nScheme, RootPath, DirectoryPath, DirectoryName, FileName, Extension, AlternateDataStreamName.\r\nIn addition to the simple paths with both types of slashes, supports paths with schemas (e.g. \"file://...\"), shared paths (e.g. \"\\\\shareddrive\\users...\"), long paths (e.g \"\\\\?\\C:...\"\"), alternate data streams (e.g. \"file1.exe:file2.exe\")" }, + { text: "parse_url", hint: "Parses an absolute URL `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object contains all parts of the URL (Scheme, Host, Port, Path, Username, Password, Query Parameters, Fragment)." }, + { text: "parse_urlquery", hint: "Parses a url query `string` and returns a [`dynamic`](./scalar-data-types/dynamic.md) object contains the Query parameters." }, + { text: "parse_user_agent", hint: "Interprets a user-agent string, which identifies the user's browser and provides certain system details to servers hosting the websites the user visits. The result is returned as [`dynamic`](./scalar-data-types/dynamic.md)." }, + { text: "parse_version", hint: "Converts input string representation of version to a comparable decimal number." }, + { text: "parse_xml", hint: "Interprets a `string` as a XML value, converts the value to a [JSON value](https://json.org/) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md)." }, + { text: "percentile", hint: "Returns an estimate for the specified [nearest-rank percentile](#nearest-rank-percentile) of the population defined by *Expr*. \r\nThe accuracy depends on the density of population in the region of the percentile." }, + { text: "percentile_tdigest", hint: "Calculates the percentile result from tdigest results (which was generated by [tdigest](tdigest-aggfunction.md) or [merge-tdigests](merge-tdigests-aggfunction.md))" }, + { text: "percentrank_tdigest", hint: "Calculates the approximate rank of the value in a set where rank is expressed as percentage of set's size. \r\nThis function can be viewed as the inverse of the percentile." }, + { text: "pi", hint: "Returns the constant value of Pi (π)." }, + { text: "point", hint: "Returns a dynamic array representation of a point." }, + { text: "pow", hint: "Returns a result of raising to power" }, + { text: "prev", hint: "Returns the value of a column in a row that it at some offset prior to the\r\ncurrent row in a [serialized row set](./windowsfunctions.md#serialized-row-set)." }, + { text: "radians", hint: "Converts angle value in degrees into value in radians, using formula `radians = (PI / 180 ) * angle_in_degrees`" }, + { text: "rand", hint: "Returns a random number." }, + { text: "range", hint: "Generates a dynamic array holding a series of equally-spaced values." }, + { text: "repeat", hint: "Generates a dynamic array holding a series of equal values." }, + { text: "replace", hint: "Replace all regex matches with another string." }, + { text: "reverse", hint: "Function makes reverse of input string." }, + { text: "round", hint: "Returns the rounded source to the specified precision." }, + { text: "row_cumsum", hint: "Calculates the cumulative sum of a column in a [serialized row set](./windowsfunctions.md#serialized-row-set)." }, + { text: "row_number", hint: "Returns the current row's index in a [serialized row set](./windowsfunctions.md#serialized-row-set).\r\nThe row index starts by default at `1` for the first row, and is incremented by `1` for each additional row.\r\nOptionally, the row index can start at a different value than `1`.\r\nAdditionally, the row index may be reset according to some provided predicate." }, + { text: "series_add", hint: "Calculates the element-wise addition of two numeric series inputs." }, + { text: "series_decompose", hint: "Applies a decomposition transformation on a series." }, + { text: "series_decompose_anomalies", hint: "Anomaly Detection based on series decomposition (refer to [series_decompose()](series-decomposefunction.md))" }, + { text: "series_decompose_forecast", hint: "Forecast based on series decomposition." }, + { text: "series_divide", hint: "Calculates the element-wise division of two numeric series inputs." }, + { text: "series_equals", hint: "Calculates the element-wise equals (`==`) logic operation of two numeric series inputs." }, + { text: "series_fill_backward", hint: "Performs backward fill interpolation of missing values in a series." }, + { text: "series_fill_const", hint: "Replaces missing values in a series with a specified constant value." }, + { text: "series_fill_forward", hint: "Performs forward fill interpolation of missing values in a series." }, + { text: "series_fill_linear", hint: "Performs linear interpolation of missing values in a series." }, + { text: "series_fir", hint: "Applies a Finite Impulse Response filter on a series." }, + { text: "series_fit_2lines", hint: "Applies two segments linear regression on a series, returning multiple columns." }, + { text: "series_fit_2lines_dynamic", hint: "Applies two segments linear regression on a series, returning dynamic object." }, + { text: "series_fit_line", hint: "Applies linear regression on a series, returning multiple columns." }, + { text: "series_fit_line_dynamic", hint: "Applies linear regression on a series, returning dynamic object." }, + { text: "series_greater", hint: "Calculates the element-wise greater (`>`) logic operation of two numeric series inputs." }, + { text: "series_greater_equals", hint: "Calculates the element-wise greater or equals (`>=`) logic operation of two numeric series inputs." }, + { text: "series_iir", hint: "Applies a Infinite Impulse Response filter on a series." }, + { text: "series_less", hint: "Calculates the element-wise less (`<`) logic operation of two numeric series inputs." }, + { text: "series_less_equals", hint: "Calculates the element-wise less or equal (`<=`) logic operation of two numeric series inputs." }, + { text: "series_multiply", hint: "Calculates the element-wise multiplication of two numeric series inputs." }, + { text: "series_not_equals", hint: "Calculates the element-wise not equals (`!=`) logic operation of two numeric series inputs." }, + { text: "series_outliers", hint: "Scores anomaly points in a series." }, + { text: "series_periods_detect", hint: "Finds the most significant periods that exist in a time series." }, + { text: "series_periods_validate", hint: "Checks whether a time series contains periodic patterns of given lengths." }, + { text: "series_seasonal", hint: "Calculates the seasonal component of a series according to the detected or given seasonal period." }, + { text: "series_stats", hint: "Returns statistics for a series in multiple columns." }, + { text: "series_stats_dynamic", hint: "Returns statistics for a series in dynamic object." }, + { text: "series_subtract", hint: "Calculates the element-wise subtraction of two numeric series inputs." }, + { text: "sign", hint: "Sign of a numeric expression" }, + { text: "sin", hint: "Returns the sine function." }, + { text: "split", hint: "Splits a given string according to a given delimiter and returns a string array with the contained substrings." }, + { text: "sqrt", hint: "Returns the square root function." }, + { text: "startofday", hint: "Returns the start of the day containing the date, shifted by an offset, if provided." }, + { text: "startofmonth", hint: "Returns the start of the month containing the date, shifted by an offset, if provided." }, + { text: "startofweek", hint: "Returns the start of the week containing the date, shifted by an offset, if provided." }, + { text: "startofyear", hint: "Returns the start of the year containing the date, shifted by an offset, if provided." }, + { text: "stdev", hint: "Calculates the standard deviation of *Expr* across the group, considering the group as a [sample](https://en.wikipedia.org/wiki/Sample_%28statistics%29)." }, + { text: "stdevif", hint: "Calculates the [stdev](stdev-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`." }, + { text: "stdevp", hint: "Calculates the standard deviation of *Expr* across the group, considering the group as a [population](https://en.wikipedia.org/wiki/Statistical_population)." }, + { text: "strcat", hint: "Concatenates between 1 and 64 arguments." }, + { text: "strcat_array", hint: "Creates a concatenated string of array values using specified delimiter." }, + { text: "strcat_delim", hint: "Concatenates between 2 and 64 arguments, with delimiter, provided as first argument." }, + { text: "strcmp", hint: "Compares two strings." }, + { text: "string_size", hint: "Returns the size, in bytes, of the input string." }, + { text: "strlen", hint: "Returns the length, in characters, of the input string." }, + { text: "strrep", hint: "Repeats given [string](./scalar-data-types/string.md) provided amount of times." }, + { text: "substring", hint: "Extracts a substring from a source string starting from some index to the end of the string." }, + { text: "sum", hint: "Calculates the sum of *Expr* across the group." }, + { text: "sumif", hint: "Returns a sum of *Expr* for which *Predicate* evaluates to `true`." }, + { text: "table", hint: "References specific table using an query-time evaluated string-expression." }, + { text: "tan", hint: "Returns the tangent function." }, + { text: "tdigest", hint: "Calculates the Intermediate results of [`percentiles()`](percentiles-aggfunction.md) across the group." }, + { text: "tdigest_merge", hint: "Merges tdigest results (scalar version of the aggregate version [`tdigest_merge()`](tdigest-merge-aggfunction.md))." }, + { text: "tobool", hint: "Converts input to boolean (signed 8-bit) representation." }, + { text: "todatetime", hint: "Converts input to [datetime](./scalar-data-types/datetime.md) scalar." }, + { text: "todecimal", hint: "Converts input to decimal number representation." }, + { text: "todouble", hint: "Converts the input to a value of type `real`. (`todouble()` and `toreal()` are synonyms.)" }, + { text: "todynamic", hint: "Interprets a `string` as a [JSON value](https://json.org/) and returns the value as [`dynamic`](./scalar-data-types/dynamic.md)." }, + { text: "toguid", hint: "Converts input to [`guid`](./scalar-data-types/guid.md) representation." }, + { text: "tohex", hint: "Converts input to a hexadecimal string." }, + { text: "toint", hint: "Converts input to integer (signed 32-bit) number representation." }, + { text: "tolong", hint: "Converts input to long (signed 64-bit) number representation." }, + { text: "tolower", hint: "Converts input string to lower case." }, + { text: "toscalar", hint: "Returns a scalar constant value of the evaluated expression." }, + { text: "tostring", hint: "Converts input to a string representation." }, + { text: "totimespan", hint: "Converts input to [timespan](./scalar-data-types/timespan.md) scalar." }, + { text: "toupper", hint: "Converts a string to upper case." }, + { text: "translate", hint: "Replaces a set of characters ('searchList') with another set of characters ('replacementList') in a given a string.\r\nThe function searches for characters in the 'searchList' and replaces them with the corresponding characters in 'replacementList'" }, + { text: "treepath", hint: "Enumerates all the path expressions that identify leaves in a dynamic object." }, + { text: "trim", hint: "Removes all leading and trailing matches of the specified regular expression." }, + { text: "trim_end", hint: "Removes trailing match of the specified regular expression." }, + { text: "trim_start", hint: "Removes leading match of the specified regular expression." }, + { text: "url_decode", hint: "The function converts encoded URL into a to regular URL representation." }, + { text: "url_encode", hint: "The function converts characters of the input URL into a format that can be transmitted over the Internet." }, + { text: "variance", hint: "Calculates the variance of *Expr* across the group, considering the group as a [sample](https://en.wikipedia.org/wiki/Sample_%28statistics%29)." }, + { text: "varianceif", hint: "Calculates the [variance](variance-aggfunction.md) of *Expr* across the group for which *Predicate* evaluates to `true`." }, + { text: "variancep", hint: "Calculates the variance of *Expr* across the group, considering the group as a [population](https://en.wikipedia.org/wiki/Statistical_population)." }, + { text: "weekofyear", hint: "Returns the integer number represents the week number." }, + { text: "welch_test", hint: "Computes the p_value of the [Welch-test function](https://en.wikipedia.org/wiki/Welch%27s_t-test)" }, + { text: "zip", hint: "The `zip` function accepts any number of `dynamic` arrays, and returns an\r\narray whose elements are each an array holding the elements of the input\r\narrays of the same index." }, +]; + +export const KEYWORDS = [ + 'by', + 'on', + 'contains', + 'notcontains', + 'containscs', + 'notcontainscs', + 'startswith', + 'has', + 'matches', + 'regex', + 'true', + 'false', + 'and', + 'or', + 'typeof', + 'int', + 'string', + 'date', + 'datetime', + 'time', + 'long', + 'real', + '​boolean', + 'bool', +]; + +export const grafanaMacros = [ + { text: '$__timeFilter', display: '$__timeFilter()', hint: 'Macro that uses the selected timerange in Grafana to filter the query.', }, + { text: '$__escapeMulti', display: '$__escapeMulti()', hint: 'Macro to escape multi-value template variables that contain illegal characters.', }, + { text: '$__contains', display: '$__contains()', hint: 'Macro for multi-value template variables.' }, +]; + +// Kusto operators +// export const OPERATORS = ['+', '-', '*', '/', '>', '<', '==', '<>', '<=', '>=', '~', '!~']; + +export const DURATION = ['SECONDS', 'MINUTES', 'HOURS', 'DAYS', 'WEEKS', 'MONTHS', 'YEARS']; + +const tokenizer = { + comment: { + pattern: /(^|[^\\:])\/\/.*/, + lookbehind: true, + greedy: true, + }, + 'function-context': { + pattern: /[a-z0-9_]+\([^)]*\)?/i, + inside: {}, + }, + duration: { + pattern: new RegExp(`${DURATION.join('?|')}?`, 'i'), + alias: 'number', + }, + builtin: new RegExp(`\\b(?:${functionTokens.map(f => f.text).join('|')})(?=\\s*\\()`, 'i'), + string: { + pattern: /(["'])(?:\\(?:\r\n|[\s\S])|(?!\1)[^\\\r\n])*\1/, + greedy: true, + }, + keyword: new RegExp(`\\b(?:${KEYWORDS.join('|')}|${operatorTokens.map(f => f.text).join('|')}|\\*)\\b`, 'i'), + boolean: /\b(?:true|false)\b/, + number: /\b0x[\da-f]+\b|(?:\b\d+\.?\d*|\B\.\d+)(?:e[+-]?\d+)?/i, + operator: /-|\+|\*|\/|>|<|==|<=?|>=?|<>|!~|~|=|\|/, + punctuation: /[{};(),.:]/, + variable: /(\[\[(.+?)\]\])|(\$(.+?))\b/, +}; + +tokenizer['function-context'].inside = { + argument: { + pattern: /[a-z0-9_]+(?=:)/i, + alias: 'symbol', + }, + duration: tokenizer.duration, + number: tokenizer.number, + builtin: tokenizer.builtin, + string: tokenizer.string, + variable: tokenizer.variable, +}; + +// console.log(tokenizer.builtin); + +export default tokenizer; + +// function escapeRegExp(str: string): string { +// return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'); // $& means the whole matched string +// } From 0c3657da7e41f4d895cbc7f32eda87695bbb25f9 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 30 Jan 2019 15:24:37 +0300 Subject: [PATCH 024/228] azuremonitor: suggest tables initially --- .../editor/KustoQueryField.tsx | 90 ++++++++++++++----- .../editor/editor_component.tsx | 8 +- .../partials/query.editor.html | 1 + .../query_ctrl.ts | 4 +- 4 files changed, 79 insertions(+), 24 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index fa79d4bdb99..c8f96fba211 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -1,3 +1,4 @@ +import _ from 'lodash'; import Plain from 'slate-plain-serializer'; import QueryField from './query_field'; @@ -25,21 +26,43 @@ interface SuggestionGroup { skipFilter?: boolean; } +interface KustoSchema { + Databases: { + Default?: KustoDBSchema; + }; + Plugins?: any[]; +} + +interface KustoDBSchema { + Name?: string; + Functions?: any; + Tables?: any; +} + +const defaultSchema = () => ({ + Databases: { + Default: {} + } +}); + const cleanText = s => s.replace(/[{}[\]="(),!~+\-*/^%]/g, '').trim(); const wrapText = text => ({ text }); export default class KustoQueryField extends QueryField { fields: any; events: any; + schema: KustoSchema; constructor(props, context) { super(props, context); + this.schema = defaultSchema(); this.onTypeahead = debounce(this.onTypeahead, TYPEAHEAD_DELAY); } componentDidMount() { this.updateMenu(); + this.fetchSchema(); } onTypeahead = () => { @@ -128,7 +151,13 @@ export default class KustoQueryField extends QueryField { suggestionGroups = this._getKeywordSuggestions(); } else if (Plain.serialize(this.state.value) === '') { typeaheadContext = 'context-new'; - suggestionGroups = this._getInitialSuggestions(); + if (this.schema) { + suggestionGroups = this._getInitialSuggestions(); + } else { + this.fetchSchema(); + setTimeout(this.onTypeahead, 0); + return; + } } let results = 0; @@ -263,7 +292,7 @@ export default class KustoQueryField extends QueryField { { prefixMatch: true, label: 'Operators', - items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + items: operatorTokens }, { prefixMatch: true, @@ -274,34 +303,46 @@ export default class KustoQueryField extends QueryField { prefixMatch: true, label: 'Macros', items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Tables', + items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })) } ]; } private _getInitialSuggestions(): SuggestionGroup[] { - // TODO: return datbase tables as an initial suggestion return [ { prefixMatch: true, - label: 'Keywords', - items: KEYWORDS.map(wrapText) - }, - { - prefixMatch: true, - label: 'Operators', - items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) - }, - { - prefixMatch: true, - label: 'Functions', - items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) - }, - { - prefixMatch: true, - label: 'Macros', - items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) + label: 'Tables', + items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })) } ]; + + // return [ + // { + // prefixMatch: true, + // label: 'Keywords', + // items: KEYWORDS.map(wrapText) + // }, + // { + // prefixMatch: true, + // label: 'Operators', + // items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) + // }, + // { + // prefixMatch: true, + // label: 'Functions', + // items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + // }, + // { + // prefixMatch: true, + // label: 'Macros', + // items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) + // } + // ]; } private async _fetchEvents() { @@ -329,4 +370,13 @@ export default class KustoQueryField extends QueryField { // Stub this.fields = []; } + + private async fetchSchema() { + const schema = await this.props.getSchema(); + if (schema) { + this.schema = schema; + } else { + this.schema = defaultSchema(); + } + } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx index 59e4ab12c81..7787f029ee7 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx @@ -31,7 +31,7 @@ class Editor extends Component { }; render() { - const { request, variables } = this.props; + const { request, variables, getSchema } = this.props; const { edited, query } = this.state; return ( @@ -45,6 +45,7 @@ class Editor extends Component { placeholder="Enter a query" request={request} templateVariables={variables} + getSchema={getSchema} />
); @@ -54,6 +55,9 @@ class Editor extends Component { coreModule.directive('kustoEditor', [ 'reactDirective', reactDirective => { - return reactDirective(Editor, ['change', 'database', 'execute', 'query', 'request', 'variables']); + return reactDirective(Editor, [ + 'change', 'database', 'execute', 'query', 'request', 'variables', + ['getSchema', { watchDepth: 'reference' }] + ]); }, ]); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html index 49f02ec8355..592fccdcda9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html @@ -130,6 +130,7 @@ change="ctrl.onLogAnalyticsQueryChange" execute="ctrl.onLogAnalyticsQueryExecute" variables="ctrl.templateVariables" + getSchema="ctrl.getAzureLogAnalyticsSchema" />
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index fd42c172f11..b3aa5f9f6e9 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -304,7 +304,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { /* Azure Log Analytics */ - getWorkspaces() { + getWorkspaces = () => { return this.datasource.azureLogAnalyticsDatasource .getWorkspaces() .then(list => { @@ -316,7 +316,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .catch(this.handleQueryCtrlError.bind(this)); } - getAzureLogAnalyticsSchema() { + getAzureLogAnalyticsSchema = () => { return this.getWorkspaces() .then(() => { return this.datasource.azureLogAnalyticsDatasource.getSchema(this.target.azureLogAnalytics.workspace); From df9ecc68162ae678a8f22acda868b06fb0433f0d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 31 Jan 2019 17:44:00 +0300 Subject: [PATCH 025/228] azuremonitor: don't go back to dashboard if escape pressed in the editor --- public/app/core/services/keybindingSrv.ts | 18 ++++++++++++++++++ .../editor/KustoQueryField.tsx | 2 +- .../editor/query_field.tsx | 14 ++++++++++++++ public/app/routes/GrafanaCtrl.ts | 3 +++ 4 files changed, 36 insertions(+), 1 deletion(-) diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index 989746fd067..6d790baa336 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -139,6 +139,10 @@ export class KeybindingSrv { ); } + unbind(keyArg: string, keyType?: string) { + Mousetrap.unbind(keyArg, keyType); + } + showDashEditView() { const search = _.extend(this.$location.search(), { editview: 'settings' }); this.$location.search(search); @@ -293,3 +297,17 @@ export class KeybindingSrv { } coreModule.service('keybindingSrv', KeybindingSrv); + +/** + * Code below exports the service to react components + */ + +let singletonInstance: KeybindingSrv; + +export function setKeybindingSrv(instance: KeybindingSrv) { + singletonInstance = instance; +} + +export function getKeybindingSrv(): KeybindingSrv { + return singletonInstance; +} diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index c8f96fba211..719d57b9b6a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -61,7 +61,7 @@ export default class KustoQueryField extends QueryField { } componentDidMount() { - this.updateMenu(); + super.componentDidMount(); this.fetchSchema(); } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 1c883a40c31..0acd53cabff 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -9,6 +9,7 @@ import NewlinePlugin from './slate-plugins/newline'; import RunnerPlugin from './slate-plugins/runner'; import Typeahead from './typeahead'; +import { getKeybindingSrv, KeybindingSrv } from 'app/core/services/keybindingSrv'; import { Block, Document, Text, Value } from 'slate'; import { Editor } from 'slate-react'; @@ -61,6 +62,7 @@ class QueryField extends React.Component { menuEl: any; plugins: any; resetTimer: any; + keybindingSrv: KeybindingSrv = getKeybindingSrv(); constructor(props, context) { super(props, context); @@ -90,6 +92,7 @@ class QueryField extends React.Component { } componentWillUnmount() { + this.restoreEscapeKeyBinding(); clearTimeout(this.resetTimer); } @@ -218,6 +221,7 @@ class QueryField extends React.Component { if (onBlur) { onBlur(); } + this.restoreEscapeKeyBinding(); }; handleFocus = () => { @@ -225,8 +229,18 @@ class QueryField extends React.Component { if (onFocus) { onFocus(); } + // Don't go back to dashboard if Escape pressed inside the editor. + this.removeEscapeKeyBinding(); }; + removeEscapeKeyBinding() { + this.keybindingSrv.unbind('esc', 'keydown'); + } + + restoreEscapeKeyBinding() { + this.keybindingSrv.setupGlobal(); + } + onClickItem = item => { const { suggestions } = this.state; if (!suggestions || suggestions.length === 0) { diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 70bdf49e5e4..e50abdc0710 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -10,6 +10,7 @@ import appEvents from 'app/core/app_events'; import { BackendSrv, setBackendSrv } from 'app/core/services/backend_srv'; import { TimeSrv, setTimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { DatasourceSrv, setDatasourceSrv } from 'app/features/plugins/datasource_srv'; +import { KeybindingSrv, setKeybindingSrv } from 'app/core/services/keybindingSrv'; import { AngularLoader, setAngularLoader } from 'app/core/services/AngularLoader'; import { configureStore } from 'app/store/configureStore'; @@ -25,6 +26,7 @@ export class GrafanaCtrl { backendSrv: BackendSrv, timeSrv: TimeSrv, datasourceSrv: DatasourceSrv, + keybindingSrv: KeybindingSrv, angularLoader: AngularLoader ) { // make angular loader service available to react components @@ -32,6 +34,7 @@ export class GrafanaCtrl { setBackendSrv(backendSrv); setDatasourceSrv(datasourceSrv); setTimeSrv(timeSrv); + setKeybindingSrv(keybindingSrv); configureStore(); $scope.init = () => { From ad821cf6296b224d3995e1473e0db6533e1fed0c Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 31 Jan 2019 20:23:40 +0300 Subject: [PATCH 026/228] azuremonitor: where clause autocomplete --- .../editor/KustoQueryField.tsx | 49 ++++++++++++++++--- 1 file changed, 43 insertions(+), 6 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 719d57b9b6a..33be370ada3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -11,7 +11,7 @@ import { KEYWORDS, functionTokens, operatorTokens, grafanaMacros } from './kusto // import '../sass/editor.base.scss'; -const TYPEAHEAD_DELAY = 500; +const TYPEAHEAD_DELAY = 100; interface Suggestion { text: string; @@ -104,12 +104,13 @@ export default class KustoQueryField extends QueryField { this._fetchFields(); return; } - } else if (modelPrefix.match(/(facet\s$)/i)) { - typeaheadContext = 'context-facet'; - if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + } else if (modelPrefix.match(/(where\s$)/i)) { + typeaheadContext = 'context-where'; + const fullQuery = Plain.serialize(this.state.value); + const table = this.getTableFromContext(fullQuery); + if (table) { + suggestionGroups = this.getWhereSuggestions(table); } else { - this._fetchFields(); return; } } else if (modelPrefix.match(/(,\s*$)/)) { @@ -345,6 +346,35 @@ export default class KustoQueryField extends QueryField { // ]; } + private getWhereSuggestions(table: string): SuggestionGroup[] { + const tableSchema = this.schema.Databases.Default.Tables[table]; + if (tableSchema) { + return [ + { + prefixMatch: true, + label: 'Fields', + items: _.map(tableSchema.OrderedColumns, (f: any) => ({ + text: f.Name, + hint: f.Type + })) + } + ]; + } else { + return []; + } + } + + private getTableFromContext(query: string) { + const tablePattern = /^\s*(\w+)\s*|/g; + const normalizedQuery = normalizeQuery(query); + const match = tablePattern.exec(normalizedQuery); + if (match && match.length > 1 && match[0] && match[1]) { + return match[1]; + } else { + return null; + } + } + private async _fetchEvents() { // const query = 'events'; // const result = await this.request(query); @@ -380,3 +410,10 @@ export default class KustoQueryField extends QueryField { } } } + +function normalizeQuery(query: string): string { + const commentPattern = /\/\/.*$/gm; + let normalizedQuery = query.replace(commentPattern, ''); + normalizedQuery = normalizedQuery.replace('\n', ' '); + return normalizedQuery; +} From ae768193e3b78532ff7b2c2dbdbac2e7ff1fff05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 13:49:14 +0100 Subject: [PATCH 027/228] Now handles all dashbord routes --- public/app/core/reducers/location.ts | 4 +- public/app/core/services/bridge_srv.ts | 4 + .../dashboard/containers/DashboardPage.tsx | 8 +- .../services/DashboardViewStateSrv.ts | 12 +-- .../features/dashboard/state/initDashboard.ts | 100 ++++++++++++++---- public/app/routes/ReactContainer.tsx | 1 + public/app/routes/routes.ts | 25 +++-- public/app/types/dashboard.ts | 9 +- public/app/types/location.ts | 5 + 9 files changed, 132 insertions(+), 36 deletions(-) diff --git a/public/app/core/reducers/location.ts b/public/app/core/reducers/location.ts index 6b39710dcca..c038ab53c9f 100644 --- a/public/app/core/reducers/location.ts +++ b/public/app/core/reducers/location.ts @@ -8,12 +8,13 @@ export const initialState: LocationState = { path: '', query: {}, routeParams: {}, + replace: false, }; export const locationReducer = (state = initialState, action: Action): LocationState => { switch (action.type) { case CoreActionTypes.UpdateLocation: { - const { path, routeParams } = action.payload; + const { path, routeParams, replace } = action.payload; let query = action.payload.query || state.query; if (action.payload.partial) { @@ -26,6 +27,7 @@ export const locationReducer = (state = initialState, action: Action): LocationS path: path || state.path, query: { ...query }, routeParams: routeParams || state.routeParams, + replace: replace === true, }; } } diff --git a/public/app/core/services/bridge_srv.ts b/public/app/core/services/bridge_srv.ts index 37f71946364..8bb828310cf 100644 --- a/public/app/core/services/bridge_srv.ts +++ b/public/app/core/services/bridge_srv.ts @@ -46,6 +46,10 @@ export class BridgeSrv { if (angularUrl !== url) { this.$timeout(() => { this.$location.url(url); + // some state changes should not trigger new browser history + if (state.location.replace) { + this.$location.replace(); + } }); console.log('store updating angular $location.url', url); } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index e01998f65a9..3fbac681498 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -22,9 +22,8 @@ import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; // Types -import { StoreState } from 'app/types'; +import { StoreState, DashboardLoadingState, DashboardRouteInfo } from 'app/types'; import { DashboardModel, PanelModel } from 'app/features/dashboard/state'; -import { DashboardLoadingState } from 'app/types/dashboard'; interface Props { urlUid?: string; @@ -32,8 +31,10 @@ interface Props { urlType?: string; editview?: string; urlPanelId?: string; + urlFolderId?: string; $scope: any; $injector: any; + routeInfo: DashboardRouteInfo; urlEdit: boolean; urlFullscreen: boolean; loadingState: DashboardLoadingState; @@ -66,6 +67,8 @@ export class DashboardPage extends PureComponent { urlSlug: this.props.urlSlug, urlUid: this.props.urlUid, urlType: this.props.urlType, + urlFolderId: this.props.urlFolderId, + routeInfo: this.props.routeInfo, }); } @@ -208,6 +211,7 @@ const mapStateToProps = (state: StoreState) => ({ urlType: state.location.routeParams.type, editview: state.location.query.editview, urlPanelId: state.location.query.panelId, + urlFolderId: state.location.query.folderId, urlFullscreen: state.location.query.fullscreen === true, urlEdit: state.location.query.edit === true, loadingState: state.dashboard.loadingState, diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts index f5a68d6f647..aa64a2e93cf 100644 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ b/public/app/features/dashboard/services/DashboardViewStateSrv.ts @@ -23,10 +23,10 @@ export class DashboardViewStateSrv { self.dashboard = $scope.dashboard; $scope.onAppEvent('$routeUpdate', () => { - const urlState = self.getQueryStringState(); - if (self.needsSync(urlState)) { - self.update(urlState, true); - } + // const urlState = self.getQueryStringState(); + // if (self.needsSync(urlState)) { + // self.update(urlState, true); + // } }); $scope.onAppEvent('panel-change-view', (evt, payload) => { @@ -35,8 +35,8 @@ export class DashboardViewStateSrv { // this marks changes to location during this digest cycle as not to add history item // don't want url changes like adding orgId to add browser history - $location.replace(); - this.update(this.getQueryStringState()); + // $location.replace(); + // this.update(this.getQueryStringState()); } needsSync(urlState) { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index a39a7fce285..d497ef92d1f 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -15,7 +15,7 @@ import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './acti import { removePanel } from '../utils/panel'; // Types -import { DashboardLoadingState } from 'app/types/dashboard'; +import { DashboardLoadingState, DashboardRouteInfo } from 'app/types'; import { DashboardModel } from './DashboardModel'; export interface InitDashboardArgs { @@ -24,6 +24,8 @@ export interface InitDashboardArgs { urlUid?: string; urlSlug?: string; urlType?: string; + urlFolderId: string; + routeInfo: string; } async function redirectToNewUrl(slug: string, dispatch: any) { @@ -35,36 +37,67 @@ async function redirectToNewUrl(slug: string, dispatch: any) { } } -export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: InitDashboardArgs): ThunkResult { - return async dispatch => { - // handle old urls with no uid - if (!urlUid && urlSlug && !urlType) { - redirectToNewUrl(urlSlug, dispatch); - return; - } - +export function initDashboard({ + $injector, + $scope, + urlUid, + urlSlug, + urlType, + urlFolderId, + routeInfo, +}: InitDashboardArgs): ThunkResult { + return async (dispatch, getState) => { let dashDTO = null; // set fetching state dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); try { - // if no uid or slug, load home dashboard - if (!urlUid && !urlSlug) { - dashDTO = await getBackendSrv().get('/api/dashboards/home'); - - if (dashDTO.redirectUri) { - const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri); - dispatch(updateLocation({ path: newUrl })); + switch (routeInfo) { + // handle old urls with no uid + case DashboardRouteInfo.Old: { + redirectToNewUrl(urlSlug, dispatch); return; - } else { + } + case DashboardRouteInfo.Home: { + // load home dash + dashDTO = await getBackendSrv().get('/api/dashboards/home'); + + // if user specified a custom home dashboard redirect to that + if (dashDTO.redirectUri) { + const newUrl = locationUtil.stripBaseFromUrl(dashDTO.redirectUri); + dispatch(updateLocation({ path: newUrl, replace: true })); + return; + } + + // disable some actions on the default home dashboard dashDTO.meta.canSave = false; dashDTO.meta.canShare = false; dashDTO.meta.canStar = false; + break; + } + case DashboardRouteInfo.Normal: { + const loaderSrv = $injector.get('dashboardLoaderSrv'); + dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); + + // check if the current url is correct (might be old slug) + const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); + const currentPath = getState().location.path; + console.log('loading dashboard: currentPath', currentPath); + console.log('loading dashboard: dashboardUrl', dashboardUrl); + + if (dashboardUrl !== currentPath) { + // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. + dispatch(updateLocation({path: dashboardUrl, partial: true, replace: true})); + return; + } + + break; + } + case DashboardRouteInfo.New: { + dashDTO = getNewDashboardModelData(urlFolderId); + break; } - } else { - const loaderSrv = $injector.get('dashboardLoaderSrv'); - dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); } } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); @@ -136,3 +169,30 @@ export function initDashboard({ $injector, $scope, urlUid, urlSlug, urlType }: I dispatch(setDashboardModel(dashboard)); }; } + +function getNewDashboardModelData(urlFolderId?: string): any { + const data = { + meta: { + canStar: false, + canShare: false, + isNew: true, + folderId: 0, + }, + dashboard: { + title: 'New dashboard', + panels: [ + { + type: 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 9 }, + title: 'Panel Title', + }, + ], + }, + }; + + if (urlFolderId) { + data.meta.folderId = parseInt(urlFolderId, 10); + } + + return data; +} diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index 2cad3d828bf..a56c8878fb1 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -44,6 +44,7 @@ export function reactContainer( $injector: $injector, $rootScope: $rootScope, $scope: scope, + routeInfo: $route.current.$$route.routeInfo, }; ReactDOM.render(WrapInProvider(store, component, props), elem[0]); diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index abe347d689a..ecd934cdccf 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -2,6 +2,7 @@ import './dashboard_loaders'; import './ReactContainer'; import { applyRouteRegistrationHandlers } from './registry'; +// Pages import ServerStats from 'app/features/admin/ServerStats'; import AlertRuleList from 'app/features/alerting/AlertRuleList'; import TeamPages from 'app/features/teams/TeamPages'; @@ -23,6 +24,9 @@ import SoloPanelPage from '../features/dashboard/containers/SoloPanelPage'; import DashboardPage from '../features/dashboard/containers/DashboardPage'; import config from 'app/core/config'; +// Types +import { DashboardRouteInfo } from 'app/types'; + /** @ngInject */ export function setupAngularRoutes($routeProvider, $locationProvider) { $locationProvider.html5Mode(true); @@ -31,6 +35,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/', { template: '', pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.Home, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -39,6 +44,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/d/:uid/:slug', { template: '', pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.Normal, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -48,6 +54,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { template: '', pageClass: 'page-dashboard', reloadOnSearch: false, + routeInfo: DashboardRouteInfo.Normal, resolve: { component: () => DashboardPage, }, @@ -55,6 +62,16 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard/:type/:slug', { template: '', pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.Old, + reloadOnSearch: false, + resolve: { + component: () => DashboardPage, + }, + }) + .when('/dashboard/new', { + template: '', + pageClass: 'page-dashboard', + routeInfo: DashboardRouteInfo.New, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -63,6 +80,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/d-solo/:uid/:slug', { template: '', pageClass: 'dashboard-solo', + routeInfo: DashboardRouteInfo.Normal, resolve: { component: () => SoloPanelPage, }, @@ -70,16 +88,11 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard-solo/:type/:slug', { template: '', pageClass: 'dashboard-solo', + routeInfo: DashboardRouteInfo.Old, resolve: { component: () => SoloPanelPage, }, }) - .when('/dashboard/new', { - templateUrl: 'public/app/partials/dashboard.html', - controller: 'NewDashboardCtrl', - reloadOnSearch: false, - pageClass: 'page-dashboard', - }) .when('/dashboard/import', { templateUrl: 'public/app/features/manage-dashboards/partials/dashboard_import.html', controller: DashboardImportCtrl, diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 9b1e750e859..9b8f539aeb2 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -7,9 +7,16 @@ export interface MutableDashboard { }; } +export enum DashboardRouteInfo { + Old = 'old-dashboard', + Home = 'home-dashboard', + New = 'new-dashboard', + Normal = 'normal-dashboard', +} + export enum DashboardLoadingState { NotStarted = 'Not started', - Fetching = 'Fetching', + Fetching = 'Fetching', Initializing = 'Initializing', Error = 'Error', Done = 'Done', diff --git a/public/app/types/location.ts b/public/app/types/location.ts index 7dcf57f7e02..a47ef05d2be 100644 --- a/public/app/types/location.ts +++ b/public/app/types/location.ts @@ -3,6 +3,10 @@ export interface LocationUpdate { query?: UrlQueryMap; routeParams?: UrlQueryMap; partial?: boolean; + /* + * If true this will replace url state (ie cause no new browser history) + */ + replace?: boolean; } export interface LocationState { @@ -10,6 +14,7 @@ export interface LocationState { path: string; query: UrlQueryMap; routeParams: UrlQueryMap; + replace: boolean; } export type UrlQueryValue = string | number | boolean | string[] | number[] | boolean[]; From 7634e0423159485366afe89d3ff67445f3837759 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 14:45:13 +0100 Subject: [PATCH 028/228] Fixed template variable value changed handling --- .../components/AdHocFilters/AdHocFiltersCtrl.ts | 6 ++++-- .../dashboard/components/DashboardRow/DashboardRow.tsx | 4 ++-- .../dashboard/components/SubMenu/template.html | 2 +- public/app/features/dashboard/state/DashboardModel.ts | 5 +++++ public/app/features/dashboard/state/initDashboard.ts | 1 - .../app/features/templating/specs/variable_srv.test.ts | 1 - .../templating/specs/variable_srv_init.test.ts | 5 +---- public/app/features/templating/variable_srv.ts | 10 +++++----- 8 files changed, 18 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts b/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts index 0ceac9ddbba..a7616e0e513 100644 --- a/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts +++ b/public/app/features/dashboard/components/AdHocFilters/AdHocFiltersCtrl.ts @@ -1,10 +1,12 @@ import _ from 'lodash'; import angular from 'angular'; import coreModule from 'app/core/core_module'; +import { DashboardModel } from 'app/features/dashboard/state'; export class AdHocFiltersCtrl { segments: any; variable: any; + dashboard: DashboardModel; removeTagFilterSegment: any; /** @ngInject */ @@ -14,14 +16,13 @@ export class AdHocFiltersCtrl { private $q, private variableSrv, $scope, - private $rootScope ) { this.removeTagFilterSegment = uiSegmentSrv.newSegment({ fake: true, value: '-- remove filter --', }); this.buildSegmentModel(); - this.$rootScope.onAppEvent('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope); + this.dashboard.events.on('template-variable-value-updated', this.buildSegmentModel.bind(this), $scope); } buildSegmentModel() { @@ -171,6 +172,7 @@ export function adHocFiltersComponent() { controllerAs: 'ctrl', scope: { variable: '=', + dashboard: '=', }, }; } diff --git a/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx b/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx index e7778a31fdb..bb63cea90ea 100644 --- a/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx +++ b/public/app/features/dashboard/components/DashboardRow/DashboardRow.tsx @@ -18,11 +18,11 @@ export class DashboardRow extends React.Component { collapsed: this.props.panel.collapsed, }; - appEvents.on('template-variable-value-updated', this.onVariableUpdated); + this.props.dashboard.on('template-variable-value-updated', this.onVariableUpdated); } componentWillUnmount() { - appEvents.off('template-variable-value-updated', this.onVariableUpdated); + this.props.dashboard.off('template-variable-value-updated', this.onVariableUpdated); } onVariableUpdated = () => { diff --git a/public/app/features/dashboard/components/SubMenu/template.html b/public/app/features/dashboard/components/SubMenu/template.html index 5d0f200d862..1ccbfcc915c 100644 --- a/public/app/features/dashboard/components/SubMenu/template.html +++ b/public/app/features/dashboard/components/SubMenu/template.html @@ -7,7 +7,7 @@
- +
diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 8d96a2eec73..ab9d764358c 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -900,4 +900,9 @@ export class DashboardModel { panel.gridPos.h = Math.round(panel.gridPos.h / scaleFactor) || 1; }); } + + templateVariableValueUpdated() { + this.processRepeats(); + this.events.emit('template-variable-value-updated'); + } } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index d497ef92d1f..a697a8e77f9 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -91,7 +91,6 @@ export function initDashboard({ dispatch(updateLocation({path: dashboardUrl, partial: true, replace: true})); return; } - break; } case DashboardRouteInfo.New: { diff --git a/public/app/features/templating/specs/variable_srv.test.ts b/public/app/features/templating/specs/variable_srv.test.ts index db42df7f516..cf10235f6e8 100644 --- a/public/app/features/templating/specs/variable_srv.test.ts +++ b/public/app/features/templating/specs/variable_srv.test.ts @@ -48,7 +48,6 @@ describe('VariableSrv', function(this: any) { ds.metricFindQuery = () => Promise.resolve(scenario.queryResult); ctx.variableSrv = new VariableSrv( - ctx.$rootScope, $q, ctx.$location, ctx.$injector, 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 b8cabf711ac..d256ab28c2c 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -25,9 +25,6 @@ describe('VariableSrv init', function(this: any) { }; const $injector = {} as any; - const $rootscope = { - $on: () => {}, - }; let ctx = {} as any; @@ -54,7 +51,7 @@ describe('VariableSrv init', function(this: any) { }; // @ts-ignore - ctx.variableSrv = new VariableSrv($rootscope, $q, {}, $injector, templateSrv, timeSrv); + ctx.variableSrv = new VariableSrv($q, {}, $injector, templateSrv, timeSrv); $injector.instantiate = (variable, model) => { return getVarMockConstructor(variable, model, ctx); diff --git a/public/app/features/templating/variable_srv.ts b/public/app/features/templating/variable_srv.ts index b2f8b43fb08..81e4e0a4a0b 100644 --- a/public/app/features/templating/variable_srv.ts +++ b/public/app/features/templating/variable_srv.ts @@ -18,18 +18,18 @@ export class VariableSrv { variables: any[]; /** @ngInject */ - constructor(private $rootScope, - private $q, + constructor(private $q, private $location, private $injector, private templateSrv: TemplateSrv, private timeSrv: TimeSrv) { - $rootScope.$on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this), $rootScope); + } init(dashboard: DashboardModel) { this.dashboard = dashboard; this.dashboard.events.on('time-range-updated', this.onTimeRangeUpdated.bind(this)); + this.dashboard.events.on('template-variable-value-updated', this.updateUrlParamsWithCurrentVariables.bind(this)); // create working class models representing variables this.variables = dashboard.templating.list = dashboard.templating.list.map(this.createVariableFromModel.bind(this)); @@ -59,7 +59,7 @@ export class VariableSrv { return variable.updateOptions().then(() => { if (angular.toJson(previousOptions) !== angular.toJson(variable.options)) { - this.$rootScope.$emit('template-variable-value-updated'); + this.dashboard.templateVariableValueUpdated(); } }); }); @@ -144,7 +144,7 @@ export class VariableSrv { return this.$q.all(promises).then(() => { if (emitChangeEvents) { - this.$rootScope.appEvent('template-variable-value-updated'); + this.dashboard.templateVariableValueUpdated(); this.dashboard.startRefresh(); } }); From f695975f651457454bc5f6ec94455487ca12da0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 15:02:35 +0100 Subject: [PATCH 029/228] Fixed handling of orgId --- public/app/core/config.ts | 2 +- public/app/features/dashboard/services/DashboardSrv.ts | 1 + public/app/features/dashboard/state/initDashboard.ts | 8 +++++++- .../features/templating/specs/variable_srv_init.test.ts | 1 - public/app/types/user.ts | 2 +- 5 files changed, 10 insertions(+), 4 deletions(-) diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 395e40e914b..368b3798117 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -68,5 +68,5 @@ const bootData = (window as any).grafanaBootData || { const options = bootData.settings; options.bootData = bootData; -const config = new Settings(options); +export const config = new Settings(options); export default config; diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 7d3dfb68cd8..532e2e1c828 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -9,6 +9,7 @@ export class DashboardSrv { /** @ngInject */ constructor(private backendSrv, private $rootScope, private $location) { appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); + appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); } create(dashboard, meta) { diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index a697a8e77f9..14d6196d69c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -6,6 +6,7 @@ import { TimeSrv } from 'app/features/dashboard/services/TimeSrv'; import { AnnotationsSrv } from 'app/features/annotations/annotations_srv'; import { VariableSrv } from 'app/features/templating/variable_srv'; import { KeybindingSrv } from 'app/core/services/keybindingSrv'; +import { config } from 'app/core/config'; // Actions import { updateLocation } from 'app/core/actions'; @@ -88,7 +89,7 @@ export function initDashboard({ if (dashboardUrl !== currentPath) { // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. - dispatch(updateLocation({path: dashboardUrl, partial: true, replace: true})); + dispatch(updateLocation({ path: dashboardUrl, partial: true, replace: true })); return; } break; @@ -117,6 +118,11 @@ export function initDashboard({ return; } + // add missing orgId query param + if (!getState().location.query.orgId) { + dispatch(updateLocation({ query: { orgId: config.bootData.user.orgId }, partial: true, replace: true })); + } + // init services const timeSrv: TimeSrv = $injector.get('timeSrv'); const annotationsSrv: AnnotationsSrv = $injector.get('annotationsSrv'); 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 d256ab28c2c..480b5207c17 100644 --- a/public/app/features/templating/specs/variable_srv_init.test.ts +++ b/public/app/features/templating/specs/variable_srv_init.test.ts @@ -25,7 +25,6 @@ describe('VariableSrv init', function(this: any) { }; const $injector = {} as any; - let ctx = {} as any; function describeInitScenario(desc, fn) { diff --git a/public/app/types/user.ts b/public/app/types/user.ts index 37c80074dca..365411147bb 100644 --- a/public/app/types/user.ts +++ b/public/app/types/user.ts @@ -1,4 +1,4 @@ -import { DashboardSearchHit } from './search'; +import { DashboardSearchHit } from './search'; export interface OrgUser { avatarUrl: string; From 716258339625e8b5e36f2615537517a2b2e8f362 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 15:41:57 +0100 Subject: [PATCH 030/228] Made dashboard view state srv panel view state obsolete --- public/app/core/services/keybindingSrv.ts | 6 +- .../dashboard/containers/DashboardPage.tsx | 8 +-- .../dashboard/services/DashboardSrv.ts | 39 +++++++++++ .../services/DashboardViewStateSrv.test.ts | 64 ------------------- .../services/DashboardViewStateSrv.ts | 2 +- .../dashboard/state/DashboardModel.ts | 2 + 6 files changed, 49 insertions(+), 72 deletions(-) delete mode 100644 public/app/features/dashboard/services/DashboardViewStateSrv.test.ts diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index aa39763841e..dfacc483b8e 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -104,7 +104,7 @@ export class KeybindingSrv { } if (search.fullscreen) { - this.$rootScope.appEvent('panel-change-view', { fullscreen: false, edit: false }); + appEvents.emit('panel-change-view', { fullscreen: false, edit: false }); return; } @@ -174,7 +174,7 @@ export class KeybindingSrv { // edit panel this.bind('e', () => { if (dashboard.meta.focusPanelId && dashboard.meta.canEdit) { - this.$rootScope.appEvent('panel-change-view', { + appEvents.emit('panel-change-view', { fullscreen: true, edit: true, panelId: dashboard.meta.focusPanelId, @@ -186,7 +186,7 @@ export class KeybindingSrv { // view panel this.bind('v', () => { if (dashboard.meta.focusPanelId) { - this.$rootScope.appEvent('panel-change-view', { + appEvents.emit('panel-change-view', { fullscreen: true, edit: null, panelId: dashboard.meta.focusPanelId, diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3fbac681498..ec143d735ab 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -94,7 +94,7 @@ export class DashboardPage extends PureComponent { // } // Sync url state with model - if (urlFullscreen !== dashboard.meta.isFullscreen || urlEdit !== dashboard.meta.isEditing) { + if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) { // entering fullscreen/edit mode if (urlPanelId) { const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); @@ -102,6 +102,7 @@ export class DashboardPage extends PureComponent { if (panel) { dashboard.setViewMode(panel, urlFullscreen, urlEdit); this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel }); + this.setPanelFullscreenClass(urlFullscreen); } else { this.handleFullscreenPanelNotFound(urlPanelId); } @@ -110,10 +111,9 @@ export class DashboardPage extends PureComponent { if (this.state.fullscreenPanel) { dashboard.setViewMode(this.state.fullscreenPanel, urlFullscreen, urlEdit); } - this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: null }); + this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }); + this.setPanelFullscreenClass(false); } - - this.setPanelFullscreenClass(urlFullscreen); } } diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index 532e2e1c828..e2e524941f8 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -10,6 +10,7 @@ export class DashboardSrv { constructor(private backendSrv, private $rootScope, private $location) { appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); + appEvents.on('panel-change-view', this.onPanelChangeView); } create(dashboard, meta) { @@ -24,6 +25,44 @@ export class DashboardSrv { return this.dash; } + onPanelChangeView = (options) => { + const urlParams = this.$location.search(); + + // handle toggle logic + if (options.fullscreen === urlParams.fullscreen) { + // I hate using these truthy converters (!!) but in this case + // I think it's appropriate. edit can be null/false/undefined and + // here i want all of those to compare the same + if (!!options.edit === !!urlParams.edit) { + delete urlParams.fullscreen; + delete urlParams.edit; + delete urlParams.panelId; + this.$location.search(urlParams); + return; + } + } + + if (options.fullscreen) { + urlParams.fullscreen = true; + } else { + delete urlParams.fullscreen; + } + + if (options.edit) { + urlParams.edit = true; + } else { + delete urlParams.edit; + } + + if (options.panelId) { + urlParams.panelId = options.panelId; + } else { + delete urlParams.panelId; + } + + this.$location.search(urlParams); + }; + handleSaveDashboardError(clone, options, err) { options = options || {}; options.overwrite = true; diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts deleted file mode 100644 index 12bb11b7a08..00000000000 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.test.ts +++ /dev/null @@ -1,64 +0,0 @@ -import config from 'app/core/config'; -import { DashboardViewStateSrv } from './DashboardViewStateSrv'; -import { DashboardModel } from '../state/DashboardModel'; - -describe('when updating view state', () => { - const location = { - replace: jest.fn(), - search: jest.fn(), - }; - - const $scope = { - appEvent: jest.fn(), - onAppEvent: jest.fn(() => {}), - dashboard: new DashboardModel({ - panels: [{ id: 1 }], - }), - }; - - let viewState; - - beforeEach(() => { - config.bootData = { - user: { - orgId: 1, - }, - }; - }); - - describe('to fullscreen true and edit true', () => { - beforeEach(() => { - location.search = jest.fn(() => { - return { fullscreen: true, edit: true, panelId: 1 }; - }); - viewState = new DashboardViewStateSrv($scope, location, {}); - }); - - it('should update querystring and view state', () => { - const updateState = { fullscreen: true, edit: true, panelId: 1 }; - - viewState.update(updateState); - - expect(location.search).toHaveBeenCalledWith({ - edit: true, - editview: null, - fullscreen: true, - orgId: 1, - panelId: 1, - }); - expect(viewState.dashboard.meta.fullscreen).toBe(true); - expect(viewState.state.fullscreen).toBe(true); - }); - }); - - describe('to fullscreen false', () => { - beforeEach(() => { - viewState = new DashboardViewStateSrv($scope, location, {}); - }); - it('should remove params from query string', () => { - viewState.update({ fullscreen: true, panelId: 1, edit: true }); - viewState.update({ fullscreen: false }); - expect(viewState.state.fullscreen).toBe(null); - }); - }); -}); diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts index aa64a2e93cf..7cb4c1de7ab 100644 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ b/public/app/features/dashboard/services/DashboardViewStateSrv.ts @@ -30,7 +30,7 @@ export class DashboardViewStateSrv { }); $scope.onAppEvent('panel-change-view', (evt, payload) => { - self.update(payload); + // self.update(payload); }); // this marks changes to location during this digest cycle as not to add history item diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index ab9d764358c..a6d45bb3cc0 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -132,6 +132,8 @@ export class DashboardModel { meta.canEdit = meta.canEdit !== false; meta.showSettings = meta.canEdit; meta.canMakeEditable = meta.canSave && !this.editable; + meta.fullscreen = false; + meta.isEditing = false; if (!this.editable) { meta.canEdit = false; From fdeea9144ccc62c1be15663e215edb5dc7261ef2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 15:50:47 +0100 Subject: [PATCH 031/228] fixed unit test --- .../dashboard/components/DashboardRow/DashboardRow.test.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx b/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx index 9ac6a6b74e1..96b673242e4 100644 --- a/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx +++ b/public/app/features/dashboard/components/DashboardRow/DashboardRow.test.tsx @@ -9,6 +9,7 @@ describe('DashboardRow', () => { beforeEach(() => { dashboardMock = { toggleRow: jest.fn(), + on: jest.fn(), meta: { canEdit: true, }, From dd8ca70151672293b48267a800b2e3682e8a79c5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Feb 2019 18:51:56 +0300 Subject: [PATCH 032/228] azuremonitor: use kusto editor for App Insights --- .../app_insights/app_insights_datasource.ts | 9 +++++++ .../app_insights/response_parser.ts | 26 +++++++++++++++++++ .../editor/KustoQueryField.tsx | 14 +++++++++- .../editor/editor_component.tsx | 22 ++++++++++++---- .../partials/query.editor.html | 15 ++++++++--- .../query_ctrl.ts | 14 ++++++++++ 6 files changed, 91 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts index 950fa73a16b..97f76d229fb 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/app_insights_datasource.ts @@ -224,4 +224,13 @@ export default class AppInsightsDatasource { return new ResponseParser(result).parseGroupBys(); }); } + + getQuerySchema() { + const url = `${this.baseUrl}/query/schema`; + return this.doRequest(url).then(result => { + const schema = new ResponseParser(result).parseQuerySchema(); + // console.log(schema); + return schema; + }); + } } diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts index 848472cf101..fa96e4a2e3e 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/app_insights/response_parser.ts @@ -199,6 +199,32 @@ export default class ResponseParser { return ResponseParser.toTextValueList(this.results.supportedGroupBy); } + parseQuerySchema() { + const result = { + Type: 'AppInsights', + Tables: {} + }; + if (this.results && this.results.data && this.results.data.Tables) { + for (let i = 0; i < this.results.data.Tables[0].Rows.length; i++) { + const column = this.results.data.Tables[0].Rows[i]; + const columnTable = column[0]; + const columnName = column[1]; + const columnType = column[2]; + if (result.Tables[columnTable]) { + result.Tables[columnTable].OrderedColumns.push({ Name: columnName, Type: columnType }); + } else { + result.Tables[columnTable] = { + Name: columnTable, + OrderedColumns: [ + { Name: columnName, Type: columnType } + ] + }; + } + } + } + return result; + } + static toTextValueList(values) { const list: any[] = []; for (let i = 0; i < values.length; i++) { diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 33be370ada3..09573f29047 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -402,8 +402,11 @@ export default class KustoQueryField extends QueryField { } private async fetchSchema() { - const schema = await this.props.getSchema(); + let schema = await this.props.getSchema(); if (schema) { + if (schema.Type === 'AppInsights') { + schema = castSchema(schema); + } this.schema = schema; } else { this.schema = defaultSchema(); @@ -411,6 +414,15 @@ export default class KustoQueryField extends QueryField { } } +/** + * Cast schema from App Insights to default Kusto schema + */ +function castSchema(schema) { + const defaultSchemaTemplate = defaultSchema(); + defaultSchemaTemplate.Databases.Default = schema; + return defaultSchemaTemplate; +} + function normalizeQuery(query: string): string { const commentPattern = /\/\/.*$/gm; let normalizedQuery = query.replace(commentPattern, ''); diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx index 7787f029ee7..bdc85f1577d 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/editor_component.tsx @@ -4,7 +4,20 @@ import Kusto from './kusto/kusto'; import React, { Component } from 'react'; import coreModule from 'app/core/core_module'; -class Editor extends Component { +interface EditorProps { + index: number; + placeholder?: string; + change: (value: string, index: number) => void; + variables: () => string[] | string[]; + getSchema?: () => Promise; + execute?: () => void; +} + +class Editor extends Component { + static defaultProps = { + placeholder: 'Enter a query' + }; + constructor(props) { super(props); this.state = { @@ -31,7 +44,7 @@ class Editor extends Component { }; render() { - const { request, variables, getSchema } = this.props; + const { variables, getSchema, placeholder } = this.props; const { edited, query } = this.state; return ( @@ -42,8 +55,7 @@ class Editor extends Component { onQueryChange={this.onChangeQuery} prismLanguage="kusto" prismDefinition={Kusto} - placeholder="Enter a query" - request={request} + placeholder={placeholder} templateVariables={variables} getSchema={getSchema} /> @@ -56,7 +68,7 @@ coreModule.directive('kustoEditor', [ 'reactDirective', reactDirective => { return reactDirective(Editor, [ - 'change', 'database', 'execute', 'query', 'request', 'variables', + 'change', 'database', 'execute', 'query', 'variables', 'placeholder', ['getSchema', { watchDepth: 'reference' }] ]); }, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html index 592fccdcda9..6299947b30a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/partials/query.editor.html @@ -124,8 +124,6 @@
-
+ +
+
diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts index b3aa5f9f6e9..cee67d11ab3 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/query_ctrl.ts @@ -345,6 +345,7 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { } return interval; } + getAppInsightsMetricNames() { if (!this.datasource.appInsightsDatasource.isConfigured()) { return; @@ -377,6 +378,19 @@ export class AzureMonitorQueryCtrl extends QueryCtrl { .catch(this.handleQueryCtrlError.bind(this)); } + onAppInsightsQueryChange = (nextQuery: string) => { + this.target.appInsights.rawQueryString = nextQuery; + } + + onAppInsightsQueryExecute = () => { + return this.refresh(); + } + + getAppInsightsQuerySchema = () => { + return this.datasource.appInsightsDatasource.getQuerySchema() + .catch(this.handleQueryCtrlError.bind(this)); + } + getAppInsightsGroupBySegments(query) { return _.map(this.target.appInsights.groupByOptions, option => { return { text: option, value: option }; From 99ff8e68ffbd851163d539891b884fbf66da67ca Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Feb 2019 19:20:18 +0300 Subject: [PATCH 033/228] azuremonitor: fix where suggestions --- .../editor/KustoQueryField.tsx | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 09573f29047..9b2df96fcdd 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -99,12 +99,12 @@ export default class KustoQueryField extends QueryField { if (wrapperClasses.contains('function-context')) { typeaheadContext = 'context-function'; if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchFields(); return; } - } else if (modelPrefix.match(/(where\s$)/i)) { + } else if (modelPrefix.match(/(where\s(\w+\b)?$)/i)) { typeaheadContext = 'context-where'; const fullQuery = Plain.serialize(this.state.value); const table = this.getTableFromContext(fullQuery); @@ -116,7 +116,7 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/(,\s*$)/)) { typeaheadContext = 'context-multiple-fields'; if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchFields(); return; @@ -124,7 +124,7 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/(from\s$)/i)) { typeaheadContext = 'context-from'; if (this.events) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchEvents(); return; @@ -132,7 +132,7 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/(^select\s\w*$)/i)) { typeaheadContext = 'context-select'; if (this.fields) { - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else { this._fetchFields(); return; @@ -140,16 +140,19 @@ export default class KustoQueryField extends QueryField { } else if (modelPrefix.match(/from\s\S+\s\w*$/i)) { prefix = ''; typeaheadContext = 'context-since'; - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); // } else if (modelPrefix.match(/\d+\s\w*$/)) { // typeaheadContext = 'context-number'; // suggestionGroups = this._getAfterNumberSuggestions(); } else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) { typeaheadContext = 'context-timeseries'; - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else if (prefix && !wrapperClasses.contains('argument')) { + if (modelPrefix.match(/\s$/i)) { + prefix = ''; + } typeaheadContext = 'context-builtin'; - suggestionGroups = this._getKeywordSuggestions(); + suggestionGroups = this.getKeywordSuggestions(); } else if (Plain.serialize(this.state.value) === '') { typeaheadContext = 'context-new'; if (this.schema) { @@ -159,6 +162,12 @@ export default class KustoQueryField extends QueryField { setTimeout(this.onTypeahead, 0); return; } + } else { + typeaheadContext = 'context-builtin'; + if (modelPrefix.match(/\s$/i)) { + prefix = ''; + } + suggestionGroups = this.getKeywordSuggestions(); } let results = 0; @@ -178,6 +187,7 @@ export default class KustoQueryField extends QueryField { .filter(group => group.items.length > 0); // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); + // console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); this.setState({ typeaheadPrefix: prefix, @@ -283,7 +293,7 @@ export default class KustoQueryField extends QueryField { // ]; // } - private _getKeywordSuggestions(): SuggestionGroup[] { + private getKeywordSuggestions(): SuggestionGroup[] { return [ { prefixMatch: true, From 7626ce9922ccac4b15f82853eef8439ab37d78a7 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 4 Feb 2019 17:28:57 +0100 Subject: [PATCH 034/228] WIP Enable js defined theme to be used in SASS --- package.json | 1 + packages/grafana-ui/src/index.ts | 1 + packages/grafana-ui/src/theme.d.ts | 116 ++++++++++++++++++++++ packages/grafana-ui/src/theme.js | 15 +++ packages/grafana-ui/src/themes/dark.js | 64 ++++++++++++ packages/grafana-ui/src/themes/default.js | 47 +++++++++ packages/grafana-ui/src/themes/light.js | 65 ++++++++++++ public/sass/_variables.dark.scss | 101 +++++++++---------- public/sass/_variables.light.scss | 94 +++++++++--------- public/sass/_variables.scss | 44 ++++---- scripts/webpack/getThemeVariable.js | 53 ++++++++++ scripts/webpack/sass.rule.js | 13 ++- scripts/webpack/webpack.hot.js | 10 +- yarn.lock | 5 + 14 files changed, 506 insertions(+), 123 deletions(-) create mode 100644 packages/grafana-ui/src/theme.d.ts create mode 100644 packages/grafana-ui/src/theme.js create mode 100644 packages/grafana-ui/src/themes/dark.js create mode 100644 packages/grafana-ui/src/themes/default.js create mode 100644 packages/grafana-ui/src/themes/light.js create mode 100644 scripts/webpack/getThemeVariable.js diff --git a/package.json b/package.json index 77fd92baf57..60a8a20cde3 100644 --- a/package.json +++ b/package.json @@ -76,6 +76,7 @@ "ng-annotate-webpack-plugin": "^0.3.0", "ngtemplate-loader": "^2.0.1", "node-sass": "^4.11.0", + "node-sass-utils": "^1.1.2", "npm": "^5.4.2", "optimize-css-assets-webpack-plugin": "^4.0.2", "phantomjs-prebuilt": "^2.1.15", diff --git a/packages/grafana-ui/src/index.ts b/packages/grafana-ui/src/index.ts index 974d976bbef..4ddc7c8485a 100644 --- a/packages/grafana-ui/src/index.ts +++ b/packages/grafana-ui/src/index.ts @@ -1,3 +1,4 @@ export * from './components'; export * from './types'; export * from './utils'; +export * from './theme'; diff --git a/packages/grafana-ui/src/theme.d.ts b/packages/grafana-ui/src/theme.d.ts new file mode 100644 index 00000000000..015bcd16136 --- /dev/null +++ b/packages/grafana-ui/src/theme.d.ts @@ -0,0 +1,116 @@ +export interface GrafanaThemeType { + name: string; + // TODO: not sure if should be a part of theme + brakpoints: { + xs: string; + s: string; + m: string; + l: string; + xl: string; + }; + typography: { + fontFamily: { + sansSerif: string; + serif: string; + monospace: string; + }; + size: { + base: string; + xs: string; + s: string; + m: string; + l: string; + }; + weight: { + light: number; + normal: number; + semibold: number; + }; + lineHeight: { + xs: number; //1 + s: number; //1.1 + m: number; // 4/3 + l: number; // 1.5 + }; + // TODO: Refactor to use size instead of custom defs + heading: { + h1: string; + h2: string; + h3: string; + h4: string; + h5: string; + h6: string; + }; + }; + spacing: { + xs: string; + s: string; + m: string; + l: string; + gutter: string; + }; + border: { + radius: { + xs: string; + s: string; + m: string; + }; + }; + colors: { + black: string; + white: string; + dark1: string; + dark2: string; + dark3: string; + dark4: string; + dark5: string; + gray1: string; + gray2: string; + gray3: string; + gray4: string; + gray5: string; + gray6: string; + gray7: string; + grayBlue: string; + inputBlack: string; + + // Accent colors + blue: string; + blueLight: string; + blueDark: string; + green: string; + red: string; + yellow: string; + pink: string; + purple: string; + variable: string; + orange: string; + queryRed: string; + queryGreen: string; + queryPurple: string; + queryKeyword: string; + queryOrange: string; + + // Status colors + online: string; + warn: string; + critical: string; + + // TODO: should this be a part of theme? + bodyBg: string; + pageBg: string; + bodyColor: string; + textColor: string; + textColorStrong: string; + textColorWeak: string; + textColorFaint: string; + textColorEmphasis: string; + linkColor: string; + linkColorDisabled: string; + linkColorHover: string; + linkColorExternal: string; + headingColor: string; + }; +} +export function getTheme(): GrafanaThemeType +export function mockTheme(themeMock: Partial): () => void diff --git a/packages/grafana-ui/src/theme.js b/packages/grafana-ui/src/theme.js new file mode 100644 index 00000000000..3d0695e2490 --- /dev/null +++ b/packages/grafana-ui/src/theme.js @@ -0,0 +1,15 @@ +const darkTheme = require('./themes/dark'); +const lightTheme = require('./themes/light'); + +const getTheme = name => (name === 'light' ? lightTheme : darkTheme); + +const mockTheme = mock => { + const originalGetTheme = getTheme; + getTheme = () => mock; + return () => (getTheme = originalGetTheme); +}; + +module.exports = { + getTheme, + mockTheme, +}; diff --git a/packages/grafana-ui/src/themes/dark.js b/packages/grafana-ui/src/themes/dark.js new file mode 100644 index 00000000000..c80a4593f53 --- /dev/null +++ b/packages/grafana-ui/src/themes/dark.js @@ -0,0 +1,64 @@ + + +const defaultTheme = require('./default'); +const tinycolor = require('tinycolor2'); + +const basicColors = { + black: '#00ff00', + white: '#ffffff', + dark1: '#141414', + dark2: '#1f1f20', + dark3: '#262628', + dark4: '#333333', + dark5: '#444444', + gray1: '#555555', + gray2: '#8e8e8e', + gray3: '#b3b3b3', + gray4: '#d8d9da', + gray5: '#ececec', + gray6: '#f4f5f8', + gray7: '#fbfbfb', + grayBlue: '#212327', + blue: '#33b5e5', + blueDark: '#005f81', + blueLight: '#00a8e6', // not used in dark theme + green: '#299c46', + red: '#d44a3a', + yellow: '#ecbb13', + pink: '#ff4444', + purple: '#9933cc', + variable: '#32d1df', + orange: '#eb7b18', +}; + +const darkTheme = { + ...defaultTheme, + name: 'Grafana Dark', + colors: { + ...basicColors, + inputBlack: '#09090b', + queryRed: '#e24d42', + queryGreen: '#74e680', + queryPurple: '#fe85fc', + queryKeyword: '#66d9ef', + queryOrange: 'eb7b18', + online: '#10a345', + warn: '#f79520', + critical: '#ed2e18', + bodyBg: '#171819', + pageBg: '#161719', + bodyColor: basicColors.gray4, + textColor: basicColors.gray4, + textColorStrong: basicColors.white, + textColorWeak: basicColors.gray2, + textColorEmphasis: basicColors.gray5, + textColorFaint: basicColors.dark5, + linkColor: new tinycolor(basicColors.white).darken(11).toString(), + linkColorDisabled: new tinycolor(basicColors.white).darken(11).toString(), + linkColorHover: basicColors.white, + linkColorExternal: basicColors.blue, + headingColor: new tinycolor(basicColors.white).darken(11).toString(), + } +} + +module.exports = darkTheme; diff --git a/packages/grafana-ui/src/themes/default.js b/packages/grafana-ui/src/themes/default.js new file mode 100644 index 00000000000..59ed050e360 --- /dev/null +++ b/packages/grafana-ui/src/themes/default.js @@ -0,0 +1,47 @@ + + +const theme = { + name: 'Grafana Default', + typography: { + fontFamily: { + sansSerif: "'Roboto', Helvetica, Arial, sans-serif;", + serif: "Georgia, 'Times New Roman', Times, serif;", + monospace: "Menlo, Monaco, Consolas, 'Courier New', monospace;" + }, + size: { + base: '13px', + xs: '10px', + s: '12px', + m: '14px', + l: '18px', + }, + heading: { + h1: '2rem', + h2: '1.75rem', + h3: '1.5rem', + h4: '1.3rem', + h5: '1.2rem', + h6: '1rem', + }, + weight: { + light: 300, + normal: 400, + semibold: 500, + }, + lineHeight: { + xs: 1, + s: 1.1, + m: 4/3, + l: 1.5 + } + }, + brakpoints: { + xs: '0', + s: '544px', + m: '768px', + l: '992px', + xl: '1200px' + } +}; + +module.exports = theme; diff --git a/packages/grafana-ui/src/themes/light.js b/packages/grafana-ui/src/themes/light.js new file mode 100644 index 00000000000..84d1e656baa --- /dev/null +++ b/packages/grafana-ui/src/themes/light.js @@ -0,0 +1,65 @@ +// import { GrafanaThemeType } from "../theme"; + +const defaultTheme = require('./default'); +const tinycolor = require('tinycolor2'); + +const basicColors = { + black: '#000000', + white: '#ffffff', + dark1: '#13161d', + dark2: '#1e2028', + dark3: '#303133', + dark4: '#35373f', + dark5: '#41444b', + gray1: '#52545c', + gray2: '#767980', + gray3: '#acb6bf', + gray4: '#c7d0d9', + gray5: '#dde4ed', + gray6: '#e9edf2', + gray7: '#f7f8fa', + grayBlue: '#212327', // not used in light theme + blue: '#0083b3', + blueDark: '#005f81', + blueLight: '#00a8e6', + green: '#3aa655', + red: '#d44939', + yellow: '#ff851b', + pink: '#e671b8', + purple: '#9954bb', + variable: '#0083b3', + orange: '#ff7941', +}; + +const lightTheme/*: GrafanaThemeType*/ = { + ...defaultTheme, + name: 'Grafana Light', + colors: { + ...basicColors, + variable: basicColors.blue, + inputBlack: '#09090b', + queryRed: basicColors.red, + queryGreen: basicColors.green, + queryPurple: basicColors.purple, + queryKeyword: basicColors.blue, + queryOrange: basicColors.orange, + online: '#01a64f', + warn: '#f79520', + critical: '#ec2128', + bodyBg: basicColors.gray7, + pageBg: basicColors.gray7, + bodyColor: basicColors.gray1, + textColor: basicColors.gray1, + textColorStrong: basicColors.dark2, + textColorWeak: basicColors.gray2, + textColorEmphasis: basicColors.gray5, + textColorFaint: basicColors.dark4, + linkColor: basicColors.gray1, + linkColorDisabled: new tinycolor(basicColors.gray1).lighten(30).toString(), + linkColorHover: new tinycolor(basicColors.gray1).darken(20).toString(), + linkColorExternal: basicColors.blueLight, + headingColor: basicColors.gray1, + } +} + +module.exports = lightTheme; diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 7b0ed869bdc..61e2c8bfc76 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -3,73 +3,69 @@ $theme-name: dark; -// Grays // ------------------------- -$black: #000; +$black: getThemeVariable('colors.black', $theme-name); +$dark-1: getThemeVariable('colors.dark1', $theme-name); +$dark-2: getThemeVariable('colors.dark2', $theme-name); +$dark-3: getThemeVariable('colors.dark3', $theme-name); +$dark-4: getThemeVariable('colors.dark4', $theme-name); +$dark-5: getThemeVariable('colors.dark5', $theme-name); +$gray-1: getThemeVariable('colors.gray1', $theme-name); +$gray-2: getThemeVariable('colors.gray2', $theme-name); +$gray-3: getThemeVariable('colors.gray3', $theme-name); +$gray-4: getThemeVariable('colors.gray4', $theme-name); +$gray-5: getThemeVariable('colors.gray5', $theme-name); +$gray-6: getThemeVariable('colors.gray6', $theme-name); +$gray-7: getThemeVariable('colors.gray7', $theme-name); -// ------------------------- -$black: #000; -$dark-1: #141414; -$dark-2: #1f1f20; -$dark-3: #262628; -$dark-4: #333333; -$dark-5: #444444; -$gray-1: #555555; -$gray-2: #8e8e8e; -$gray-3: #b3b3b3; -$gray-4: #d8d9da; -$gray-5: #ececec; -$gray-6: #f4f5f8; -$gray-7: #fbfbfb; +$gray-blue: getThemeVariable('colors.grayBlue', $theme-name); +$input-black: getThemeVariable('colors.inputBlack', $theme-name); -$gray-blue: #212327; -$input-black: #09090b; - -$white: #fff; +$white: getThemeVariable('colors.white', $theme-name); // Accent colors // ------------------------- -$blue: #33b5e5; -$blue-dark: #005f81; -$green: #299c46; -$red: #d44a3a; -$yellow: #ecbb13; -$pink: #ff4444; -$purple: #9933cc; -$variable: #32d1df; -$orange: #eb7b18; +$blue: getThemeVariable('colors.blue', $theme-name); +$blue-dark: getThemeVariable('colors.blueDark', $theme-name); +$green: getThemeVariable('colors.green', $theme-name); +$red: getThemeVariable('colors.red', $theme-name); +$yellow: getThemeVariable('colors.yellow', $theme-name); +$pink: getThemeVariable('colors.pink', $theme-name); +$purple: getThemeVariable('colors.purple', $theme-name); +$variable: getThemeVariable('colors.variable', $theme-name); +$orange: getThemeVariable('colors.orange', $theme-name); $brand-primary: $orange; $brand-success: $green; $brand-warning: $brand-primary; $brand-danger: $red; -$query-red: #e24d42; -$query-green: #74e680; -$query-purple: #fe85fc; -$query-keyword: #66d9ef; -$query-orange: $orange; +$query-red: getThemeVariable('colors.queryRed', $theme-name); +$query-green: getThemeVariable('colors.queryGreen', $theme-name); +$query-purple: getThemeVariable('colors.queryPurple', $theme-name); +$query-keyword: getThemeVariable('colors.queryKeyword', $theme-name); +$query-orange: getThemeVariable('colors.queryOrange', $theme-name); // Status colors // ------------------------- -$online: #10a345; -$warn: #f79520; -$critical: #ed2e18; +$online: getThemeVariable('colors.online', $theme-name); +$warn: getThemeVariable('colors.warn', $theme-name); +$critical: getThemeVariable('colors.critical', $theme-name); // Scaffolding // ------------------------- -$body-bg: rgb(23, 24, 25); -$page-bg: rgb(22, 23, 25); +$body-bg: getThemeVariable('colors.bodyBg', $theme-name); +$page-bg: getThemeVariable('colors.pageBg', $theme-name); -$body-color: $gray-4; -$text-color: $gray-4; -$text-color-strong: $white; -$text-color-weak: $gray-2; -$text-color-faint: $dark-5; -$text-color-emphasis: $gray-5; +$body-color: getThemeVariable('colors.bodyColor', $theme-name); +$text-color: getThemeVariable('colors.textColor', $theme-name); +$text-color-strong: getThemeVariable('colors.textColorStrong', $theme-name); +$text-color-weak: getThemeVariable('colors.textColorWeak', $theme-name); +$text-color-faint: getThemeVariable('colors.textColorFaint', $theme-name); +$text-color-emphasis: getThemeVariable('colors.textColorEmphasis', $theme-name); -$text-shadow-strong: 1px 1px 4px $black; -$text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); +$text-shadow-strong: 1px 1px 4px getThemeVariable('colors.black', $theme-name); +$text-shadow-faint: 1px 1px 4px #2d2d2d; // gradients $brand-gradient: linear-gradient( @@ -84,10 +80,11 @@ $edit-gradient: linear-gradient(180deg, rgb(22, 23, 25) 50%, #090909); // Links // ------------------------- -$link-color: darken($white, 11%); -$link-color-disabled: darken($link-color, 30%); -$link-hover-color: $white; -$external-link-color: $blue; +$link-color: getThemeVariable('colors.linkColor', $theme-name); +$link-color-disabled: getThemeVariable('colors.linkColorDisabled', $theme-name); +$link-hover-color: getThemeVariable('colors.linkColorHover', $theme-name); + +$external-link-color: getThemeVariable('colors.linkColorExternal', $theme-name); // Typography // ------------------------- @@ -135,7 +132,7 @@ $list-item-shadow: $card-shadow; $empty-list-cta-bg: $gray-blue; // Scrollbars -$scrollbarBackground: #404357; +$scrollbarBackground: #aeb5df; $scrollbarBackground2: #3a3a3a; $scrollbarBorder: black; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 10c074e1481..ad2b22e201a 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -12,83 +12,85 @@ $theme-name: light; $black: #000; // ------------------------- -$black: #000; -$dark-1: #13161d; -$dark-2: #1e2028; -$dark-3: #303133; -$dark-4: #35373f; -$dark-5: #41444b; -$gray-1: #52545c; -$gray-2: #767980; -$gray-3: #acb6bf; -$gray-4: #c7d0d9; -$gray-5: #dde4ed; -$gray-6: #e9edf2; -$gray-7: #f7f8fa; +$black: getThemeVariable('colors.black', $theme-name); +$dark-1: getThemeVariable('colors.dark1', $theme-name); +$dark-2: getThemeVariable('colors.dark2', $theme-name); +$dark-3: getThemeVariable('colors.dark3', $theme-name); +$dark-4: getThemeVariable('colors.dark4', $theme-name); +$dark-5: getThemeVariable('colors.dark5', $theme-name); +$gray-1: getThemeVariable('colors.gray1', $theme-name); +$gray-2: getThemeVariable('colors.gray2', $theme-name); +$gray-3: getThemeVariable('colors.gray3', $theme-name); +$gray-4: getThemeVariable('colors.gray4', $theme-name); +$gray-5: getThemeVariable('colors.gray5', $theme-name); +$gray-6: getThemeVariable('colors.gray6', $theme-name); +$gray-7: getThemeVariable('colors.gray7', $theme-name); -$white: #fff; +$white: getThemeVariable('colors.white', $theme-name); // Accent colors // ------------------------- -$blue: #0083b3; -$blue-dark: #005f81; -$blue-light: #00a8e6; -$green: #3aa655; -$red: #d44939; -$yellow: #ff851b; -$orange: #ff7941; -$pink: #e671b8; -$purple: #9954bb; -$variable: $blue; +$blue: getThemeVariable('colors.blue', $theme-name); +$blue-dark: getThemeVariable('colors.blueDark', $theme-name); +$blue-light: getThemeVariable('colors.blueLight', $theme-name); +$green: getThemeVariable('colors.green', $theme-name); +$red: getThemeVariable('colors.red', $theme-name); +$yellow: getThemeVariable('colors.yellow', $theme-name); +$orange: getThemeVariable('colors.orange', $theme-name); +$pink: getThemeVariable('colors.pink', $theme-name); +$purple: getThemeVariable('colors.purple', $theme-name); +$variable: getThemeVariable('colors.variable', $theme-name); $brand-primary: $orange; $brand-success: $green; $brand-warning: $orange; $brand-danger: $red; -$query-red: $red; -$query-green: $green; -$query-purple: $purple; -$query-orange: $orange; -$query-keyword: $blue; +$query-red: getThemeVariable('colors.queryRed', $theme-name); +$query-green: getThemeVariable('colors.queryGreen', $theme-name); +$query-purple: getThemeVariable('colors.queryPurple', $theme-name); +$query-keyword: getThemeVariable('colors.queryKeyword', $theme-name); +$query-orange: getThemeVariable('colors.queryOrange', $theme-name); // Status colors // ------------------------- -$online: #01a64f; -$warn: #f79520; -$critical: #ec2128; +$online: getThemeVariable('colors.online', $theme-name); +$warn: getThemeVariable('colors.warn', $theme-name); +$critical: getThemeVariable('colors.critical', $theme-name); // Scaffolding // ------------------------- -$body-bg: $gray-7; -$page-bg: $gray-7; -$body-color: $gray-1; -$text-color: $gray-1; -$text-color-strong: $dark-2; -$text-color-weak: $gray-2; -$text-color-faint: $gray-4; -$text-color-emphasis: $dark-5; +$body-bg: getThemeVariable('colors.bodyBg', $theme-name); +$page-bg: getThemeVariable('colors.pageBg', $theme-name); + +$body-color: getThemeVariable('colors.bodyColor', $theme-name); +$text-color: getThemeVariable('colors.textColor', $theme-name); +$text-color-strong: getThemeVariable('colors.textColorStrong', $theme-name); +$text-color-weak: getThemeVariable('colors.textColorWeak', $theme-name); +$text-color-faint: getThemeVariable('colors.textColorFaint', $theme-name); +$text-color-emphasis: getThemeVariable('colors.textColorEmphasis', $theme-name); $text-shadow-strong: none; $text-shadow-faint: none; $textShadow: none; // gradients -$brand-gradient: linear-gradient(to right, rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); +$brand-gradient: linear-gradient(to right, hsl(50, 100%, 50%) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); $page-gradient: linear-gradient(180deg, $white 10px, $gray-7 100px); $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%); // Links // ------------------------- -$link-color: $gray-1; -$link-color-disabled: lighten($link-color, 30%); -$link-hover-color: darken($link-color, 20%); -$external-link-color: $blue-light; +$link-color: getThemeVariable('colors.linkColor', $theme-name); +$link-color-disabled: getThemeVariable('colors.linkColorDisabled', $theme-name); +$link-hover-color: getThemeVariable('colors.linkColorHover', $theme-name); + +$external-link-color: getThemeVariable('colors.linkColorExternal', $theme-name); // Typography // ------------------------- -$headings-color: $text-color; +$headings-color: getThemeVariable('colors.headingColor', $theme-name); $abbr-border-color: $gray-2 !default; $text-muted: $text-color-weak; diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 4e9e69c4d2f..eab0e8c7f5a 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -47,45 +47,45 @@ $enable-flex: true; // Typography // ------------------------- -$font-family-sans-serif: 'Roboto', Helvetica, Arial, sans-serif; -$font-family-serif: Georgia, 'Times New Roman', Times, serif; -$font-family-monospace: Menlo, Monaco, Consolas, 'Courier New', monospace; +$font-family-sans-serif: getThemeVariable('typography.fontFamily.sansSerif'); +$font-family-serif: getThemeVariable('typography.fontFamily.serif'); +$font-family-monospace: getThemeVariable('typography.fontFamily.monospace'); $font-family-base: $font-family-sans-serif !default; -$font-size-root: 14px !default; -$font-size-base: 13px !default; +$font-size-root: getThemeVariable('typography.size.m') !default; +$font-size-base: getThemeVariable('typography.size.base') !default; -$font-size-lg: 18px !default; -$font-size-md: 14px !default; -$font-size-sm: 12px !default; -$font-size-xs: 10px !default; +$font-size-lg: getThemeVariable('typography.size.l') !default; +$font-size-md: getThemeVariable('typography.size.m') !default; +$font-size-sm: getThemeVariable('typography.size.s') !default; +$font-size-xs: getThemeVariable('typography.size.xs') !default; -$line-height-base: 1.5 !default; -$font-weight-semi-bold: 500; +$line-height-base: getThemeVariable('typography.lineHeight.l') !default; +$font-weight-semi-bold: getThemeVariable('typography.weight.semibold'); -$font-size-h1: 2rem !default; -$font-size-h2: 1.75rem !default; -$font-size-h3: 1.5rem !default; -$font-size-h4: 1.3rem !default; -$font-size-h5: 1.2rem !default; -$font-size-h6: 1rem !default; +$font-size-h1: getThemeVariable('typography.heading.h1') !default; +$font-size-h2: getThemeVariable('typography.heading.h2') !default; +$font-size-h3: getThemeVariable('typography.heading.h3') !default; +$font-size-h4: getThemeVariable('typography.heading.h4') !default; +$font-size-h5: getThemeVariable('typography.heading.h5') !default; +$font-size-h6: getThemeVariable('typography.heading.h6') !default; $display1-size: 6rem !default; $display2-size: 5.5rem !default; $display3-size: 4.5rem !default; $display4-size: 3.5rem !default; -$display1-weight: 400 !default; -$display2-weight: 400 !default; -$display3-weight: 400 !default; -$display4-weight: 400 !default; +$display1-weight: getThemeVariable('typography.weight.normal') !default; +$display2-weight: getThemeVariable('typography.weight.normal') !default; +$display3-weight: getThemeVariable('typography.weight.normal') !default; +$display4-weight: getThe1meVariable('typography.weight.normal') !default; $lead-font-size: 1.25rem !default; $lead-font-weight: 300 !default; $headings-margin-bottom: ($spacer / 2) !default; $headings-font-family: 'Roboto', 'Helvetica Neue', Helvetica, Arial, sans-serif; -$headings-font-weight: 400 !default; +$headings-font-weight: getThemeVariable('typography.weight.normal') !default; $headings-line-height: 1.1 !default; $hr-border-width: $border-width !default; diff --git a/scripts/webpack/getThemeVariable.js b/scripts/webpack/getThemeVariable.js new file mode 100644 index 00000000000..c0b6bc4ed79 --- /dev/null +++ b/scripts/webpack/getThemeVariable.js @@ -0,0 +1,53 @@ +const sass = require('node-sass'); +const sassUtils = require('node-sass-utils')(sass); +const { getTheme } = require('../../packages/grafana-ui/src/theme'); +const { get } = require('lodash'); +const tinycolor = require('tinycolor2'); + +const units = ['rem', 'em', 'vh', 'vw', 'vmin', 'vmax', 'ex', '%', 'px', 'cm', 'mm', 'in', 'pt', 'pc', 'ch']; +const matchDimension = value => value.match(/[a-zA-Z]+|[0-9]+/g); + +const isHex = value => { + const hexRegex = /^((0x){0,1}|#{0,1})([0-9A-F]{8}|[0-9A-F]{6})$/gi; + return hexRegex.test(value); +}; + +const isDimension = value => { + if( typeof value !== "string") { + return false; + } + + const [val, unit] = matchDimension(value); + return units.indexOf(unit) > -1 +}; + +/** + * @param {SassString} variablePath + * @param {"dark"|"light"} themeName + */ +function getThemeVariable(variablePath, themeName) { + const theme = getTheme(themeName.getValue()); + const variable = get(theme, variablePath.getValue()); + + if (!variable) { + throw new Error(`${variablePath} is not defined fo ${themeName}`); + } + + if (isHex(variable)) { + const rgb = new tinycolor(variable).toRgb(); + const color = sass.types.Color(rgb.r, rgb.g, rgb.b); + return color; + } + + if (isDimension(variable)) { + const [value, unit] = matchDimension(variable) + + const tmp = new sassUtils.SassDimension(parseInt(value,10), unit); + // debugger + return sassUtils.castToSass(tmp) + } + + return sassUtils.castToSass(variable); +} + +module.exports = getThemeVariable; diff --git a/scripts/webpack/sass.rule.js b/scripts/webpack/sass.rule.js index 75455fc4184..78f6b60d33f 100644 --- a/scripts/webpack/sass.rule.js +++ b/scripts/webpack/sass.rule.js @@ -1,6 +1,7 @@ 'use strict'; -const MiniCssExtractPlugin = require("mini-css-extract-plugin"); +const MiniCssExtractPlugin = require('mini-css-extract-plugin'); +const getThemeVariable = require('./getThemeVariable'); module.exports = function(options) { return { @@ -23,7 +24,15 @@ module.exports = function(options) { config: { path: __dirname + '/postcss.config.js' }, }, }, - { loader: 'sass-loader', options: { sourceMap: options.sourceMap } }, + { + loader: 'sass-loader', + options: { + sourceMap: options.sourceMap, + functions: { + 'getThemeVariable($themeVar, $themeName: dark)': getThemeVariable, + }, + }, + }, ], }; }; diff --git a/scripts/webpack/webpack.hot.js b/scripts/webpack/webpack.hot.js index b37e4c08592..4519e292c6b 100644 --- a/scripts/webpack/webpack.hot.js +++ b/scripts/webpack/webpack.hot.js @@ -8,6 +8,7 @@ const HtmlWebpackPlugin = require('html-webpack-plugin'); const HtmlWebpackHarddiskPlugin = require('html-webpack-harddisk-plugin'); const CleanWebpackPlugin = require('clean-webpack-plugin'); const IgnoreNotFoundExportPlugin = require("./IgnoreNotFoundExportPlugin.js"); +const getThemeVariable = require("./getThemeVariable"); module.exports = merge(common, { entry: { @@ -85,7 +86,14 @@ module.exports = merge(common, { config: { path: __dirname + '/postcss.config.js' }, }, }, - 'sass-loader', // compiles Sass to CSS + { + loader: 'sass-loader', + options: { + functions: { + "getThemeVariable($themeVar, $themeName: dark)": getThemeVariable + } + } + } ], }, { diff --git a/yarn.lock b/yarn.lock index 169abd40ee4..bc94a05d702 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11910,6 +11910,11 @@ node-releases@^1.0.0-alpha.11, node-releases@^1.1.3: dependencies: semver "^5.3.0" +node-sass-utils@^1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/node-sass-utils/-/node-sass-utils-1.1.2.tgz#d03639cfa4fc962398ba3648ab466f0db7cc2131" + integrity sha1-0DY5z6T8liOYujZIq0ZvDbfMITE= + node-sass@^4.11.0: version "4.11.0" resolved "https://registry.yarnpkg.com/node-sass/-/node-sass-4.11.0.tgz#183faec398e9cbe93ba43362e2768ca988a6369a" From 3baaf2c3e4f13c8ee4e70984953f92f4839a6801 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 17:36:04 +0100 Subject: [PATCH 035/228] Added handling of kiosk mode --- .../dashboard/state/DashboardModel.ts | 11 ++--- .../features/dashboard/state/initDashboard.ts | 40 ++++++++++--------- public/app/routes/routes.ts | 4 +- public/app/types/dashboard.ts | 2 +- 4 files changed, 29 insertions(+), 28 deletions(-) diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index a6d45bb3cc0..8756af2ceea 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -15,6 +15,7 @@ import sortByKeys from 'app/core/utils/sort_by_keys'; import { PanelModel } from './PanelModel'; import { DashboardMigrator } from './DashboardMigrator'; import { TimeRange } from '@grafana/ui/src'; +import { UrlQueryValue } from 'app/types'; export class DashboardModel { id: any; @@ -867,11 +868,7 @@ export class DashboardModel { return !_.isEqual(updated, this.originalTemplating); } - autoFitPanels(viewHeight: number) { - if (!this.meta.autofitpanels) { - return; - } - + autoFitPanels(viewHeight: number, kioskMode?: UrlQueryValue) { const currentGridHeight = Math.max( ...this.panels.map(panel => { return panel.gridPos.h + panel.gridPos.y; @@ -885,12 +882,12 @@ export class DashboardModel { let visibleHeight = viewHeight - navbarHeight - margin; // Remove submenu height if visible - if (this.meta.submenuEnabled && !this.meta.kiosk) { + if (this.meta.submenuEnabled && !kioskMode) { visibleHeight -= submenuHeight; } // add back navbar height - if (this.meta.kiosk === 'b') { + if (kioskMode === 'tv') { visibleHeight += 55; } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 14d6196d69c..89b8d470d3c 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -56,10 +56,6 @@ export function initDashboard({ try { switch (routeInfo) { // handle old urls with no uid - case DashboardRouteInfo.Old: { - redirectToNewUrl(urlSlug, dispatch); - return; - } case DashboardRouteInfo.Home: { // load home dash dashDTO = await getBackendSrv().get('/api/dashboards/home'); @@ -78,20 +74,27 @@ export function initDashboard({ break; } case DashboardRouteInfo.Normal: { + // for old db routes we redirect + if (urlType === 'db') { + redirectToNewUrl(urlSlug, dispatch); + return; + } + const loaderSrv = $injector.get('dashboardLoaderSrv'); dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - // check if the current url is correct (might be old slug) - const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); - const currentPath = getState().location.path; - console.log('loading dashboard: currentPath', currentPath); - console.log('loading dashboard: dashboardUrl', dashboardUrl); + if (dashDTO.meta.url) { + // check if the current url is correct (might be old slug) + const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); + const currentPath = getState().location.path; - if (dashboardUrl !== currentPath) { - // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. - dispatch(updateLocation({ path: dashboardUrl, partial: true, replace: true })); - return; + if (dashboardUrl !== currentPath) { + // replace url to not create additional history items and then return so that initDashboard below isn't executed multiple times. + dispatch(updateLocation({ path: dashboardUrl, partial: true, replace: true })); + return; + } } + break; } case DashboardRouteInfo.New: { @@ -129,7 +132,6 @@ export function initDashboard({ const variableSrv: VariableSrv = $injector.get('variableSrv'); const keybindingSrv: KeybindingSrv = $injector.get('keybindingSrv'); const unsavedChangesSrv = $injector.get('unsavedChangesSrv'); - const viewStateSrv = $injector.get('dashboardViewStateSrv'); const dashboardSrv: DashboardSrv = $injector.get('dashboardSrv'); timeSrv.init(dashboard); @@ -147,14 +149,16 @@ export function initDashboard({ try { dashboard.processRepeats(); dashboard.updateSubmenuVisibility(); - dashboard.autoFitPanels(window.innerHeight); + + // handle auto fix experimental feature + const queryParams = getState().location.query; + if (queryParams.autofitpanels) { + dashboard.autoFitPanels(window.innerHeight, queryParams.kiosk); + } // init unsaved changes tracking unsavedChangesSrv.init(dashboard, $scope); - $scope.dashboard = dashboard; - viewStateSrv.create($scope); - // dashboard keybindings should not live in core, this needs a bigger refactoring // So declaring this here so it can depend on the removePanel util function // Long term onRemovePanel should be handled via react prop callback diff --git a/public/app/routes/routes.ts b/public/app/routes/routes.ts index ecd934cdccf..e0029cf2464 100644 --- a/public/app/routes/routes.ts +++ b/public/app/routes/routes.ts @@ -62,7 +62,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard/:type/:slug', { template: '', pageClass: 'page-dashboard', - routeInfo: DashboardRouteInfo.Old, + routeInfo: DashboardRouteInfo.Normal, reloadOnSearch: false, resolve: { component: () => DashboardPage, @@ -88,7 +88,7 @@ export function setupAngularRoutes($routeProvider, $locationProvider) { .when('/dashboard-solo/:type/:slug', { template: '', pageClass: 'dashboard-solo', - routeInfo: DashboardRouteInfo.Old, + routeInfo: DashboardRouteInfo.Normal, resolve: { component: () => SoloPanelPage, }, diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 9b8f539aeb2..36c0a420f28 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -8,10 +8,10 @@ export interface MutableDashboard { } export enum DashboardRouteInfo { - Old = 'old-dashboard', Home = 'home-dashboard', New = 'new-dashboard', Normal = 'normal-dashboard', + Scripted = 'scripted-dashboard', } export enum DashboardLoadingState { From 23ac9405c16b259887a9a5610e955537950de811 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 17:39:29 +0100 Subject: [PATCH 036/228] Set page title on dashboard load --- .../dashboard/containers/DashboardPage.tsx | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index ec143d735ab..3b902e4d089 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -72,6 +72,13 @@ export class DashboardPage extends PureComponent { }); } + componentWillUnmount() { + if (this.props.dashboard) { + this.props.dashboard.destroy(); + this.props.setDashboardModel(null); + } + } + componentDidUpdate(prevProps: Props) { const { dashboard, editview, urlEdit, urlFullscreen, urlPanelId } = this.props; @@ -79,6 +86,11 @@ export class DashboardPage extends PureComponent { return; } + // if we just got dashboard update title + if (!prevProps.dashboard) { + document.title = dashboard.title + ' - Grafana'; + } + // handle animation states when opening dashboard settings if (!prevProps.editview && editview) { this.setState({ isSettingsOpening: true }); @@ -135,13 +147,6 @@ export class DashboardPage extends PureComponent { $('body').toggleClass('panel-in-fullscreen', isFullscreen); } - componentWillUnmount() { - if (this.props.dashboard) { - this.props.dashboard.destroy(); - this.props.setDashboardModel(null); - } - } - renderLoadingState() { return ; } From d978a66ef6b1b1b496959e017d5d714d1f187ef7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 18:24:56 +0100 Subject: [PATCH 037/228] Fixed lots of loading flow issues and updated solo route page --- public/app/core/utils/location_util.ts | 2 +- .../dashboard/components/DashNav/DashNav.tsx | 148 ++++++++++-------- .../dashboard/containers/DashboardPage.tsx | 1 + .../dashboard/containers/SoloPanelPage.tsx | 92 +++++------ .../dashboard/services/DashboardSrv.ts | 1 - .../features/dashboard/state/initDashboard.ts | 21 ++- public/app/routes/GrafanaCtrl.ts | 6 - 7 files changed, 137 insertions(+), 134 deletions(-) diff --git a/public/app/core/utils/location_util.ts b/public/app/core/utils/location_util.ts index 76f2fc5881f..15e1c275550 100644 --- a/public/app/core/utils/location_util.ts +++ b/public/app/core/utils/location_util.ts @@ -1,6 +1,6 @@ import config from 'app/core/config'; -export const stripBaseFromUrl = url => { +export const stripBaseFromUrl = (url: string): string => { const appSubUrl = config.appSubUrl; const stripExtraChars = appSubUrl.endsWith('/') ? 1 : 0; const urlWithoutBase = diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 66edb149433..00f89920727 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -5,6 +5,7 @@ import { connect } from 'react-redux'; // Utils & Services import { AngularComponent, getAngularLoader } from 'app/core/services/AngularLoader'; import { appEvents } from 'app/core/app_events'; +import { PlaylistSrv } from 'app/features/playlist/playlist_srv'; // Components import { DashNavButton } from './DashNavButton'; @@ -116,12 +117,13 @@ export class DashNav extends PureComponent { }; render() { - const { dashboard, isFullscreen, editview } = this.props; + const { dashboard, isFullscreen, editview, $injector } = this.props; const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; const snapshotUrl = snapshot && snapshot.originalUrl; + const playlistSrv: PlaylistSrv = $injector.get('playlistSrv'); return (
@@ -135,13 +137,29 @@ export class DashNav extends PureComponent {
- {/* - - */} + + {playlistSrv.isPlaying && ( +
+ playlistSrv.prev()} + /> + playlistSrv.stop()} + /> + playlistSrv.next()} + /> +
+ )}
{canSave && ( @@ -151,71 +169,71 @@ export class DashNav extends PureComponent { icon="gicon gicon-add-panel" onClick={this.onAddPanel} /> - )} + )} - {canStar && ( - - )} + {canStar && ( + + )} - {canShare && ( - - )} + {canShare && ( + + )} - {canSave && ( - - )} + {canSave && ( + + )} - {snapshotUrl && ( - - )} + {snapshotUrl && ( + + )} - {showSettings && ( - - )} -
+ {showSettings && ( + + )} +
-
- -
+
+ +
-
(this.timePickerEl = element)} /> +
(this.timePickerEl = element)} /> - {(isFullscreen || editview) && ( -
- -
- )} -
+ {(isFullscreen || editview) && ( +
+ +
+ )} +
); } } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3b902e4d089..a7b3f51d92c 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -69,6 +69,7 @@ export class DashboardPage extends PureComponent { urlType: this.props.urlType, urlFolderId: this.props.urlFolderId, routeInfo: this.props.routeInfo, + fixUrl: true, }); } diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index 097c8015929..beb45b6904d 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -3,98 +3,78 @@ import React, { Component } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; -// Utils & Services -import appEvents from 'app/core/app_events'; -import locationUtil from 'app/core/utils/location_util'; -import { getBackendSrv } from 'app/core/services/backend_srv'; - // Components import { DashboardPanel } from '../dashgrid/DashboardPanel'; // Redux -import { updateLocation } from 'app/core/actions'; +import { initDashboard } from '../state/initDashboard'; // Types -import { StoreState } from 'app/types'; +import { StoreState, DashboardRouteInfo } from 'app/types'; import { PanelModel, DashboardModel } from 'app/features/dashboard/state'; interface Props { - panelId: string; + urlPanelId: string; urlUid?: string; urlSlug?: string; urlType?: string; $scope: any; $injector: any; - updateLocation: typeof updateLocation; + routeInfo: DashboardRouteInfo; + initDashboard: typeof initDashboard; + dashboard: DashboardModel | null; } interface State { panel: PanelModel | null; - dashboard: DashboardModel | null; notFound: boolean; } export class SoloPanelPage extends Component { - state: State = { panel: null, - dashboard: null, notFound: false, }; componentDidMount() { - const { $injector, $scope, urlUid, urlType, urlSlug } = this.props; + const { $injector, $scope, urlUid, urlType, urlSlug, routeInfo } = this.props; - // handle old urls with no uid - if (!urlUid && !(urlType === 'script' || urlType === 'snapshot')) { - this.redirectToNewUrl(); - return; - } - - const dashboardLoaderSrv = $injector.get('dashboardLoaderSrv'); - - // subscribe to event to know when dashboard controller is done with inititalization - appEvents.on('dashboard-initialized', this.onDashoardInitialized); - - dashboardLoaderSrv.loadDashboard(urlType, urlSlug, urlUid).then(result => { - result.meta.soloMode = true; - $scope.initDashboard(result, $scope); + this.props.initDashboard({ + $injector: $injector, + $scope: $scope, + urlSlug: urlSlug, + urlUid: urlUid, + urlType: urlType, + routeInfo: routeInfo, + fixUrl: false, }); } - redirectToNewUrl() { - getBackendSrv().getDashboardBySlug(this.props.urlSlug).then(res => { - if (res) { - const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - this.props.updateLocation(url); + componentDidUpdate(prevProps: Props) { + const { urlPanelId, dashboard } = this.props; + + if (!dashboard) { + return; + } + + // we just got the dashboard! + if (!prevProps.dashboard) { + const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + if (!panel) { + this.setState({ notFound: true }); + return; } - }); - } - onDashoardInitialized = () => { - const { $scope, panelId } = this.props; - - const dashboard: DashboardModel = $scope.dashboard; - const panel = dashboard.getPanelById(parseInt(panelId, 10)); - - if (!panel) { - this.setState({ notFound: true }); - return; + this.setState({ panel }); } - - this.setState({ dashboard, panel }); - }; + } render() { - const { panelId } = this.props; - const { notFound, panel, dashboard } = this.state; + const { urlPanelId, dashboard } = this.props; + const { notFound, panel } = this.state; if (notFound) { - return ( -
- Panel with id { panelId } not found -
- ); + return
Panel with id {urlPanelId} not found
; } if (!panel) { @@ -113,11 +93,13 @@ const mapStateToProps = (state: StoreState) => ({ urlUid: state.location.routeParams.uid, urlSlug: state.location.routeParams.slug, urlType: state.location.routeParams.type, - panelId: state.location.query.panelId + urlPanelId: state.location.query.panelId, + loadingState: state.dashboard.loadingState, + dashboard: state.dashboard.model as DashboardModel, }); const mapDispatchToProps = { - updateLocation + initDashboard, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(SoloPanelPage)); diff --git a/public/app/features/dashboard/services/DashboardSrv.ts b/public/app/features/dashboard/services/DashboardSrv.ts index e2e524941f8..38fadfecdc1 100644 --- a/public/app/features/dashboard/services/DashboardSrv.ts +++ b/public/app/features/dashboard/services/DashboardSrv.ts @@ -8,7 +8,6 @@ export class DashboardSrv { /** @ngInject */ constructor(private backendSrv, private $rootScope, private $location) { - appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); appEvents.on('save-dashboard', this.saveDashboard.bind(this), $rootScope); appEvents.on('panel-change-view', this.onPanelChangeView); } diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 89b8d470d3c..5419fcb41d7 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -25,16 +25,24 @@ export interface InitDashboardArgs { urlUid?: string; urlSlug?: string; urlType?: string; - urlFolderId: string; + urlFolderId?: string; routeInfo: string; + fixUrl: boolean; } -async function redirectToNewUrl(slug: string, dispatch: any) { +async function redirectToNewUrl(slug: string, dispatch: any, currentPath: string) { const res = await getBackendSrv().getDashboardBySlug(slug); if (res) { - const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/')); - dispatch(updateLocation(url)); + let newUrl = res.meta.url; + + // fix solo route urls + if (currentPath.indexOf('dashboard-solo') !== -1) { + newUrl = newUrl.replace('/d/', '/d-solo/'); + } + + const url = locationUtil.stripBaseFromUrl(newUrl); + dispatch(updateLocation({ path: url, partial: true, replace: true })); } } @@ -46,6 +54,7 @@ export function initDashboard({ urlType, urlFolderId, routeInfo, + fixUrl, }: InitDashboardArgs): ThunkResult { return async (dispatch, getState) => { let dashDTO = null; @@ -76,14 +85,14 @@ export function initDashboard({ case DashboardRouteInfo.Normal: { // for old db routes we redirect if (urlType === 'db') { - redirectToNewUrl(urlSlug, dispatch); + redirectToNewUrl(urlSlug, dispatch, getState().location.path); return; } const loaderSrv = $injector.get('dashboardLoaderSrv'); dashDTO = await loaderSrv.loadDashboard(urlType, urlSlug, urlUid); - if (dashDTO.meta.url) { + if (fixUrl && dashDTO.meta.url) { // check if the current url is correct (might be old slug) const dashboardUrl = locationUtil.stripBaseFromUrl(dashDTO.meta.url); const currentPath = getState().location.path; diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 817e6452f44..f87fe69a684 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -45,12 +45,6 @@ export class GrafanaCtrl { }; $rootScope.colors = colors; - - $scope.initDashboard = (dashboardData, viewScope) => { - $scope.appEvent('dashboard-fetch-end', dashboardData); - $controller('DashboardCtrl', { $scope: viewScope }).init(dashboardData); - }; - $rootScope.onAppEvent = function(name, callback, localScope) { const unbind = $rootScope.$on(name, callback); let callerScope = this; From 70974c01f2cdf487cc0e24800f93db2333ae26ff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 21:08:30 +0100 Subject: [PATCH 038/228] Added playlist controls to new react DashNav --- pkg/api/dtos/playlist.go | 1 + pkg/api/playlist_play.go | 1 + .../dashboard/components/DashNav/DashNav.tsx | 37 ++++++++++++++----- .../dashboard/containers/DashboardPage.tsx | 6 --- public/app/features/playlist/playlist_srv.ts | 23 +++++++++--- public/app/routes/GrafanaCtrl.ts | 17 ++++----- 6 files changed, 55 insertions(+), 30 deletions(-) diff --git a/pkg/api/dtos/playlist.go b/pkg/api/dtos/playlist.go index 317ff83339a..7f43bb4df8a 100644 --- a/pkg/api/dtos/playlist.go +++ b/pkg/api/dtos/playlist.go @@ -5,6 +5,7 @@ type PlaylistDashboard struct { Slug string `json:"slug"` Title string `json:"title"` Uri string `json:"uri"` + Url string `json:"url"` Order int `json:"order"` } diff --git a/pkg/api/playlist_play.go b/pkg/api/playlist_play.go index e82c7b438b4..5ca136c32c4 100644 --- a/pkg/api/playlist_play.go +++ b/pkg/api/playlist_play.go @@ -26,6 +26,7 @@ func populateDashboardsByID(dashboardByIDs []int64, dashboardIDOrder map[int64]i Slug: item.Slug, Title: item.Title, Uri: "db/" + item.Slug, + Url: m.GetDashboardUrl(item.Uid, item.Slug), Order: dashboardIDOrder[item.Id], }) } diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 00f89920727..374fd6dcd36 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -28,6 +28,13 @@ export interface Props { export class DashNav extends PureComponent { timePickerEl: HTMLElement; timepickerCmp: AngularComponent; + playlistSrv: PlaylistSrv; + + constructor(props: Props) { + super(props); + + this.playlistSrv = this.props.$injector.get('playlistSrv'); + } componentDidMount() { const loader = getAngularLoader(); @@ -95,7 +102,7 @@ export class DashNav extends PureComponent { }; onStarDashboard = () => { - const { $injector, dashboard } = this.props; + const { dashboard, $injector } = this.props; const dashboardSrv = $injector.get('dashboardSrv'); dashboardSrv.starDashboard(dashboard.id, dashboard.meta.isStarred).then(newState => { @@ -104,6 +111,19 @@ export class DashNav extends PureComponent { }); }; + onPlaylistPrev = () => { + this.playlistSrv.prev(); + }; + + onPlaylistNext = () => { + this.playlistSrv.next(); + }; + + onPlaylistStop = () => { + this.playlistSrv.stop(); + this.forceUpdate(); + }; + onOpenShare = () => { const $rootScope = this.props.$injector.get('$rootScope'); const modalScope = $rootScope.$new(); @@ -117,13 +137,12 @@ export class DashNav extends PureComponent { }; render() { - const { dashboard, isFullscreen, editview, $injector } = this.props; + const { dashboard, isFullscreen, editview } = this.props; const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; const haveFolder = dashboard.meta.folderId > 0; const snapshotUrl = snapshot && snapshot.originalUrl; - const playlistSrv: PlaylistSrv = $injector.get('playlistSrv'); return (
@@ -138,25 +157,25 @@ export class DashNav extends PureComponent {
- {playlistSrv.isPlaying && ( + {this.playlistSrv.isPlaying && (
playlistSrv.prev()} + onClick={this.onPlaylistPrev} /> playlistSrv.stop()} + onClick={this.onPlaylistStop} /> playlistSrv.next()} + onClick={this.onPlaylistNext} />
)} diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index a7b3f51d92c..3705cf15dac 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -100,12 +100,6 @@ export class DashboardPage extends PureComponent { }, 10); } - // // when dashboard has loaded subscribe to somme events - // if (prevProps.dashboard === null) { - // // set initial fullscreen class state - // this.setPanelFullscreenClass(); - // } - // Sync url state with model if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) { // entering fullscreen/edit mode diff --git a/public/app/features/playlist/playlist_srv.ts b/public/app/features/playlist/playlist_srv.ts index 0a80ce0cdf0..6c1cf2b4256 100644 --- a/public/app/features/playlist/playlist_srv.ts +++ b/public/app/features/playlist/playlist_srv.ts @@ -1,12 +1,16 @@ -import coreModule from '../../core/core_module'; -import kbn from 'app/core/utils/kbn'; -import appEvents from 'app/core/app_events'; +// Libraries import _ from 'lodash'; + +// Utils import { toUrlParams } from 'app/core/utils/url'; +import coreModule from '../../core/core_module'; +import appEvents from 'app/core/app_events'; +import locationUtil from 'app/core/utils/location_util'; +import kbn from 'app/core/utils/kbn'; export class PlaylistSrv { private cancelPromise: any; - private dashboards: Array<{ uri: string }>; + private dashboards: Array<{ url: string }>; private index: number; private interval: number; private startUrl: string; @@ -36,7 +40,12 @@ export class PlaylistSrv { const queryParams = this.$location.search(); const filteredParams = _.pickBy(queryParams, value => value !== null); - this.$location.url('dashboard/' + dash.uri + '?' + toUrlParams(filteredParams)); + // this is done inside timeout to make sure digest happens after + // as this can be called from react + this.$timeout(() => { + const stripedUrl = locationUtil.stripBaseFromUrl(dash.url); + this.$location.url(stripedUrl + '?' + toUrlParams(filteredParams)); + }); this.index++; this.cancelPromise = this.$timeout(() => this.next(), this.interval); @@ -54,6 +63,8 @@ export class PlaylistSrv { this.index = 0; this.isPlaying = true; + appEvents.emit('playlist-started'); + return this.backendSrv.get(`/api/playlists/${playlistId}`).then(playlist => { return this.backendSrv.get(`/api/playlists/${playlistId}/dashboards`).then(dashboards => { this.dashboards = dashboards; @@ -77,6 +88,8 @@ export class PlaylistSrv { if (this.cancelPromise) { this.$timeout.cancel(this.cancelPromise); } + + appEvents.emit('playlist-stopped'); } } diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index f87fe69a684..07d99725113 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -120,12 +120,13 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop body.toggleClass('sidemenu-hidden'); }); - scope.$watch( - () => playlistSrv.isPlaying, - newValue => { - elem.toggleClass('view-mode--playlist', newValue === true); - } - ); + appEvents.on('playlist-started', () => { + elem.toggleClass('view-mode--playlist', true); + }); + + appEvents.on('playlist-stopped', () => { + elem.toggleClass('view-mode--playlist', false); + }); // check if we are in server side render if (document.cookie.indexOf('renderKey') !== -1) { @@ -258,10 +259,6 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop }, 100); } - if (target.parents('.navbar-buttons--playlist').length === 0) { - playlistSrv.stop(); - } - // hide search if (body.find('.search-container').length > 0) { if (target.parents('.search-results-container, .search-field-wrapper').length === 0) { From d29e1278dca3c7b07cf79e9c9d161569ba6b6460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Feb 2019 21:39:48 +0100 Subject: [PATCH 039/228] render after leaving fullscreen --- .../dashboard/containers/DashboardPage.tsx | 50 +++++++++++++------ 1 file changed, 34 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 3705cf15dac..404c953eecb 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -102,28 +102,46 @@ export class DashboardPage extends PureComponent { // Sync url state with model if (urlFullscreen !== dashboard.meta.fullscreen || urlEdit !== dashboard.meta.isEditing) { - // entering fullscreen/edit mode if (urlPanelId) { - const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); - - if (panel) { - dashboard.setViewMode(panel, urlFullscreen, urlEdit); - this.setState({ isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel }); - this.setPanelFullscreenClass(urlFullscreen); - } else { - this.handleFullscreenPanelNotFound(urlPanelId); - } + this.onEnterFullscreen(); } else { - // handle leaving fullscreen mode - if (this.state.fullscreenPanel) { - dashboard.setViewMode(this.state.fullscreenPanel, urlFullscreen, urlEdit); - } - this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }); - this.setPanelFullscreenClass(false); + this.onLeaveFullscreen(); } } } + onEnterFullscreen() { + const { dashboard, urlEdit, urlFullscreen, urlPanelId } = this.props; + + const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + + if (panel) { + dashboard.setViewMode(panel, urlFullscreen, urlEdit); + this.setState({ + isEditing: urlEdit, + isFullscreen: urlFullscreen, + fullscreenPanel: panel, + }); + this.setPanelFullscreenClass(urlFullscreen); + } else { + this.handleFullscreenPanelNotFound(urlPanelId); + } + } + + onLeaveFullscreen() { + const { dashboard } = this.props; + + if (this.state.fullscreenPanel) { + dashboard.setViewMode(this.state.fullscreenPanel, false, false); + } + + this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }, () => { + dashboard.render(); + }); + + this.setPanelFullscreenClass(false); + } + handleFullscreenPanelNotFound(urlPanelId: string) { // Panel not found this.props.notifyApp(createErrorNotification(`Panel with id ${urlPanelId} not found`)); From 7cd3cd6cd43cb0ef2597ce032935e4b2f38904db Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Sat, 2 Feb 2019 12:11:30 +0100 Subject: [PATCH 040/228] auth package refactoring moving middleware/hooks away from package exposing public struct UserToken accessible from other packages fix debug log lines so the same order and naming are used --- pkg/services/auth/auth.go | 6 + pkg/services/auth/auth_token.go | 279 ------------- pkg/services/auth/auth_token_test.go | 378 ----------------- pkg/services/auth/authtoken/auth_token.go | 225 ++++++++++ .../auth/authtoken/auth_token_test.go | 386 ++++++++++++++++++ pkg/services/auth/authtoken/model.go | 76 ++++ .../auth/{ => authtoken}/session_cleanup.go | 2 +- .../{ => authtoken}/session_cleanup_test.go | 2 +- pkg/services/auth/model.go | 25 -- 9 files changed, 695 insertions(+), 684 deletions(-) create mode 100644 pkg/services/auth/auth.go delete mode 100644 pkg/services/auth/auth_token.go delete mode 100644 pkg/services/auth/auth_token_test.go create mode 100644 pkg/services/auth/authtoken/auth_token.go create mode 100644 pkg/services/auth/authtoken/auth_token_test.go create mode 100644 pkg/services/auth/authtoken/model.go rename pkg/services/auth/{ => authtoken}/session_cleanup.go (98%) rename pkg/services/auth/{ => authtoken}/session_cleanup_test.go (98%) delete mode 100644 pkg/services/auth/model.go diff --git a/pkg/services/auth/auth.go b/pkg/services/auth/auth.go new file mode 100644 index 00000000000..31316f473f5 --- /dev/null +++ b/pkg/services/auth/auth.go @@ -0,0 +1,6 @@ +package auth + +type UserToken interface { + GetUserId() int64 + GetToken() string +} diff --git a/pkg/services/auth/auth_token.go b/pkg/services/auth/auth_token.go deleted file mode 100644 index 13b9ef607f5..00000000000 --- a/pkg/services/auth/auth_token.go +++ /dev/null @@ -1,279 +0,0 @@ -package auth - -import ( - "crypto/sha256" - "encoding/hex" - "errors" - "net/http" - "net/url" - "time" - - "github.com/grafana/grafana/pkg/bus" - "github.com/grafana/grafana/pkg/infra/serverlock" - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/sqlstore" - "github.com/grafana/grafana/pkg/setting" - "github.com/grafana/grafana/pkg/util" -) - -func init() { - registry.RegisterService(&UserAuthTokenServiceImpl{}) -} - -var ( - getTime = time.Now - UrgentRotateTime = 1 * time.Minute - oneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. -) - -// UserAuthTokenService are used for generating and validating user auth tokens -type UserAuthTokenService interface { - InitContextWithToken(ctx *models.ReqContext, orgID int64) bool - UserAuthenticatedHook(user *models.User, c *models.ReqContext) error - SignOutUser(c *models.ReqContext) error -} - -type UserAuthTokenServiceImpl struct { - SQLStore *sqlstore.SqlStore `inject:""` - ServerLockService *serverlock.ServerLockService `inject:""` - Cfg *setting.Cfg `inject:""` - log log.Logger -} - -// Init this service -func (s *UserAuthTokenServiceImpl) Init() error { - s.log = log.New("auth") - return nil -} - -func (s *UserAuthTokenServiceImpl) InitContextWithToken(ctx *models.ReqContext, orgID int64) bool { - //auth User - unhashedToken := ctx.GetCookie(s.Cfg.LoginCookieName) - if unhashedToken == "" { - return false - } - - userToken, err := s.LookupToken(unhashedToken) - if err != nil { - ctx.Logger.Info("failed to look up user based on cookie", "error", err) - return false - } - - query := models.GetSignedInUserQuery{UserId: userToken.UserId, OrgId: orgID} - if err := bus.Dispatch(&query); err != nil { - ctx.Logger.Error("Failed to get user with id", "userId", userToken.UserId, "error", err) - return false - } - - ctx.SignedInUser = query.Result - ctx.IsSignedIn = true - - //rotate session token if needed. - rotated, err := s.RefreshToken(userToken, ctx.RemoteAddr(), ctx.Req.UserAgent()) - if err != nil { - ctx.Logger.Error("failed to rotate token", "error", err, "userId", userToken.UserId, "tokenId", userToken.Id) - return true - } - - if rotated { - s.writeSessionCookie(ctx, userToken.UnhashedToken, oneYearInSeconds) - } - - return true -} - -func (s *UserAuthTokenServiceImpl) writeSessionCookie(ctx *models.ReqContext, value string, maxAge int) { - if setting.Env == setting.DEV { - ctx.Logger.Debug("new token", "unhashed token", value) - } - - ctx.Resp.Header().Del("Set-Cookie") - cookie := http.Cookie{ - Name: s.Cfg.LoginCookieName, - Value: url.QueryEscape(value), - HttpOnly: true, - Path: setting.AppSubUrl + "/", - Secure: s.Cfg.SecurityHTTPSCookies, - MaxAge: maxAge, - SameSite: s.Cfg.LoginCookieSameSite, - } - - http.SetCookie(ctx.Resp, &cookie) -} - -func (s *UserAuthTokenServiceImpl) UserAuthenticatedHook(user *models.User, c *models.ReqContext) error { - userToken, err := s.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) - if err != nil { - return err - } - - s.writeSessionCookie(c, userToken.UnhashedToken, oneYearInSeconds) - return nil -} - -func (s *UserAuthTokenServiceImpl) SignOutUser(c *models.ReqContext) error { - unhashedToken := c.GetCookie(s.Cfg.LoginCookieName) - if unhashedToken == "" { - return errors.New("cannot logout without session token") - } - - hashedToken := hashToken(unhashedToken) - - sql := `DELETE FROM user_auth_token WHERE auth_token = ?` - _, err := s.SQLStore.NewSession().Exec(sql, hashedToken) - - s.writeSessionCookie(c, "", -1) - return err -} - -func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (*userAuthToken, error) { - clientIP = util.ParseIPAddress(clientIP) - token, err := util.RandomHex(16) - if err != nil { - return nil, err - } - - hashedToken := hashToken(token) - - now := getTime().Unix() - - userToken := userAuthToken{ - UserId: userId, - AuthToken: hashedToken, - PrevAuthToken: hashedToken, - ClientIp: clientIP, - UserAgent: userAgent, - RotatedAt: now, - CreatedAt: now, - UpdatedAt: now, - SeenAt: 0, - AuthTokenSeen: false, - } - _, err = s.SQLStore.NewSession().Insert(&userToken) - if err != nil { - return nil, err - } - - userToken.UnhashedToken = token - - return &userToken, nil -} - -func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (*userAuthToken, error) { - hashedToken := hashToken(unhashedToken) - if setting.Env == setting.DEV { - s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) - } - - expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix() - - var userToken userAuthToken - exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&userToken) - if err != nil { - return nil, err - } - - if !exists { - return nil, ErrAuthTokenNotFound - } - - if userToken.AuthToken != hashedToken && userToken.PrevAuthToken == hashedToken && userToken.AuthTokenSeen { - userTokenCopy := userToken - userTokenCopy.AuthTokenSeen = false - expireBefore := getTime().Add(-UrgentRotateTime).Unix() - affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", userTokenCopy.Id, userTokenCopy.PrevAuthToken, expireBefore).AllCols().Update(&userTokenCopy) - if err != nil { - return nil, err - } - - if affectedRows == 0 { - s.log.Debug("prev seen token unchanged", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } else { - s.log.Debug("prev seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } - } - - if !userToken.AuthTokenSeen && userToken.AuthToken == hashedToken { - userTokenCopy := userToken - userTokenCopy.AuthTokenSeen = true - userTokenCopy.SeenAt = getTime().Unix() - affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", userTokenCopy.Id, userTokenCopy.AuthToken).AllCols().Update(&userTokenCopy) - if err != nil { - return nil, err - } - - if affectedRows == 1 { - userToken = userTokenCopy - } - - if affectedRows == 0 { - s.log.Debug("seen wrong token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } else { - s.log.Debug("seen token", "userTokenId", userToken.Id, "userId", userToken.UserId, "authToken", userToken.AuthToken, "clientIP", userToken.ClientIp, "userAgent", userToken.UserAgent) - } - } - - userToken.UnhashedToken = unhashedToken - - return &userToken, nil -} - -func (s *UserAuthTokenServiceImpl) RefreshToken(token *userAuthToken, clientIP, userAgent string) (bool, error) { - if token == nil { - return false, nil - } - - now := getTime() - - needsRotation := false - rotatedAt := time.Unix(token.RotatedAt, 0) - if token.AuthTokenSeen { - needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute)) - } else { - needsRotation = rotatedAt.Before(now.Add(-UrgentRotateTime)) - } - - if !needsRotation { - return false, nil - } - - s.log.Debug("refresh token needs rotation?", "auth_token_seen", token.AuthTokenSeen, "rotated_at", rotatedAt, "token.Id", token.Id) - - clientIP = util.ParseIPAddress(clientIP) - newToken, _ := util.RandomHex(16) - hashedToken := hashToken(newToken) - - // very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly - sql := ` - UPDATE user_auth_token - SET - seen_at = 0, - user_agent = ?, - client_ip = ?, - prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end, - auth_token = ?, - auth_token_seen = ?, - rotated_at = ? - WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)` - - res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), token.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix()) - if err != nil { - return false, err - } - - affected, _ := res.RowsAffected() - s.log.Debug("rotated", "affected", affected, "auth_token_id", token.Id, "userId", token.UserId) - if affected > 0 { - token.UnhashedToken = newToken - return true, nil - } - - return false, nil -} - -func hashToken(token string) string { - hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) - return hex.EncodeToString(hashBytes[:]) -} diff --git a/pkg/services/auth/auth_token_test.go b/pkg/services/auth/auth_token_test.go deleted file mode 100644 index 312e53a3970..00000000000 --- a/pkg/services/auth/auth_token_test.go +++ /dev/null @@ -1,378 +0,0 @@ -package auth - -import ( - "fmt" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/setting" - macaron "gopkg.in/macaron.v1" - - "github.com/grafana/grafana/pkg/log" - "github.com/grafana/grafana/pkg/services/sqlstore" - . "github.com/smartystreets/goconvey/convey" -) - -func TestUserAuthToken(t *testing.T) { - Convey("Test user auth token", t, func() { - ctx := createTestContext(t) - userAuthTokenService := ctx.tokenService - userID := int64(10) - - t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC) - getTime = func() time.Time { - return t - } - - Convey("When creating token", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - So(token.AuthTokenSeen, ShouldBeFalse) - - Convey("When lookup unhashed token should return user auth token", func() { - LookupToken, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(LookupToken, ShouldNotBeNil) - So(LookupToken.UserId, ShouldEqual, userID) - So(LookupToken.AuthTokenSeen, ShouldBeTrue) - - storedAuthToken, err := ctx.getAuthTokenByID(LookupToken.Id) - So(err, ShouldBeNil) - So(storedAuthToken, ShouldNotBeNil) - So(storedAuthToken.AuthTokenSeen, ShouldBeTrue) - }) - - Convey("When lookup hashed token should return user auth token not found error", func() { - LookupToken, err := userAuthTokenService.LookupToken(token.AuthToken) - So(err, ShouldEqual, ErrAuthTokenNotFound) - So(LookupToken, ShouldBeNil) - }) - - Convey("signing out should delete token and cookie if present", func() { - httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: token.UnhashedToken}) - - ctx := &models.ReqContext{Context: &macaron.Context{ - Req: macaron.Request{Request: httpreq}, - Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), - }, - Logger: log.New("fakelogger"), - } - - err = userAuthTokenService.SignOutUser(ctx) - So(err, ShouldBeNil) - - // makes sure we tell the browser to overwrite the cookie - cookieHeader := fmt.Sprintf("%s=; Path=/; Max-Age=0; HttpOnly", userAuthTokenService.Cfg.LoginCookieName) - So(ctx.Resp.Header().Get("Set-Cookie"), ShouldEqual, cookieHeader) - }) - - Convey("signing out an none existing session should return an error", func() { - httpreq := &http.Request{Header: make(http.Header)} - httpreq.AddCookie(&http.Cookie{Name: userAuthTokenService.Cfg.LoginCookieName, Value: ""}) - - ctx := &models.ReqContext{Context: &macaron.Context{ - Req: macaron.Request{Request: httpreq}, - Resp: macaron.NewResponseWriter("POST", httptest.NewRecorder()), - }, - Logger: log.New("fakelogger"), - } - - err = userAuthTokenService.SignOutUser(ctx) - So(err, ShouldNotBeNil) - }) - }) - - Convey("expires correctly", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - _, err = userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - - getTime = func() time.Time { - return t.Add(time.Hour) - } - - refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - _, err = userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - - stillGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(stillGood, ShouldNotBeNil) - - getTime = func() time.Time { - return t.Add(24 * 7 * time.Hour) - } - notGood, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldEqual, ErrAuthTokenNotFound) - So(notGood, ShouldBeNil) - }) - - Convey("can properly rotate tokens", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - prevToken := token.AuthToken - unhashedPrev := token.UnhashedToken - - refreshed, err := userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeFalse) - - updated, err := ctx.markAuthTokenAsSeen(token.Id) - So(err, ShouldBeNil) - So(updated, ShouldBeTrue) - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - - getTime = func() time.Time { - return t.Add(time.Hour) - } - - refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - unhashedToken := token.UnhashedToken - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - token.UnhashedToken = unhashedToken - - So(token.RotatedAt, ShouldEqual, getTime().Unix()) - So(token.ClientIp, ShouldEqual, "192.168.10.12") - So(token.UserAgent, ShouldEqual, "a new user agent") - So(token.AuthTokenSeen, ShouldBeFalse) - So(token.SeenAt, ShouldEqual, 0) - So(token.PrevAuthToken, ShouldEqual, prevToken) - - // ability to auth using an old token - - lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - So(lookedUp.SeenAt, ShouldEqual, getTime().Unix()) - - lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.Id, ShouldEqual, token.Id) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(time.Hour + (2 * time.Minute)) - } - - lookedUp, err = userAuthTokenService.LookupToken(unhashedPrev) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - - lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeFalse) - - refreshed, err = userAuthTokenService.RefreshToken(token, "192.168.10.12:1234", "a new user agent") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - token, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - So(token.SeenAt, ShouldEqual, 0) - }) - - Convey("keeps prev token valid for 1 minute after it is confirmed", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - - getTime = func() time.Time { - return t.Add(10 * time.Minute) - } - - prevToken := token.UnhashedToken - refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(20 * time.Minute) - } - - current, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(current, ShouldNotBeNil) - - prev, err := userAuthTokenService.LookupToken(prevToken) - So(err, ShouldBeNil) - So(prev, ShouldNotBeNil) - }) - - Convey("will not mark token unseen when prev and current are the same", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - lookedUp, err := userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - - lookedUp, err = userAuthTokenService.LookupToken(token.UnhashedToken) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - - lookedUp, err = ctx.getAuthTokenByID(lookedUp.Id) - So(err, ShouldBeNil) - So(lookedUp, ShouldNotBeNil) - So(lookedUp.AuthTokenSeen, ShouldBeTrue) - }) - - Convey("Rotate token", func() { - token, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") - So(err, ShouldBeNil) - So(token, ShouldNotBeNil) - - prevToken := token.AuthToken - - Convey("Should rotate current token and previous token when auth token seen", func() { - updated, err := ctx.markAuthTokenAsSeen(token.Id) - So(err, ShouldBeNil) - So(updated, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(10 * time.Minute) - } - - refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - storedToken, err := ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(storedToken, ShouldNotBeNil) - So(storedToken.AuthTokenSeen, ShouldBeFalse) - So(storedToken.PrevAuthToken, ShouldEqual, prevToken) - So(storedToken.AuthToken, ShouldNotEqual, prevToken) - - prevToken = storedToken.AuthToken - - updated, err = ctx.markAuthTokenAsSeen(token.Id) - So(err, ShouldBeNil) - So(updated, ShouldBeTrue) - - getTime = func() time.Time { - return t.Add(20 * time.Minute) - } - - refreshed, err = userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - storedToken, err = ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(storedToken, ShouldNotBeNil) - So(storedToken.AuthTokenSeen, ShouldBeFalse) - So(storedToken.PrevAuthToken, ShouldEqual, prevToken) - So(storedToken.AuthToken, ShouldNotEqual, prevToken) - }) - - Convey("Should rotate current token, but keep previous token when auth token not seen", func() { - token.RotatedAt = getTime().Add(-2 * time.Minute).Unix() - - getTime = func() time.Time { - return t.Add(2 * time.Minute) - } - - refreshed, err := userAuthTokenService.RefreshToken(token, "1.1.1.1", "firefox") - So(err, ShouldBeNil) - So(refreshed, ShouldBeTrue) - - storedToken, err := ctx.getAuthTokenByID(token.Id) - So(err, ShouldBeNil) - So(storedToken, ShouldNotBeNil) - So(storedToken.AuthTokenSeen, ShouldBeFalse) - So(storedToken.PrevAuthToken, ShouldEqual, prevToken) - So(storedToken.AuthToken, ShouldNotEqual, prevToken) - }) - }) - - Reset(func() { - getTime = time.Now - }) - }) -} - -func createTestContext(t *testing.T) *testContext { - t.Helper() - - sqlstore := sqlstore.InitTestDB(t) - tokenService := &UserAuthTokenServiceImpl{ - SQLStore: sqlstore, - Cfg: &setting.Cfg{ - LoginCookieName: "grafana_session", - LoginCookieMaxDays: 7, - LoginDeleteExpiredTokensAfterDays: 30, - LoginCookieRotation: 10, - }, - log: log.New("test-logger"), - } - - UrgentRotateTime = time.Minute - - return &testContext{ - sqlstore: sqlstore, - tokenService: tokenService, - } -} - -type testContext struct { - sqlstore *sqlstore.SqlStore - tokenService *UserAuthTokenServiceImpl -} - -func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) { - sess := c.sqlstore.NewSession() - var t userAuthToken - found, err := sess.ID(id).Get(&t) - if err != nil || !found { - return nil, err - } - - return &t, nil -} - -func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) { - sess := c.sqlstore.NewSession() - res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id) - if err != nil { - return false, err - } - - rowsAffected, err := res.RowsAffected() - if err != nil { - return false, err - } - return rowsAffected == 1, nil -} diff --git a/pkg/services/auth/authtoken/auth_token.go b/pkg/services/auth/authtoken/auth_token.go new file mode 100644 index 00000000000..4e4bd375501 --- /dev/null +++ b/pkg/services/auth/authtoken/auth_token.go @@ -0,0 +1,225 @@ +package authtoken + +import ( + "crypto/sha256" + "encoding/hex" + "time" + + "github.com/grafana/grafana/pkg/services/auth" + + "github.com/grafana/grafana/pkg/infra/serverlock" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/registry" + "github.com/grafana/grafana/pkg/services/sqlstore" + "github.com/grafana/grafana/pkg/setting" + "github.com/grafana/grafana/pkg/util" +) + +func init() { + registry.Register(®istry.Descriptor{ + Name: "AuthTokenService", + Instance: &UserAuthTokenServiceImpl{}, + InitPriority: registry.Low, + }) +} + +var getTime = time.Now + +const urgentRotateTime = 1 * time.Minute + +type UserAuthTokenServiceImpl struct { + SQLStore *sqlstore.SqlStore `inject:""` + ServerLockService *serverlock.ServerLockService `inject:""` + Cfg *setting.Cfg `inject:""` + log log.Logger +} + +func (s *UserAuthTokenServiceImpl) Init() error { + s.log = log.New("auth") + return nil +} + +func (s *UserAuthTokenServiceImpl) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + clientIP = util.ParseIPAddress(clientIP) + token, err := util.RandomHex(16) + if err != nil { + return nil, err + } + + hashedToken := hashToken(token) + + now := getTime().Unix() + + userAuthToken := userAuthToken{ + UserId: userId, + AuthToken: hashedToken, + PrevAuthToken: hashedToken, + ClientIp: clientIP, + UserAgent: userAgent, + RotatedAt: now, + CreatedAt: now, + UpdatedAt: now, + SeenAt: 0, + AuthTokenSeen: false, + } + _, err = s.SQLStore.NewSession().Insert(&userAuthToken) + if err != nil { + return nil, err + } + + userAuthToken.UnhashedToken = token + + s.log.Debug("user auth token created", "tokenId", userAuthToken.Id, "userId", userAuthToken.UserId, "clientIP", userAuthToken.ClientIp, "userAgent", userAuthToken.UserAgent, "authToken", userAuthToken.AuthToken) + + return userAuthToken.toUserToken() +} + +func (s *UserAuthTokenServiceImpl) LookupToken(unhashedToken string) (auth.UserToken, error) { + hashedToken := hashToken(unhashedToken) + if setting.Env == setting.DEV { + s.log.Debug("looking up token", "unhashed", unhashedToken, "hashed", hashedToken) + } + + expireBefore := getTime().Add(time.Duration(-86400*s.Cfg.LoginCookieMaxDays) * time.Second).Unix() + + var model userAuthToken + exists, err := s.SQLStore.NewSession().Where("(auth_token = ? OR prev_auth_token = ?) AND created_at > ?", hashedToken, hashedToken, expireBefore).Get(&model) + if err != nil { + return nil, err + } + + if !exists { + return nil, ErrAuthTokenNotFound + } + + if model.AuthToken != hashedToken && model.PrevAuthToken == hashedToken && model.AuthTokenSeen { + modelCopy := model + modelCopy.AuthTokenSeen = false + expireBefore := getTime().Add(-urgentRotateTime).Unix() + affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND prev_auth_token = ? AND rotated_at < ?", modelCopy.Id, modelCopy.PrevAuthToken, expireBefore).AllCols().Update(&modelCopy) + if err != nil { + return nil, err + } + + if affectedRows == 0 { + s.log.Debug("prev seen token unchanged", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } else { + s.log.Debug("prev seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } + } + + if !model.AuthTokenSeen && model.AuthToken == hashedToken { + modelCopy := model + modelCopy.AuthTokenSeen = true + modelCopy.SeenAt = getTime().Unix() + affectedRows, err := s.SQLStore.NewSession().Where("id = ? AND auth_token = ?", modelCopy.Id, modelCopy.AuthToken).AllCols().Update(&modelCopy) + if err != nil { + return nil, err + } + + if affectedRows == 1 { + model = modelCopy + } + + if affectedRows == 0 { + s.log.Debug("seen wrong token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } else { + s.log.Debug("seen token", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent, "authToken", model.AuthToken) + } + } + + model.UnhashedToken = unhashedToken + return model.toUserToken() +} + +func (s *UserAuthTokenServiceImpl) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) { + if token == nil { + return false, nil + } + + model, err := extractModelFromToken(token) + if err != nil { + return false, err + } + + now := getTime() + + needsRotation := false + rotatedAt := time.Unix(model.RotatedAt, 0) + if model.AuthTokenSeen { + needsRotation = rotatedAt.Before(now.Add(-time.Duration(s.Cfg.LoginCookieRotation) * time.Minute)) + } else { + needsRotation = rotatedAt.Before(now.Add(-urgentRotateTime)) + } + + if !needsRotation { + return false, nil + } + + s.log.Debug("token needs rotation", "tokenId", model.Id, "authTokenSeen", model.AuthTokenSeen, "rotatedAt", rotatedAt) + + clientIP = util.ParseIPAddress(clientIP) + newToken, err := util.RandomHex(16) + if err != nil { + return false, err + } + hashedToken := hashToken(newToken) + + // very important that auth_token_seen is set after the prev_auth_token = case when ... for mysql to function correctly + sql := ` + UPDATE user_auth_token + SET + seen_at = 0, + user_agent = ?, + client_ip = ?, + prev_auth_token = case when auth_token_seen = ? then auth_token else prev_auth_token end, + auth_token = ?, + auth_token_seen = ?, + rotated_at = ? + WHERE id = ? AND (auth_token_seen = ? OR rotated_at < ?)` + + res, err := s.SQLStore.NewSession().Exec(sql, userAgent, clientIP, s.SQLStore.Dialect.BooleanStr(true), hashedToken, s.SQLStore.Dialect.BooleanStr(false), now.Unix(), model.Id, s.SQLStore.Dialect.BooleanStr(true), now.Add(-30*time.Second).Unix()) + if err != nil { + return false, err + } + + affected, _ := res.RowsAffected() + s.log.Debug("auth token rotated", "affected", affected, "auth_token_id", model.Id, "userId", model.UserId) + if affected > 0 { + model.UnhashedToken = newToken + return true, nil + } + + return false, nil +} + +func (s *UserAuthTokenServiceImpl) RevokeToken(token auth.UserToken) error { + if token == nil { + return ErrAuthTokenNotFound + } + + model, err := extractModelFromToken(token) + if err != nil { + return err + } + + rowsAffected, err := s.SQLStore.NewSession().Delete(model) + if err != nil { + return err + } + + if rowsAffected == 0 { + s.log.Debug("user auth token not found/revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent) + return ErrAuthTokenNotFound + } + + s.log.Debug("user auth token revoked", "tokenId", model.Id, "userId", model.UserId, "clientIP", model.ClientIp, "userAgent", model.UserAgent) + + return nil +} + +func hashToken(token string) string { + hashBytes := sha256.Sum256([]byte(token + setting.SecretKey)) + return hex.EncodeToString(hashBytes[:]) +} diff --git a/pkg/services/auth/authtoken/auth_token_test.go b/pkg/services/auth/authtoken/auth_token_test.go new file mode 100644 index 00000000000..7809e235f5c --- /dev/null +++ b/pkg/services/auth/authtoken/auth_token_test.go @@ -0,0 +1,386 @@ +package authtoken + +import ( + "testing" + "time" + + "github.com/grafana/grafana/pkg/setting" + + "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/sqlstore" + . "github.com/smartystreets/goconvey/convey" +) + +func TestUserAuthToken(t *testing.T) { + Convey("Test user auth token", t, func() { + ctx := createTestContext(t) + userAuthTokenService := ctx.tokenService + userID := int64(10) + + t := time.Date(2018, 12, 13, 13, 45, 0, 0, time.UTC) + getTime = func() time.Time { + return t + } + + Convey("When creating token", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + So(model.AuthTokenSeen, ShouldBeFalse) + + Convey("When lookup unhashed token should return user auth token", func() { + userToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.UserId, ShouldEqual, userID) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + + storedAuthToken, err := ctx.getAuthTokenByID(lookedUpModel.Id) + So(err, ShouldBeNil) + So(storedAuthToken, ShouldNotBeNil) + So(storedAuthToken.AuthTokenSeen, ShouldBeTrue) + }) + + Convey("When lookup hashed token should return user auth token not found error", func() { + userToken, err := userAuthTokenService.LookupToken(model.AuthToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(userToken, ShouldBeNil) + }) + + Convey("revoking existing token should delete token", func() { + err = userAuthTokenService.RevokeToken(userToken) + So(err, ShouldBeNil) + + model, err := ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(model, ShouldBeNil) + }) + + Convey("revoking nil token should return error", func() { + err = userAuthTokenService.RevokeToken(nil) + So(err, ShouldEqual, ErrAuthTokenNotFound) + }) + + Convey("revoking non-existing token should return error", func() { + model.Id = 1000 + nonExistingToken, err := model.toUserToken() + So(err, ShouldBeNil) + err = userAuthTokenService.RevokeToken(nonExistingToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + }) + }) + + Convey("expires correctly", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + _, err = userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + + userToken, err = model.toUserToken() + So(err, ShouldBeNil) + + getTime = func() time.Time { + return t.Add(time.Hour) + } + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + _, err = userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + + stillGood, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + So(stillGood, ShouldNotBeNil) + + getTime = func() time.Time { + return t.Add(24 * 7 * time.Hour) + } + notGood, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldEqual, ErrAuthTokenNotFound) + So(notGood, ShouldBeNil) + }) + + Convey("can properly rotate tokens", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + prevToken := model.AuthToken + unhashedPrev := model.UnhashedToken + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeFalse) + + updated, err := ctx.markAuthTokenAsSeen(model.Id) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + tok, err := model.toUserToken() + So(err, ShouldBeNil) + + getTime = func() time.Time { + return t.Add(time.Hour) + } + + rotated, err = userAuthTokenService.TryRotateToken(tok, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + unhashedToken := model.UnhashedToken + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + model.UnhashedToken = unhashedToken + + So(model.RotatedAt, ShouldEqual, getTime().Unix()) + So(model.ClientIp, ShouldEqual, "192.168.10.12") + So(model.UserAgent, ShouldEqual, "a new user agent") + So(model.AuthTokenSeen, ShouldBeFalse) + So(model.SeenAt, ShouldEqual, 0) + So(model.PrevAuthToken, ShouldEqual, prevToken) + + // ability to auth using an old token + + lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err := extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + So(lookedUpModel.SeenAt, ShouldEqual, getTime().Unix()) + + lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.Id, ShouldEqual, model.Id) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(time.Hour + (2 * time.Minute)) + } + + lookedUpUserToken, err = userAuthTokenService.LookupToken(unhashedPrev) + So(err, ShouldBeNil) + lookedUpModel, err = extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + + lookedUpModel, err = ctx.getAuthTokenByID(lookedUpModel.Id) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeFalse) + + rotated, err = userAuthTokenService.TryRotateToken(userToken, "192.168.10.12:1234", "a new user agent") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + model, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + So(model.SeenAt, ShouldEqual, 0) + }) + + Convey("keeps prev token valid for 1 minute after it is confirmed", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + So(lookedUpUserToken, ShouldNotBeNil) + + getTime = func() time.Time { + return t.Add(10 * time.Minute) + } + + prevToken := model.UnhashedToken + rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(20 * time.Minute) + } + + currentUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + So(currentUserToken, ShouldNotBeNil) + + prevUserToken, err := userAuthTokenService.LookupToken(prevToken) + So(err, ShouldBeNil) + So(prevUserToken, ShouldNotBeNil) + }) + + Convey("will not mark token unseen when prev and current are the same", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + lookedUpUserToken, err := userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err := extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + + lookedUpUserToken, err = userAuthTokenService.LookupToken(model.UnhashedToken) + So(err, ShouldBeNil) + lookedUpModel, err = extractModelFromToken(lookedUpUserToken) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + + lookedUpModel, err = ctx.getAuthTokenByID(lookedUpModel.Id) + So(err, ShouldBeNil) + So(lookedUpModel, ShouldNotBeNil) + So(lookedUpModel.AuthTokenSeen, ShouldBeTrue) + }) + + Convey("Rotate token", func() { + userToken, err := userAuthTokenService.CreateToken(userID, "192.168.10.11:1234", "some user agent") + So(err, ShouldBeNil) + model, err := extractModelFromToken(userToken) + So(err, ShouldBeNil) + So(model, ShouldNotBeNil) + + prevToken := model.AuthToken + + Convey("Should rotate current token and previous token when auth token seen", func() { + updated, err := ctx.markAuthTokenAsSeen(model.Id) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(10 * time.Minute) + } + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + storedToken, err := ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(storedToken, ShouldNotBeNil) + So(storedToken.AuthTokenSeen, ShouldBeFalse) + So(storedToken.PrevAuthToken, ShouldEqual, prevToken) + So(storedToken.AuthToken, ShouldNotEqual, prevToken) + + prevToken = storedToken.AuthToken + + updated, err = ctx.markAuthTokenAsSeen(model.Id) + So(err, ShouldBeNil) + So(updated, ShouldBeTrue) + + getTime = func() time.Time { + return t.Add(20 * time.Minute) + } + + rotated, err = userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + storedToken, err = ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(storedToken, ShouldNotBeNil) + So(storedToken.AuthTokenSeen, ShouldBeFalse) + So(storedToken.PrevAuthToken, ShouldEqual, prevToken) + So(storedToken.AuthToken, ShouldNotEqual, prevToken) + }) + + Convey("Should rotate current token, but keep previous token when auth token not seen", func() { + model.RotatedAt = getTime().Add(-2 * time.Minute).Unix() + + getTime = func() time.Time { + return t.Add(2 * time.Minute) + } + + rotated, err := userAuthTokenService.TryRotateToken(userToken, "1.1.1.1", "firefox") + So(err, ShouldBeNil) + So(rotated, ShouldBeTrue) + + storedToken, err := ctx.getAuthTokenByID(model.Id) + So(err, ShouldBeNil) + So(storedToken, ShouldNotBeNil) + So(storedToken.AuthTokenSeen, ShouldBeFalse) + So(storedToken.PrevAuthToken, ShouldEqual, prevToken) + So(storedToken.AuthToken, ShouldNotEqual, prevToken) + }) + }) + + Reset(func() { + getTime = time.Now + }) + }) +} + +func createTestContext(t *testing.T) *testContext { + t.Helper() + + sqlstore := sqlstore.InitTestDB(t) + tokenService := &UserAuthTokenServiceImpl{ + SQLStore: sqlstore, + Cfg: &setting.Cfg{ + LoginCookieName: "grafana_session", + LoginCookieMaxDays: 7, + LoginDeleteExpiredTokensAfterDays: 30, + LoginCookieRotation: 10, + }, + log: log.New("test-logger"), + } + + return &testContext{ + sqlstore: sqlstore, + tokenService: tokenService, + } +} + +type testContext struct { + sqlstore *sqlstore.SqlStore + tokenService *UserAuthTokenServiceImpl +} + +func (c *testContext) getAuthTokenByID(id int64) (*userAuthToken, error) { + sess := c.sqlstore.NewSession() + var t userAuthToken + found, err := sess.ID(id).Get(&t) + if err != nil || !found { + return nil, err + } + + return &t, nil +} + +func (c *testContext) markAuthTokenAsSeen(id int64) (bool, error) { + sess := c.sqlstore.NewSession() + res, err := sess.Exec("UPDATE user_auth_token SET auth_token_seen = ? WHERE id = ?", c.sqlstore.Dialect.BooleanStr(true), id) + if err != nil { + return false, err + } + + rowsAffected, err := res.RowsAffected() + if err != nil { + return false, err + } + return rowsAffected == 1, nil +} diff --git a/pkg/services/auth/authtoken/model.go b/pkg/services/auth/authtoken/model.go new file mode 100644 index 00000000000..8bd89c68b04 --- /dev/null +++ b/pkg/services/auth/authtoken/model.go @@ -0,0 +1,76 @@ +package authtoken + +import ( + "errors" + "fmt" + + "github.com/grafana/grafana/pkg/services/auth" +) + +// Typed errors +var ( + ErrAuthTokenNotFound = errors.New("user auth token not found") +) + +type userAuthToken struct { + Id int64 + UserId int64 + AuthToken string + PrevAuthToken string + UserAgent string + ClientIp string + AuthTokenSeen bool + SeenAt int64 + RotatedAt int64 + CreatedAt int64 + UpdatedAt int64 + UnhashedToken string `xorm:"-"` +} + +func (uat *userAuthToken) toUserToken() (auth.UserToken, error) { + if uat == nil { + return nil, fmt.Errorf("needs pointer to userAuthToken struct") + } + + return &userTokenImpl{ + userAuthToken: uat, + }, nil +} + +type userToken interface { + auth.UserToken + GetModel() *userAuthToken +} + +type userTokenImpl struct { + *userAuthToken +} + +func (ut *userTokenImpl) GetUserId() int64 { + return ut.UserId +} + +func (ut *userTokenImpl) GetToken() string { + return ut.UnhashedToken +} + +func (ut *userTokenImpl) GetModel() *userAuthToken { + return ut.userAuthToken +} + +func extractModelFromToken(token auth.UserToken) (*userAuthToken, error) { + ut, ok := token.(userToken) + if !ok { + return nil, fmt.Errorf("failed to cast token") + } + + return ut.GetModel(), nil +} + +// UserAuthTokenService are used for generating and validating user auth tokens +type UserAuthTokenService interface { + CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) + LookupToken(unhashedToken string) (auth.UserToken, error) + TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) + RevokeToken(token auth.UserToken) error +} diff --git a/pkg/services/auth/session_cleanup.go b/pkg/services/auth/authtoken/session_cleanup.go similarity index 98% rename from pkg/services/auth/session_cleanup.go rename to pkg/services/auth/authtoken/session_cleanup.go index 7e523181a7b..cd2b766d6c0 100644 --- a/pkg/services/auth/session_cleanup.go +++ b/pkg/services/auth/authtoken/session_cleanup.go @@ -1,4 +1,4 @@ -package auth +package authtoken import ( "context" diff --git a/pkg/services/auth/session_cleanup_test.go b/pkg/services/auth/authtoken/session_cleanup_test.go similarity index 98% rename from pkg/services/auth/session_cleanup_test.go rename to pkg/services/auth/authtoken/session_cleanup_test.go index eef2cd74d04..101a279c374 100644 --- a/pkg/services/auth/session_cleanup_test.go +++ b/pkg/services/auth/authtoken/session_cleanup_test.go @@ -1,4 +1,4 @@ -package auth +package authtoken import ( "fmt" diff --git a/pkg/services/auth/model.go b/pkg/services/auth/model.go deleted file mode 100644 index 7a0f49539f2..00000000000 --- a/pkg/services/auth/model.go +++ /dev/null @@ -1,25 +0,0 @@ -package auth - -import ( - "errors" -) - -// Typed errors -var ( - ErrAuthTokenNotFound = errors.New("User auth token not found") -) - -type userAuthToken struct { - Id int64 - UserId int64 - AuthToken string - PrevAuthToken string - UserAgent string - ClientIp string - AuthTokenSeen bool - SeenAt int64 - RotatedAt int64 - CreatedAt int64 - UpdatedAt int64 - UnhashedToken string `xorm:"-"` -} From d53e64a32c48fca9149d2e291313a6dc8b04bb53 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Mon, 4 Feb 2019 23:44:28 +0100 Subject: [PATCH 041/228] move auth token middleware/hooks to middleware package fix/adds auth token middleware tests --- pkg/api/common_test.go | 63 +++++++++-- pkg/api/http_server.go | 18 +-- pkg/api/login.go | 17 ++- pkg/middleware/middleware.go | 65 ++++++++++- pkg/middleware/middleware_test.go | 165 +++++++++++++++++++++++++--- pkg/middleware/org_redirect_test.go | 31 ++++-- pkg/middleware/quota_test.go | 16 ++- pkg/models/context.go | 2 + 8 files changed, 324 insertions(+), 53 deletions(-) diff --git a/pkg/api/common_test.go b/pkg/api/common_test.go index fe02c94e277..853a04b5c11 100644 --- a/pkg/api/common_test.go +++ b/pkg/api/common_test.go @@ -8,6 +8,7 @@ import ( "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "gopkg.in/macaron.v1" . "github.com/smartystreets/goconvey/convey" @@ -129,24 +130,70 @@ func setupScenarioContext(url string) *scenarioContext { return sc } +type fakeUserToken interface { + auth.UserToken + SetToken(token string) +} + +type userTokenImpl struct { + userId int64 + token string +} + +func (ut *userTokenImpl) GetUserId() int64 { + return ut.userId +} + +func (ut *userTokenImpl) GetToken() string { + return ut.token +} + +func (ut *userTokenImpl) SetToken(token string) { + ut.token = token +} + type fakeUserAuthTokenService struct { - initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool + createTokenProvider func(userId int64, clientIP, userAgent string) (auth.UserToken, error) + tryRotateTokenProvider func(token auth.UserToken, clientIP, userAgent string) (bool, error) + lookupTokenProvider func(unhashedToken string) (auth.UserToken, error) + revokeTokenProvider func(token auth.UserToken) error } func newFakeUserAuthTokenService() *fakeUserAuthTokenService { return &fakeUserAuthTokenService{ - initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool { - return false + createTokenProvider: func(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + tryRotateTokenProvider: func(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return false, nil + }, + lookupTokenProvider: func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + revokeTokenProvider: func(token auth.UserToken) error { + return nil }, } } -func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool { - return s.initContextWithTokenProvider(ctx, orgID) +func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return s.createTokenProvider(userId, clientIP, userAgent) } -func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error { - return nil +func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (auth.UserToken, error) { + return s.lookupTokenProvider(unhashedToken) } -func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return s.tryRotateTokenProvider(token, clientIP, userAgent) +} + +func (s *fakeUserAuthTokenService) RevokeToken(token auth.UserToken) error { + return s.revokeTokenProvider(token) +} diff --git a/pkg/api/http_server.go b/pkg/api/http_server.go index 7b7c1478a4c..a0a65d73244 100644 --- a/pkg/api/http_server.go +++ b/pkg/api/http_server.go @@ -21,7 +21,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/plugins" "github.com/grafana/grafana/pkg/registry" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtoken" "github.com/grafana/grafana/pkg/services/cache" "github.com/grafana/grafana/pkg/services/datasources" "github.com/grafana/grafana/pkg/services/hooks" @@ -48,14 +48,14 @@ type HTTPServer struct { streamManager *live.StreamManager httpSrv *http.Server - RouteRegister routing.RouteRegister `inject:""` - Bus bus.Bus `inject:""` - RenderService rendering.Service `inject:""` - Cfg *setting.Cfg `inject:""` - HooksService *hooks.HooksService `inject:""` - CacheService *cache.CacheService `inject:""` - DatasourceCache datasources.CacheService `inject:""` - AuthTokenService auth.UserAuthTokenService `inject:""` + RouteRegister routing.RouteRegister `inject:""` + Bus bus.Bus `inject:""` + RenderService rendering.Service `inject:""` + Cfg *setting.Cfg `inject:""` + HooksService *hooks.HooksService `inject:""` + CacheService *cache.CacheService `inject:""` + DatasourceCache datasources.CacheService `inject:""` + AuthTokenService authtoken.UserAuthTokenService `inject:""` } func (hs *HTTPServer) Init() error { diff --git a/pkg/api/login.go b/pkg/api/login.go index 49da147724e..d25e83d34e8 100644 --- a/pkg/api/login.go +++ b/pkg/api/login.go @@ -5,11 +5,14 @@ import ( "net/http" "net/url" + "github.com/grafana/grafana/pkg/services/auth/authtoken" + "github.com/grafana/grafana/pkg/api/dtos" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/log" "github.com/grafana/grafana/pkg/login" "github.com/grafana/grafana/pkg/metrics" + "github.com/grafana/grafana/pkg/middleware" m "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -126,17 +129,23 @@ func (hs *HTTPServer) LoginPost(c *m.ReqContext, cmd dtos.LoginCommand) Response func (hs *HTTPServer) loginUserWithUser(user *m.User, c *m.ReqContext) { if user == nil { - hs.log.Error("User login with nil user") + hs.log.Error("user login with nil user") } - err := hs.AuthTokenService.UserAuthenticatedHook(user, c) + userToken, err := hs.AuthTokenService.CreateToken(user.Id, c.RemoteAddr(), c.Req.UserAgent()) if err != nil { - hs.log.Error("User auth hook failed", "error", err) + hs.log.Error("failed to create auth token", "error", err) } + + middleware.WriteSessionCookie(c, userToken.GetToken(), middleware.OneYearInSeconds) } func (hs *HTTPServer) Logout(c *m.ReqContext) { - hs.AuthTokenService.SignOutUser(c) + if err := hs.AuthTokenService.RevokeToken(c.UserToken); err != nil && err != authtoken.ErrAuthTokenNotFound { + hs.log.Error("failed to revoke auth token", "error", err) + } + + middleware.WriteSessionCookie(c, "", -1) if setting.SignoutRedirectUrl != "" { c.Redirect(setting.SignoutRedirectUrl) diff --git a/pkg/middleware/middleware.go b/pkg/middleware/middleware.go index 3722ac3058f..6cf29340b82 100644 --- a/pkg/middleware/middleware.go +++ b/pkg/middleware/middleware.go @@ -1,13 +1,15 @@ package middleware import ( + "net/http" + "net/url" "strconv" "github.com/grafana/grafana/pkg/bus" "github.com/grafana/grafana/pkg/components/apikeygen" "github.com/grafana/grafana/pkg/log" m "github.com/grafana/grafana/pkg/models" - "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtoken" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -21,7 +23,7 @@ var ( ReqOrgAdmin = RoleAuth(m.ROLE_ADMIN) ) -func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler { +func GetContextHandler(ats authtoken.UserAuthTokenService) macaron.Handler { return func(c *macaron.Context) { ctx := &m.ReqContext{ Context: c, @@ -49,7 +51,7 @@ func GetContextHandler(ats auth.UserAuthTokenService) macaron.Handler { case initContextWithApiKey(ctx): case initContextWithBasicAuth(ctx, orgId): case initContextWithAuthProxy(ctx, orgId): - case ats.InitContextWithToken(ctx, orgId): + case initContextWithToken(ats, ctx, orgId): case initContextWithAnonymousUser(ctx): } @@ -166,6 +168,63 @@ func initContextWithBasicAuth(ctx *m.ReqContext, orgId int64) bool { return true } +const cookieName = "grafana_session" +const OneYearInSeconds = 31557600 //used as default maxage for session cookies. We validate/rotate them more often. + +func initContextWithToken(authTokenService authtoken.UserAuthTokenService, ctx *m.ReqContext, orgID int64) bool { + rawToken := ctx.GetCookie(cookieName) + if rawToken == "" { + return false + } + + token, err := authTokenService.LookupToken(rawToken) + if err != nil { + ctx.Logger.Error("failed to look up user based on cookie", "error", err) + return false + } + + query := m.GetSignedInUserQuery{UserId: token.GetUserId(), OrgId: orgID} + if err := bus.Dispatch(&query); err != nil { + ctx.Logger.Error("failed to get user with id", "userId", token.GetUserId(), "error", err) + return false + } + + ctx.SignedInUser = query.Result + ctx.IsSignedIn = true + ctx.UserToken = token + + rotated, err := authTokenService.TryRotateToken(token, ctx.RemoteAddr(), ctx.Req.UserAgent()) + if err != nil { + ctx.Logger.Error("failed to rotate token", "error", err) + return true + } + + if rotated { + WriteSessionCookie(ctx, token.GetToken(), OneYearInSeconds) + } + + return true +} + +func WriteSessionCookie(ctx *m.ReqContext, value string, maxAge int) { + if setting.Env == setting.DEV { + ctx.Logger.Info("new token", "unhashed token", value) + } + + ctx.Resp.Header().Del("Set-Cookie") + cookie := http.Cookie{ + Name: cookieName, + Value: url.QueryEscape(value), + HttpOnly: true, + Path: setting.AppSubUrl + "/", + Secure: false, // TODO: use setting SecurityHTTPSCookies + MaxAge: maxAge, + SameSite: http.SameSiteLaxMode, // TODO: use setting LoginCookieSameSite + } + + http.SetCookie(ctx.Resp, &cookie) +} + func AddDefaultResponseHeaders() macaron.Handler { return func(ctx *m.ReqContext) { if ctx.IsApiRequest() && ctx.Req.Method == "GET" { diff --git a/pkg/middleware/middleware_test.go b/pkg/middleware/middleware_test.go index 4679c449853..4e10ee39201 100644 --- a/pkg/middleware/middleware_test.go +++ b/pkg/middleware/middleware_test.go @@ -10,6 +10,8 @@ import ( msession "github.com/go-macaron/session" "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" + "github.com/grafana/grafana/pkg/services/auth/authtoken" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/util" @@ -146,17 +148,91 @@ func TestMiddlewareContext(t *testing.T) { }) }) - middlewareScenario("Auth token service", func(sc *scenarioContext) { - var wasCalled bool - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - wasCalled = true - return false + middlewareScenario("Non-expired auth token in cookie which not are being rotated", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: unhashedToken, + }, nil } sc.fakeReq("GET", "/").exec() - Convey("should call middleware", func() { - So(wasCalled, ShouldBeTrue) + Convey("should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 12) + So(sc.context.UserToken.GetUserId(), ShouldEqual, 12) + So(sc.context.UserToken.GetToken(), ShouldEqual, "token") + }) + + Convey("should not set cookie", func() { + So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, "") + }) + }) + + middlewareScenario("Non-expired auth token in cookie which are being rotated", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") + + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: unhashedToken, + }, nil + } + + sc.userAuthTokenService.tryRotateTokenProvider = func(userToken auth.UserToken, clientIP, userAgent string) (bool, error) { + userToken.(fakeUserToken).SetToken("rotated") + return true, nil + } + + expectedCookie := &http.Cookie{ + Name: cookieName, + Value: "rotated", + Path: setting.AppSubUrl + "/", + HttpOnly: true, + MaxAge: OneYearInSeconds, + SameSite: http.SameSiteLaxMode, + } + + sc.fakeReq("GET", "/").exec() + + Convey("should init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeTrue) + So(sc.context.UserId, ShouldEqual, 12) + So(sc.context.UserToken.GetUserId(), ShouldEqual, 12) + So(sc.context.UserToken.GetToken(), ShouldEqual, "rotated") + }) + + Convey("should set cookie", func() { + So(sc.resp.Header().Get("Set-Cookie"), ShouldEqual, expectedCookie.String()) + }) + }) + + middlewareScenario("Invalid/expired auth token in cookie", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return nil, authtoken.ErrAuthTokenNotFound + } + + sc.fakeReq("GET", "/").exec() + + Convey("should not init context with user info", func() { + So(sc.context.IsSignedIn, ShouldBeFalse) + So(sc.context.UserId, ShouldEqual, 0) + So(sc.context.UserToken, ShouldBeNil) }) }) @@ -508,6 +584,7 @@ type scenarioContext struct { resp *httptest.ResponseRecorder apiKey string authHeader string + tokenSessionCookie string respJson map[string]interface{} handlerFunc handlerFunc defaultHandler macaron.Handler @@ -522,6 +599,11 @@ func (sc *scenarioContext) withValidApiKey() *scenarioContext { return sc } +func (sc *scenarioContext) withTokenSessionCookie(unhashedToken string) *scenarioContext { + sc.tokenSessionCookie = unhashedToken + return sc +} + func (sc *scenarioContext) withAuthorizationHeader(authHeader string) *scenarioContext { sc.authHeader = authHeader return sc @@ -571,6 +653,13 @@ func (sc *scenarioContext) exec() { sc.req.Header.Add("Authorization", sc.authHeader) } + if sc.tokenSessionCookie != "" { + sc.req.AddCookie(&http.Cookie{ + Name: cookieName, + Value: sc.tokenSessionCookie, + }) + } + sc.m.ServeHTTP(sc.resp, sc.req) if sc.resp.Header().Get("Content-Type") == "application/json; charset=UTF-8" { @@ -582,24 +671,70 @@ func (sc *scenarioContext) exec() { type scenarioFunc func(c *scenarioContext) type handlerFunc func(c *m.ReqContext) +type fakeUserToken interface { + auth.UserToken + SetToken(token string) +} + +type userTokenImpl struct { + userId int64 + token string +} + +func (ut *userTokenImpl) GetUserId() int64 { + return ut.userId +} + +func (ut *userTokenImpl) GetToken() string { + return ut.token +} + +func (ut *userTokenImpl) SetToken(token string) { + ut.token = token +} + type fakeUserAuthTokenService struct { - initContextWithTokenProvider func(ctx *m.ReqContext, orgID int64) bool + createTokenProvider func(userId int64, clientIP, userAgent string) (auth.UserToken, error) + tryRotateTokenProvider func(token auth.UserToken, clientIP, userAgent string) (bool, error) + lookupTokenProvider func(unhashedToken string) (auth.UserToken, error) + revokeTokenProvider func(token auth.UserToken) error } func newFakeUserAuthTokenService() *fakeUserAuthTokenService { return &fakeUserAuthTokenService{ - initContextWithTokenProvider: func(ctx *m.ReqContext, orgID int64) bool { - return false + createTokenProvider: func(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + tryRotateTokenProvider: func(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return false, nil + }, + lookupTokenProvider: func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 0, + token: "", + }, nil + }, + revokeTokenProvider: func(token auth.UserToken) error { + return nil }, } } -func (s *fakeUserAuthTokenService) InitContextWithToken(ctx *m.ReqContext, orgID int64) bool { - return s.initContextWithTokenProvider(ctx, orgID) +func (s *fakeUserAuthTokenService) CreateToken(userId int64, clientIP, userAgent string) (auth.UserToken, error) { + return s.createTokenProvider(userId, clientIP, userAgent) } -func (s *fakeUserAuthTokenService) UserAuthenticatedHook(user *m.User, c *m.ReqContext) error { - return nil +func (s *fakeUserAuthTokenService) LookupToken(unhashedToken string) (auth.UserToken, error) { + return s.lookupTokenProvider(unhashedToken) } -func (s *fakeUserAuthTokenService) SignOutUser(c *m.ReqContext) error { return nil } +func (s *fakeUserAuthTokenService) TryRotateToken(token auth.UserToken, clientIP, userAgent string) (bool, error) { + return s.tryRotateTokenProvider(token, clientIP, userAgent) +} + +func (s *fakeUserAuthTokenService) RevokeToken(token auth.UserToken) error { + return s.revokeTokenProvider(token) +} diff --git a/pkg/middleware/org_redirect_test.go b/pkg/middleware/org_redirect_test.go index 46b8776fdcc..c7479b3e9bc 100644 --- a/pkg/middleware/org_redirect_test.go +++ b/pkg/middleware/org_redirect_test.go @@ -3,6 +3,8 @@ package middleware import ( "testing" + "github.com/grafana/grafana/pkg/services/auth" + "fmt" "github.com/grafana/grafana/pkg/bus" @@ -14,14 +16,21 @@ func TestOrgRedirectMiddleware(t *testing.T) { Convey("Can redirect to correct org", t, func() { middlewareScenario("when setting a correct org for the user", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { return nil }) - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12} - ctx.IsSignedIn = true - return true + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 1, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: "", + }, nil } sc.m.Get("/", sc.defaultHandler) @@ -33,21 +42,23 @@ func TestOrgRedirectMiddleware(t *testing.T) { }) middlewareScenario("when setting an invalid org for user", func(sc *scenarioContext) { + sc.withTokenSessionCookie("token") bus.AddHandler("test", func(query *m.SetUsingOrgCommand) error { return fmt.Errorf("") }) - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - ctx.SignedInUser = &m.SignedInUser{OrgId: 1, UserId: 12} - ctx.IsSignedIn = true - return true - } - bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { query.Result = &m.SignedInUser{OrgId: 1, UserId: 12} return nil }) + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: "", + }, nil + } + sc.m.Get("/", sc.defaultHandler) sc.fakeReq("GET", "/?orgId=3").exec() diff --git a/pkg/middleware/quota_test.go b/pkg/middleware/quota_test.go index 4f2203a5d3d..af22f41deba 100644 --- a/pkg/middleware/quota_test.go +++ b/pkg/middleware/quota_test.go @@ -5,6 +5,7 @@ import ( "github.com/grafana/grafana/pkg/bus" m "github.com/grafana/grafana/pkg/models" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" . "github.com/smartystreets/goconvey/convey" @@ -74,10 +75,17 @@ func TestMiddlewareQuota(t *testing.T) { }) middlewareScenario("with user logged in", func(sc *scenarioContext) { - sc.userAuthTokenService.initContextWithTokenProvider = func(ctx *m.ReqContext, orgId int64) bool { - ctx.SignedInUser = &m.SignedInUser{OrgId: 2, UserId: 12} - ctx.IsSignedIn = true - return true + sc.withTokenSessionCookie("token") + bus.AddHandler("test", func(query *m.GetSignedInUserQuery) error { + query.Result = &m.SignedInUser{OrgId: 2, UserId: 12} + return nil + }) + + sc.userAuthTokenService.lookupTokenProvider = func(unhashedToken string) (auth.UserToken, error) { + return &userTokenImpl{ + userId: 12, + token: "", + }, nil } bus.AddHandler("globalQuota", func(query *m.GetGlobalQuotaByTargetQuery) error { diff --git a/pkg/models/context.go b/pkg/models/context.go index df970451304..da63db63f45 100644 --- a/pkg/models/context.go +++ b/pkg/models/context.go @@ -4,6 +4,7 @@ import ( "strings" "github.com/grafana/grafana/pkg/log" + "github.com/grafana/grafana/pkg/services/auth" "github.com/grafana/grafana/pkg/services/session" "github.com/grafana/grafana/pkg/setting" "github.com/prometheus/client_golang/prometheus" @@ -13,6 +14,7 @@ import ( type ReqContext struct { *macaron.Context *SignedInUser + UserToken auth.UserToken // This should only be used by the auth_proxy Session session.SessionStore From a7a964ec19cec8f0848d14eee6109d3656f61701 Mon Sep 17 00:00:00 2001 From: SamuelToh Date: Sun, 27 Jan 2019 21:49:22 +1000 Subject: [PATCH 042/228] Added PATCH verb end point for annotation op Added new PATCH verb annotation endpoint Removed unwanted fmt Added test cases for PATCH verb annotation endpoint Fixed formatting issue Check arr len before proceeding Updated doc to include PATCH verb annotation endpt --- docs/sources/http_api/annotations.md | 27 +++++++++++- pkg/api/annotations.go | 59 ++++++++++++++++++++++++++ pkg/api/annotations_test.go | 62 ++++++++++++++++++++++++++++ pkg/api/api.go | 1 + pkg/api/dtos/annotations.go | 8 ++++ 5 files changed, 156 insertions(+), 1 deletion(-) diff --git a/docs/sources/http_api/annotations.md b/docs/sources/http_api/annotations.md index 6633714d77b..dee4ede0777 100644 --- a/docs/sources/http_api/annotations.md +++ b/docs/sources/http_api/annotations.md @@ -160,15 +160,18 @@ Content-Type: application/json } ``` -## Update Annotation +## Replace Annotation `PUT /api/annotations/:id` +Replaces the annotation that matches the specified id. + **Example Request**: ```json PUT /api/annotations/1141 HTTP/1.1 Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk Content-Type: application/json { @@ -180,6 +183,28 @@ Content-Type: application/json } ``` +## Update Annotation + +`PATCH /api/annotations/:id` + +Updates one or more properties of an annotation that matches the specified id. + +**Example Request**: + +```json +PATCH /api/annotations/1145 HTTP/1.1 +Accept: application/json +Authorization: Bearer eyJrIjoiT0tTcG1pUlY2RnVKZTFVaDFsNFZXdE9ZWmNrMkZYbk +Content-Type: application/json + +{ + "time":1507037197000, + "timeEnd":1507180807095, + "text":"New Annotation Description", + "tags":["tag6","tag7","tag8"] +} +``` + ## Delete Annotation By Id `DELETE /api/annotations/:id` diff --git a/pkg/api/annotations.go b/pkg/api/annotations.go index 242b5531f51..da9b55a1c16 100644 --- a/pkg/api/annotations.go +++ b/pkg/api/annotations.go @@ -210,6 +210,65 @@ func UpdateAnnotation(c *m.ReqContext, cmd dtos.UpdateAnnotationsCmd) Response { return Success("Annotation updated") } +func PatchAnnotation(c *m.ReqContext, cmd dtos.PatchAnnotationsCmd) Response { + annotationID := c.ParamsInt64(":annotationId") + + repo := annotations.GetRepository() + + if resp := canSave(c, repo, annotationID); resp != nil { + return resp + } + + items, err := repo.Find(&annotations.ItemQuery{AnnotationId: annotationID, OrgId: c.OrgId}) + + if err != nil || len(items) == 0 { + return Error(500, "Could not find annotation to update", err) + } + + existing := annotations.Item{ + OrgId: c.OrgId, + UserId: c.UserId, + Id: annotationID, + Epoch: items[0].Time, + Text: items[0].Text, + Tags: items[0].Tags, + RegionId: items[0].RegionId, + } + + if cmd.Tags != nil { + existing.Tags = cmd.Tags + } + + if cmd.Text != "" && cmd.Text != existing.Text { + existing.Text = cmd.Text + } + + if cmd.Time > 0 && cmd.Time != existing.Epoch { + existing.Epoch = cmd.Time + } + + if err := repo.Update(&existing); err != nil { + return Error(500, "Failed to update annotation", err) + } + + // Update region end time if provided + if existing.RegionId != 0 && cmd.TimeEnd > 0 { + itemRight := existing + itemRight.RegionId = existing.Id + itemRight.Epoch = cmd.TimeEnd + + // We don't know id of region right event, so set it to 0 and find then using query like + // ... WHERE region_id = AND id != ... + itemRight.Id = 0 + + if err := repo.Update(&itemRight); err != nil { + return Error(500, "Failed to update annotation for region end time", err) + } + } + + return Success("Annotation patched") +} + func DeleteAnnotations(c *m.ReqContext, cmd dtos.DeleteAnnotationsCmd) Response { repo := annotations.GetRepository() diff --git a/pkg/api/annotations_test.go b/pkg/api/annotations_test.go index 08f3018c694..ebdd867a031 100644 --- a/pkg/api/annotations_test.go +++ b/pkg/api/annotations_test.go @@ -27,6 +27,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) { IsRegion: false, } + patchCmd := dtos.PatchAnnotationsCmd{ + Time: 1000, + Text: "annotation text", + Tags: []string{"tag1", "tag2"}, + } + Convey("When user is an Org Viewer", func() { role := m.ROLE_VIEWER Convey("Should not be allowed to save an annotation", func() { @@ -40,6 +46,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 403) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -67,6 +78,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -100,6 +116,13 @@ func TestAnnotationsApiEndpoint(t *testing.T) { Id: 1, } + patchCmd := dtos.PatchAnnotationsCmd{ + Time: 8000, + Text: "annotation text 50", + Tags: []string{"foo", "bar"}, + Id: 1, + } + deleteCmd := dtos.DeleteAnnotationsCmd{ DashboardId: 1, PanelId: 1, @@ -136,6 +159,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 403) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 403) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -163,6 +191,11 @@ func TestAnnotationsApiEndpoint(t *testing.T) { So(sc.resp.Code, ShouldEqual, 200) }) + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + loggedInUserScenarioWithRole("When calling DELETE on", "DELETE", "/api/annotations/1", "/api/annotations/:annotationId", role, func(sc *scenarioContext) { sc.handlerFunc = DeleteAnnotationByID sc.fakeReqWithParams("DELETE", sc.url, map[string]string{}).exec() @@ -189,6 +222,12 @@ func TestAnnotationsApiEndpoint(t *testing.T) { sc.fakeReqWithParams("PUT", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) }) + + patchAnnotationScenario("When calling PATCH on", "/api/annotations/1", "/api/annotations/:annotationId", role, patchCmd, func(sc *scenarioContext) { + sc.fakeReqWithParams("PATCH", sc.url, map[string]string{}).exec() + So(sc.resp.Code, ShouldEqual, 200) + }) + deleteAnnotationsScenario("When calling POST on", "/api/annotations/mass-delete", "/api/annotations/mass-delete", role, deleteCmd, func(sc *scenarioContext) { sc.fakeReqWithParams("POST", sc.url, map[string]string{}).exec() So(sc.resp.Code, ShouldEqual, 200) @@ -264,6 +303,29 @@ func putAnnotationScenario(desc string, url string, routePattern string, role m. }) } +func patchAnnotationScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.PatchAnnotationsCmd, fn scenarioFunc) { + Convey(desc+" "+url, func() { + defer bus.ClearBusHandlers() + + sc := setupScenarioContext(url) + sc.defaultHandler = Wrap(func(c *m.ReqContext) Response { + sc.context = c + sc.context.UserId = TestUserID + sc.context.OrgId = TestOrgID + sc.context.OrgRole = role + + return PatchAnnotation(c, cmd) + }) + + fakeAnnoRepo = &fakeAnnotationsRepo{} + annotations.SetRepository(fakeAnnoRepo) + + sc.m.Patch(routePattern, sc.defaultHandler) + + fn(sc) + }) +} + func deleteAnnotationsScenario(desc string, url string, routePattern string, role m.RoleType, cmd dtos.DeleteAnnotationsCmd, fn scenarioFunc) { Convey(desc+" "+url, func() { defer bus.ClearBusHandlers() diff --git a/pkg/api/api.go b/pkg/api/api.go index 980706d8355..0685ef3814d 100644 --- a/pkg/api/api.go +++ b/pkg/api/api.go @@ -354,6 +354,7 @@ func (hs *HTTPServer) registerRoutes() { annotationsRoute.Post("/", bind(dtos.PostAnnotationsCmd{}), Wrap(PostAnnotation)) annotationsRoute.Delete("/:annotationId", Wrap(DeleteAnnotationByID)) annotationsRoute.Put("/:annotationId", bind(dtos.UpdateAnnotationsCmd{}), Wrap(UpdateAnnotation)) + annotationsRoute.Patch("/:annotationId", bind(dtos.PatchAnnotationsCmd{}), Wrap(PatchAnnotation)) annotationsRoute.Delete("/region/:regionId", Wrap(DeleteAnnotationRegion)) annotationsRoute.Post("/graphite", reqEditorRole, bind(dtos.PostGraphiteAnnotationsCmd{}), Wrap(PostGraphiteAnnotation)) }) diff --git a/pkg/api/dtos/annotations.go b/pkg/api/dtos/annotations.go index c917b0d9feb..b64329e56d1 100644 --- a/pkg/api/dtos/annotations.go +++ b/pkg/api/dtos/annotations.go @@ -22,6 +22,14 @@ type UpdateAnnotationsCmd struct { TimeEnd int64 `json:"timeEnd"` } +type PatchAnnotationsCmd struct { + Id int64 `json:"id"` + Time int64 `json:"time"` + Text string `json:"text"` + Tags []string `json:"tags"` + TimeEnd int64 `json:"timeEnd"` +} + type DeleteAnnotationsCmd struct { AlertId int64 `json:"alertId"` DashboardId int64 `json:"dashboardId"` From e4c92ae12433cefffc3b9855fc117def44a0ebb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 09:24:30 +0100 Subject: [PATCH 043/228] added comment to initDashboard --- public/app/features/dashboard/state/initDashboard.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 5419fcb41d7..14428bfa290 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -46,6 +46,15 @@ async function redirectToNewUrl(slug: string, dispatch: any, currentPath: string } } +/** + * This action (or saga) does everything needed to bootstrap a dashboard & dashboard model. + * First it handles the process of fetching the dashboard, correcting the url if required (causing redirects/url updates) + * + * This is used both for single dashboard & solo panel routes, home & new dashboard routes. + * + * Then it handles the initializing of the old angular services that the dashboard components & panels still depend on + * + */ export function initDashboard({ $injector, $scope, From 04f190c3e357476adb1fd95284b6392648be38bb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 11:11:17 +0100 Subject: [PATCH 044/228] Updated playlist test --- public/app/features/playlist/specs/playlist_srv.test.ts | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/public/app/features/playlist/specs/playlist_srv.test.ts b/public/app/features/playlist/specs/playlist_srv.test.ts index e6b7671c964..d2ff27e54e0 100644 --- a/public/app/features/playlist/specs/playlist_srv.test.ts +++ b/public/app/features/playlist/specs/playlist_srv.test.ts @@ -1,6 +1,6 @@ import { PlaylistSrv } from '../playlist_srv'; -const dashboards = [{ uri: 'dash1' }, { uri: 'dash2' }]; +const dashboards = [{ url: 'dash1' }, { url: 'dash2' }]; const createPlaylistSrv = (): [PlaylistSrv, { url: jest.MockInstance }] => { const mockBackendSrv = { @@ -50,13 +50,12 @@ const mockWindowLocation = (): [jest.MockInstance, () => void] => { describe('PlaylistSrv', () => { let srv: PlaylistSrv; - let mockLocationService: { url: jest.MockInstance }; let hrefMock: jest.MockInstance; let unmockLocation: () => void; const initialUrl = 'http://localhost/playlist'; beforeEach(() => { - [srv, mockLocationService] = createPlaylistSrv(); + [srv] = createPlaylistSrv(); [hrefMock, unmockLocation] = mockWindowLocation(); // This will be cached in the srv when start() is called @@ -71,7 +70,6 @@ describe('PlaylistSrv', () => { await srv.start(1); for (let i = 0; i < 6; i++) { - expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); srv.next(); } @@ -84,7 +82,6 @@ describe('PlaylistSrv', () => { // 1 complete loop for (let i = 0; i < 3; i++) { - expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); srv.next(); } @@ -93,7 +90,6 @@ describe('PlaylistSrv', () => { // Another 2 loops for (let i = 0; i < 4; i++) { - expect(mockLocationService.url).toHaveBeenLastCalledWith(`dashboard/${dashboards[i % 2].uri}?`); srv.next(); } From 4b5bfd3da54e8ee681da9c6d74750714b7a882ff Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 Feb 2019 14:01:06 +0300 Subject: [PATCH 045/228] azuremonitor: more autocomplete suggestions for built-in functions --- .../editor/KustoQueryField.tsx | 197 ++++++++---------- 1 file changed, 89 insertions(+), 108 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index 9b2df96fcdd..bbe34b8f46a 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -96,57 +96,51 @@ export default class KustoQueryField extends QueryField { const wrapperClasses = wrapperNode.classList; let typeaheadContext: string | null = null; + // Built-in functions if (wrapperClasses.contains('function-context')) { typeaheadContext = 'context-function'; - if (this.fields) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchFields(); - return; - } + suggestionGroups = this.getColumnSuggestions(); + + // where } else if (modelPrefix.match(/(where\s(\w+\b)?$)/i)) { typeaheadContext = 'context-where'; - const fullQuery = Plain.serialize(this.state.value); - const table = this.getTableFromContext(fullQuery); - if (table) { - suggestionGroups = this.getWhereSuggestions(table); - } else { - return; - } - } else if (modelPrefix.match(/(,\s*$)/)) { - typeaheadContext = 'context-multiple-fields'; - if (this.fields) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchFields(); - return; - } - } else if (modelPrefix.match(/(from\s$)/i)) { - typeaheadContext = 'context-from'; - if (this.events) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchEvents(); - return; - } - } else if (modelPrefix.match(/(^select\s\w*$)/i)) { - typeaheadContext = 'context-select'; - if (this.fields) { - suggestionGroups = this.getKeywordSuggestions(); - } else { - this._fetchFields(); - return; - } - } else if (modelPrefix.match(/from\s\S+\s\w*$/i)) { - prefix = ''; - typeaheadContext = 'context-since'; - suggestionGroups = this.getKeywordSuggestions(); - // } else if (modelPrefix.match(/\d+\s\w*$/)) { - // typeaheadContext = 'context-number'; - // suggestionGroups = this._getAfterNumberSuggestions(); - } else if (modelPrefix.match(/ago\b/i) || modelPrefix.match(/facet\b/i) || modelPrefix.match(/\$__timefilter\b/i)) { - typeaheadContext = 'context-timeseries'; - suggestionGroups = this.getKeywordSuggestions(); + suggestionGroups = this.getColumnSuggestions(); + + // summarize by + } else if (modelPrefix.match(/(summarize\s(\w+\b)?$)/i)) { + typeaheadContext = 'context-summarize'; + suggestionGroups = this.getFunctionSuggestions(); + } else if (modelPrefix.match(/(summarize\s(.+\s)?by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) { + typeaheadContext = 'context-summarize-by'; + suggestionGroups = this.getColumnSuggestions(); + + // order by, top X by, ... by ... + } else if (modelPrefix.match(/(by\s+([^,\s]+,\s*)*([^,\s]+\b)?$)/i)) { + typeaheadContext = 'context-by'; + suggestionGroups = this.getColumnSuggestions(); + + // join + } else if (modelPrefix.match(/(on\s(.+\b)?$)/i)) { + typeaheadContext = 'context-join-on'; + suggestionGroups = this.getColumnSuggestions(); + } else if (modelPrefix.match(/(join\s+(\(\s+)?(\w+\b)?$)/i)) { + typeaheadContext = 'context-join'; + suggestionGroups = this.getTableSuggestions(); + + // distinct + } else if (modelPrefix.match(/(distinct\s(.+\b)?$)/i)) { + typeaheadContext = 'context-distinct'; + suggestionGroups = this.getColumnSuggestions(); + + // database() + } else if (modelPrefix.match(/(database\(\"(\w+)\"\)\.(.+\b)?$)/i)) { + typeaheadContext = 'context-database-table'; + const db = this.getDBFromDatabaseFunction(modelPrefix); + console.log(db); + suggestionGroups = this.getTableSuggestions(db); + prefix = prefix.replace('.', ''); + + // built-in } else if (prefix && !wrapperClasses.contains('argument')) { if (modelPrefix.match(/\s$/i)) { prefix = ''; @@ -156,7 +150,7 @@ export default class KustoQueryField extends QueryField { } else if (Plain.serialize(this.state.value) === '') { typeaheadContext = 'context-new'; if (this.schema) { - suggestionGroups = this._getInitialSuggestions(); + suggestionGroups = this.getInitialSuggestions(); } else { this.fetchSchema(); setTimeout(this.onTypeahead, 0); @@ -187,7 +181,7 @@ export default class KustoQueryField extends QueryField { .filter(group => group.items.length > 0); // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); - // console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); + console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); this.setState({ typeaheadPrefix: prefix, @@ -293,6 +287,10 @@ export default class KustoQueryField extends QueryField { // ]; // } + private getInitialSuggestions(): SuggestionGroup[] { + return this.getTableSuggestions(); + } + private getKeywordSuggestions(): SuggestionGroup[] { return [ { @@ -323,50 +321,28 @@ export default class KustoQueryField extends QueryField { ]; } - private _getInitialSuggestions(): SuggestionGroup[] { + private getFunctionSuggestions(): SuggestionGroup[] { return [ { prefixMatch: true, - label: 'Tables', - items: _.map(this.schema.Databases.Default.Tables, (t: any) => ({ text: t.Name })) + label: 'Functions', + items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) + }, + { + prefixMatch: true, + label: 'Macros', + items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) } ]; - - // return [ - // { - // prefixMatch: true, - // label: 'Keywords', - // items: KEYWORDS.map(wrapText) - // }, - // { - // prefixMatch: true, - // label: 'Operators', - // items: operatorTokens.map((s: any) => { s.type = 'function'; return s; }) - // }, - // { - // prefixMatch: true, - // label: 'Functions', - // items: functionTokens.map((s: any) => { s.type = 'function'; return s; }) - // }, - // { - // prefixMatch: true, - // label: 'Macros', - // items: grafanaMacros.map((s: any) => { s.type = 'function'; return s; }) - // } - // ]; } - private getWhereSuggestions(table: string): SuggestionGroup[] { - const tableSchema = this.schema.Databases.Default.Tables[table]; - if (tableSchema) { + getTableSuggestions(db = 'Default'): SuggestionGroup[] { + if (this.schema.Databases[db]) { return [ { prefixMatch: true, - label: 'Fields', - items: _.map(tableSchema.OrderedColumns, (f: any) => ({ - text: f.Name, - hint: f.Type - })) + label: 'Tables', + items: _.map(this.schema.Databases[db].Tables, (t: any) => ({ text: t.Name })) } ]; } else { @@ -374,7 +350,28 @@ export default class KustoQueryField extends QueryField { } } - private getTableFromContext(query: string) { + private getColumnSuggestions(): SuggestionGroup[] { + const table = this.getTableFromContext(); + if (table) { + const tableSchema = this.schema.Databases.Default.Tables[table]; + if (tableSchema) { + return [ + { + prefixMatch: true, + label: 'Fields', + items: _.map(tableSchema.OrderedColumns, (f: any) => ({ + text: f.Name, + hint: f.Type + })) + } + ]; + } + } + return []; + } + + private getTableFromContext() { + const query = Plain.serialize(this.state.value); const tablePattern = /^\s*(\w+)\s*|/g; const normalizedQuery = normalizeQuery(query); const match = tablePattern.exec(normalizedQuery); @@ -385,30 +382,14 @@ export default class KustoQueryField extends QueryField { } } - private async _fetchEvents() { - // const query = 'events'; - // const result = await this.request(query); - - // if (result === undefined) { - // this.events = []; - // } else { - // this.events = result; - // } - // setTimeout(this.onTypeahead, 0); - - //Stub - this.events = []; - } - - private async _fetchFields() { - // const query = 'fields'; - // const result = await this.request(query); - - // this.fields = result || []; - - // setTimeout(this.onTypeahead, 0); - // Stub - this.fields = []; + private getDBFromDatabaseFunction(prefix: string) { + const databasePattern = /database\(\"(\w+)\"\)/gi; + const match = databasePattern.exec(prefix); + if (match && match.length > 1 && match[0] && match[1]) { + return match[1]; + } else { + return null; + } } private async fetchSchema() { From 0642c5269315cfa2acbb61648daf2d1de20004e6 Mon Sep 17 00:00:00 2001 From: ijin08 Date: Tue, 5 Feb 2019 12:05:02 +0100 Subject: [PATCH 046/228] created new color variables, changed primary to blue, changed success-btns to primary-btns. --- .../ColorPicker/SeriesColorPickerPopover.tsx | 4 +- .../PanelOptionsGroup/_PanelOptionsGroup.scss | 4 +- .../ThresholdsEditor/_ThresholdsEditor.scss | 2 +- .../components/EmptyListCTA/EmptyListCTA.tsx | 2 +- .../__snapshots__/EmptyListCTA.test.tsx.snap | 2 +- .../components/OrgActionBar/OrgActionBar.tsx | 2 +- .../__snapshots__/OrgActionBar.test.tsx.snap | 2 +- .../PermissionList/AddPermission.tsx | 2 +- .../SharedPreferences/SharedPreferences.tsx | 2 +- .../manage_dashboards/manage_dashboards.html | 6 +- .../app/features/admin/partials/edit_org.html | 2 +- .../features/admin/partials/edit_user.html | 8 +- .../app/features/admin/partials/new_user.html | 2 +- public/app/features/admin/partials/orgs.html | 2 +- public/app/features/admin/partials/users.html | 2 +- .../alerting/partials/notification_edit.html | 2 +- .../alerting/partials/notifications_list.html | 2 +- .../features/annotations/partials/editor.html | 8 +- .../annotations/partials/event_editor.html | 2 +- public/app/features/api-keys/ApiKeysPage.tsx | 4 +- .../__snapshots__/ApiKeysPage.test.tsx.snap | 2 +- .../AddPanelWidget/AddPanelWidget.tsx | 2 +- .../components/DashExportModal/template.html | 2 +- .../components/DashLinks/editor.html | 8 +- .../DashboardPermissions.tsx | 2 +- .../DashboardSettings/template.html | 6 +- .../components/ExportDataModal/template.html | 2 +- .../components/RowOptions/template.html | 2 +- .../SaveModals/SaveDashboardAsModalCtrl.ts | 2 +- .../SaveModals/SaveDashboardModalCtrl.ts | 4 +- .../SaveProvisionedDashboardModalCtrl.ts | 2 +- .../components/ShareModal/template.html | 2 +- .../UnsavedChangesModalCtrl.ts | 2 +- .../components/VersionHistory/template.html | 2 +- .../datasources/settings/ButtonRow.tsx | 2 +- .../__snapshots__/ButtonRow.test.tsx.snap | 4 +- .../features/folders/FolderPermissions.tsx | 2 +- .../features/folders/FolderSettingsPage.tsx | 2 +- .../FolderSettingsPage.test.tsx.snap | 4 +- .../folders/partials/create_folder.html | 2 +- .../MoveToFolderModal/template.html | 2 +- .../uploadDashboardDirective.ts | 2 +- .../partials/dashboard_import.html | 2 +- public/app/features/org/OrgProfile.tsx | 2 +- .../__snapshots__/OrgProfile.test.tsx.snap | 2 +- public/app/features/org/partials/invite.html | 2 +- public/app/features/org/partials/newOrg.html | 2 +- .../app/features/org/partials/select_org.html | 2 +- .../features/playlist/partials/playlist.html | 4 +- .../features/playlist/partials/playlists.html | 2 +- .../plugins/partials/plugin_edit.html | 4 +- .../profile/partials/change_password.html | 2 +- .../features/profile/partials/profile.html | 2 +- public/app/features/teams/TeamGroupSync.tsx | 6 +- public/app/features/teams/TeamList.tsx | 2 +- public/app/features/teams/TeamMembers.tsx | 4 +- public/app/features/teams/TeamSettings.tsx | 2 +- .../__snapshots__/TeamGroupSync.test.tsx.snap | 8 +- .../__snapshots__/TeamList.test.tsx.snap | 2 +- .../__snapshots__/TeamMembers.test.tsx.snap | 6 +- .../__snapshots__/TeamSettings.test.tsx.snap | 2 +- .../features/teams/partials/create_team.html | 2 +- .../features/templating/partials/editor.html | 8 +- public/app/features/users/UsersActionBar.tsx | 4 +- .../UsersActionBar.test.tsx.snap | 4 +- public/app/partials/confirm_modal.html | 2 +- public/app/partials/edit_json.html | 2 +- public/app/partials/login.html | 2 +- public/app/partials/reset_password.html | 6 +- public/app/partials/signup_invited.html | 2 +- public/app/partials/signup_step2.html | 2 +- public/sass/_variables.dark.scss | 87 +++++++------- public/sass/_variables.light.scss | 110 +++++++++--------- public/sass/base/_type.scss | 4 +- public/sass/components/_buttons.scss | 34 +----- public/sass/components/_navbar.scss | 2 +- .../components/_panel_gettingstarted.scss | 2 +- public/vendor/angular-ui/ui-bootstrap-tpls.js | 2 +- 78 files changed, 212 insertions(+), 243 deletions(-) diff --git a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx index 3fa7a1f4a45..75727f18dcb 100644 --- a/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx +++ b/packages/grafana-ui/src/components/ColorPicker/SeriesColorPickerPopover.tsx @@ -69,8 +69,8 @@ export class AxisSelector extends React.PureComponent diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss index b5b815cf57c..ddcb8971275 100644 --- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -29,14 +29,14 @@ &:hover { .panel-options-group__add-circle { - background-color: $btn-success-bg; + background-color: $btn-primary-bg; color: $text-color-strong; } } } .panel-options-group__add-circle { - @include gradientBar($btn-success-bg, $btn-success-bg-hl, $text-color); + @include gradientBar($btn-primary-bg, $btn-primary-bg-hl, #fff); border-radius: 50px; width: 20px; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index 200adfbfd75..923244af781 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -21,7 +21,7 @@ } .thresholds-row-add-button { - @include buttonBackground($btn-success-bg, $btn-success-bg-hl, $text-color); + @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl, #fff); align-self: center; margin-right: 5px; diff --git a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx index d63af72ae4d..6b5c6ebb7ca 100644 --- a/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx +++ b/public/app/core/components/EmptyListCTA/EmptyListCTA.tsx @@ -20,7 +20,7 @@ class EmptyListCTA extends Component { return (
{title}
- + {buttonTitle} diff --git a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap index b85660bcc6f..21c2ed294b4 100644 --- a/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap +++ b/public/app/core/components/EmptyListCTA/__snapshots__/EmptyListCTA.test.tsx.snap @@ -10,7 +10,7 @@ exports[`EmptyListCTA renders correctly 1`] = ` Title
diff --git a/public/app/core/components/OrgActionBar/OrgActionBar.tsx b/public/app/core/components/OrgActionBar/OrgActionBar.tsx index 8fc34a018e1..b6b2046736f 100644 --- a/public/app/core/components/OrgActionBar/OrgActionBar.tsx +++ b/public/app/core/components/OrgActionBar/OrgActionBar.tsx @@ -35,7 +35,7 @@ export default class OrgActionBar extends PureComponent { onSetLayoutMode(mode)} />
diff --git a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap index dc53e7863ea..25de037930a 100644 --- a/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap +++ b/public/app/core/components/OrgActionBar/__snapshots__/OrgActionBar.test.tsx.snap @@ -29,7 +29,7 @@ exports[`Render should render component 1`] = ` className="page-action-bar__spacer" /> diff --git a/public/app/core/components/PermissionList/AddPermission.tsx b/public/app/core/components/PermissionList/AddPermission.tsx index 30219371257..80afcedf873 100644 --- a/public/app/core/components/PermissionList/AddPermission.tsx +++ b/public/app/core/components/PermissionList/AddPermission.tsx @@ -130,7 +130,7 @@ class AddPermissions extends Component {
-
diff --git a/public/app/core/components/SharedPreferences/SharedPreferences.tsx b/public/app/core/components/SharedPreferences/SharedPreferences.tsx index 33aca1de2aa..171e0e8109e 100644 --- a/public/app/core/components/SharedPreferences/SharedPreferences.tsx +++ b/public/app/core/components/SharedPreferences/SharedPreferences.tsx @@ -126,7 +126,7 @@ export class SharedPreferences extends PureComponent { />
-
diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 6fbd65afaf5..6036ead3ef1 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -5,15 +5,15 @@
-
+ Dashboard - + Folder - + Import diff --git a/public/app/features/admin/partials/edit_org.html b/public/app/features/admin/partials/edit_org.html index 975d663e9b0..911181ef999 100644 --- a/public/app/features/admin/partials/edit_org.html +++ b/public/app/features/admin/partials/edit_org.html @@ -10,7 +10,7 @@
- +
diff --git a/public/app/features/admin/partials/edit_user.html b/public/app/features/admin/partials/edit_user.html index 5b0efa8bdf3..7e6457a8a76 100644 --- a/public/app/features/admin/partials/edit_user.html +++ b/public/app/features/admin/partials/edit_user.html @@ -21,7 +21,7 @@
- +
@@ -34,7 +34,7 @@
- +
@@ -46,7 +46,7 @@
- +
@@ -65,7 +65,7 @@
- +
diff --git a/public/app/features/admin/partials/new_user.html b/public/app/features/admin/partials/new_user.html index 5199d957c33..e3374d080ca 100644 --- a/public/app/features/admin/partials/new_user.html +++ b/public/app/features/admin/partials/new_user.html @@ -24,7 +24,7 @@
- +
diff --git a/public/app/features/admin/partials/orgs.html b/public/app/features/admin/partials/orgs.html index d28cf4dc967..b40aed6faab 100644 --- a/public/app/features/admin/partials/orgs.html +++ b/public/app/features/admin/partials/orgs.html @@ -3,7 +3,7 @@
- + New Org diff --git a/public/app/features/admin/partials/users.html b/public/app/features/admin/partials/users.html index 806c10851e5..08704dc0459 100644 --- a/public/app/features/admin/partials/users.html +++ b/public/app/features/admin/partials/users.html @@ -7,7 +7,7 @@
- + Add new user diff --git a/public/app/features/alerting/partials/notification_edit.html b/public/app/features/alerting/partials/notification_edit.html index b2cd2f21e4d..5e7201cdfdd 100644 --- a/public/app/features/alerting/partials/notification_edit.html +++ b/public/app/features/alerting/partials/notification_edit.html @@ -68,7 +68,7 @@
- + Back
diff --git a/public/app/features/alerting/partials/notifications_list.html b/public/app/features/alerting/partials/notifications_list.html index 246cb45b4db..6624a1d1132 100644 --- a/public/app/features/alerting/partials/notifications_list.html +++ b/public/app/features/alerting/partials/notifications_list.html @@ -7,7 +7,7 @@
- + New Channel diff --git a/public/app/features/annotations/partials/editor.html b/public/app/features/annotations/partials/editor.html index 65ee7e52bd0..9a7a8cb738a 100644 --- a/public/app/features/annotations/partials/editor.html +++ b/public/app/features/annotations/partials/editor.html @@ -9,7 +9,7 @@
@@ -48,7 +48,7 @@
There are no custom annotation queries added yet
- + Add Annotation Query @@ -105,8 +105,8 @@
- - + +
diff --git a/public/app/features/annotations/partials/event_editor.html b/public/app/features/annotations/partials/event_editor.html index 529434755f1..286decb34ce 100644 --- a/public/app/features/annotations/partials/event_editor.html +++ b/public/app/features/annotations/partials/event_editor.html @@ -26,7 +26,7 @@
- + Cancel
diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 41b9b0c8a55..21d1ca54a66 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -169,7 +169,7 @@ export class ApiKeysPage extends PureComponent {
- +
@@ -199,7 +199,7 @@ export class ApiKeysPage extends PureComponent {
-
diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index f40894426ae..03f11f79cc3 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -137,7 +137,7 @@ exports[`Render should render CTA if there are no API keys 1`] = ` className="gf-form" > diff --git a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx index 8c1ab93cec1..dbd2fb1ffeb 100644 --- a/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx +++ b/public/app/features/dashboard/components/AddPanelWidget/AddPanelWidget.tsx @@ -142,7 +142,7 @@ export class AddPanelWidget extends React.Component {
- {addCopyButton} diff --git a/public/app/features/dashboard/components/DashExportModal/template.html b/public/app/features/dashboard/components/DashExportModal/template.html index 3c14c4b184d..e399d166d04 100644 --- a/public/app/features/dashboard/components/DashExportModal/template.html +++ b/public/app/features/dashboard/components/DashExportModal/template.html @@ -12,7 +12,7 @@
-
@@ -126,10 +126,10 @@ - - diff --git a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx index ce6a866ce57..8cc26c4a1f2 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx @@ -76,7 +76,7 @@ export class DashboardPermissions extends PureComponent {
-
diff --git a/public/app/features/dashboard/components/DashboardSettings/template.html b/public/app/features/dashboard/components/DashboardSettings/template.html index 97002f7bf92..99edc035bd5 100644 --- a/public/app/features/dashboard/components/DashboardSettings/template.html +++ b/public/app/features/dashboard/components/DashboardSettings/template.html @@ -10,7 +10,7 @@
-
-
@@ -128,7 +128,7 @@

Make Editable

-
diff --git a/public/app/features/dashboard/components/ExportDataModal/template.html b/public/app/features/dashboard/components/ExportDataModal/template.html index 8b766889c33..f59bd629e03 100644 --- a/public/app/features/dashboard/components/ExportDataModal/template.html +++ b/public/app/features/dashboard/components/ExportDataModal/template.html @@ -29,7 +29,7 @@ diff --git a/public/app/features/dashboard/components/RowOptions/template.html b/public/app/features/dashboard/components/RowOptions/template.html index 3d5c6116679..13e00b631ed 100644 --- a/public/app/features/dashboard/components/RowOptions/template.html +++ b/public/app/features/dashboard/components/RowOptions/template.html @@ -22,7 +22,7 @@
- +
diff --git a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts index 6a470785fdb..60fa031f71c 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardAsModalCtrl.ts @@ -32,7 +32,7 @@ const template = `
- + Cancel
diff --git a/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts index 88fba13f711..ed187befb95 100644 --- a/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts +++ b/public/app/features/dashboard/components/SaveModals/SaveDashboardModalCtrl.ts @@ -52,8 +52,8 @@ const template = ` diff --git a/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts b/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts index cb83a1baa0c..b08a733d877 100644 --- a/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts +++ b/public/app/features/dashboard/components/UnsavedChangesModal/UnsavedChangesModalCtrl.ts @@ -20,7 +20,7 @@ const template = `
- +
diff --git a/public/app/features/dashboard/components/VersionHistory/template.html b/public/app/features/dashboard/components/VersionHistory/template.html index 5a053c46cc6..c7e94682d28 100644 --- a/public/app/features/dashboard/components/VersionHistory/template.html +++ b/public/app/features/dashboard/components/VersionHistory/template.html @@ -64,7 +64,7 @@ Show more versions diff --git a/public/app/features/folders/FolderSettingsPage.tsx b/public/app/features/folders/FolderSettingsPage.tsx index 08bc84775dc..efd2802178f 100644 --- a/public/app/features/folders/FolderSettingsPage.tsx +++ b/public/app/features/folders/FolderSettingsPage.tsx @@ -82,7 +82,7 @@ export class FolderSettingsPage extends PureComponent { />
-
-
diff --git a/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html b/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html index 8a67517aa92..fd805465a55 100644 --- a/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html +++ b/public/app/features/manage-dashboards/components/MoveToFolderModal/template.html @@ -26,7 +26,7 @@
- + Cancel
diff --git a/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts index 0c38a1247f1..44f831af0c2 100644 --- a/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts +++ b/public/app/features/manage-dashboards/components/UploadDashboard/uploadDashboardDirective.ts @@ -4,7 +4,7 @@ import angular from 'angular'; const template = ` -
@@ -317,8 +317,8 @@
- - + +
diff --git a/public/app/features/users/UsersActionBar.tsx b/public/app/features/users/UsersActionBar.tsx index 28ed4754d01..c7ce8c6f894 100644 --- a/public/app/features/users/UsersActionBar.tsx +++ b/public/app/features/users/UsersActionBar.tsx @@ -65,12 +65,12 @@ export class UsersActionBar extends PureComponent { )}
{canInvite && ( - + Invite )} {externalUserMngLinkUrl && ( - + {externalUserMngLinkName} )} diff --git a/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap b/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap index e69accb011b..a73d298581e 100644 --- a/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap +++ b/public/app/features/users/__snapshots__/UsersActionBar.test.tsx.snap @@ -105,7 +105,7 @@ exports[`Render should show external user management button 1`] = ` className="page-action-bar__spacer" /> @@ -143,7 +143,7 @@ exports[`Render should show invite button 1`] = ` className="page-action-bar__spacer" /> diff --git a/public/app/partials/confirm_modal.html b/public/app/partials/confirm_modal.html index d0b0d260f78..5d80f59a41f 100644 --- a/public/app/partials/confirm_modal.html +++ b/public/app/partials/confirm_modal.html @@ -26,7 +26,7 @@
- +
diff --git a/public/app/partials/edit_json.html b/public/app/partials/edit_json.html index 91552f95d41..fb411e662fc 100644 --- a/public/app/partials/edit_json.html +++ b/public/app/partials/edit_json.html @@ -15,7 +15,7 @@
diff --git a/public/app/partials/reset_password.html b/public/app/partials/reset_password.html index bba38af0235..085cc34d111 100644 --- a/public/app/partials/reset_password.html +++ b/public/app/partials/reset_password.html @@ -16,7 +16,7 @@
-
diff --git a/public/app/partials/signup_invited.html b/public/app/partials/signup_invited.html index c4c08c9ded8..966dba2d352 100644 --- a/public/app/partials/signup_invited.html +++ b/public/app/partials/signup_invited.html @@ -30,7 +30,7 @@
-
diff --git a/public/app/partials/signup_step2.html b/public/app/partials/signup_step2.html index b01c8160b16..5fae3563600 100644 --- a/public/app/partials/signup_step2.html +++ b/public/app/partials/signup_step2.html @@ -37,7 +37,7 @@
- diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 7b0ed869bdc..66943bb733a 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -3,6 +3,18 @@ $theme-name: dark; +// New Colors +// ------------------------- +$sapphire-faint: #041126; +$sapphire-bright: #5794F2; +$sapphire-base: #3274D9; +$sapphire-shade: #1F60C4; +$lobster-base: #E02F44; +$lobster-shade: #C4162A; +$forest-light: #96D98D; +$forest-base: #37872D; +$forest-shade: #19730E; + // Grays // ------------------------- $black: #000; @@ -30,31 +42,29 @@ $white: #fff; // Accent colors // ------------------------- $blue: #33b5e5; -$blue-dark: #005f81; $green: #299c46; -$red: #d44a3a; +$red: $lobster-base; $yellow: #ecbb13; -$pink: #ff4444; $purple: #9933cc; $variable: #32d1df; $orange: #eb7b18; $brand-primary: $orange; -$brand-success: $green; +$brand-success: $forest-base; $brand-warning: $brand-primary; -$brand-danger: $red; +$brand-danger: $lobster-base; -$query-red: #e24d42; -$query-green: #74e680; +$query-red: $lobster-base; +$query-green: $forest-light; $query-purple: #fe85fc; $query-keyword: #66d9ef; $query-orange: $orange; // Status colors // ------------------------- -$online: #10a345; +$online: $forest-base; $warn: #f79520; -$critical: #ed2e18; +$critical: $lobster-base; // Scaffolding // ------------------------- @@ -68,7 +78,6 @@ $text-color-weak: $gray-2; $text-color-faint: $dark-5; $text-color-emphasis: $gray-5; -$text-shadow-strong: 1px 1px 4px $black; $text-shadow-faint: 1px 1px 4px rgb(45, 45, 45); // gradients @@ -87,7 +96,7 @@ $edit-gradient: linear-gradient(180deg, rgb(22, 23, 25) 50%, #090909); $link-color: darken($white, 11%); $link-color-disabled: darken($link-color, 30%); $link-hover-color: $white; -$external-link-color: $blue; +$external-link-color: $sapphire-bright; // Typography // ------------------------- @@ -100,8 +109,7 @@ $hr-border-color: $dark-4; // Panel // ------------------------- $panel-bg: #212124; -$panel-border-color: $dark-1; -$panel-border: solid 1px $panel-border-color; +$panel-border: solid 1px $dark-1; $panel-header-hover-bg: $dark-4; $panel-corner: $panel-bg; @@ -110,7 +118,7 @@ $page-header-bg: linear-gradient(90deg, #292a2d, black); $page-header-shadow: inset 0px -4px 14px $dark-2; $page-header-border-color: $dark-4; -$divider-border-color: #555; +$divider-border-color: $gray-1; // Graphite Target Editor $tight-form-bg: $dark-3; @@ -153,29 +161,20 @@ $table-bg-hover: $dark-3; // Buttons // ------------------------- -$btn-primary-bg: #ff6600; -$btn-primary-bg-hl: #bc3e06; +$btn-primary-bg: $sapphire-base; +$btn-primary-bg-hl: $sapphire-shade; -$btn-secondary-bg-hl: lighten($blue-dark, 5%); -$btn-secondary-bg: $blue-dark; +$btn-secondary-bg: $sapphire-base; +$btn-secondary-bg-hl: $sapphire-shade; -$btn-success-bg: $green; -$btn-success-bg-hl: darken($green, 6%); - -$btn-warning-bg: $brand-warning; -$btn-warning-bg-hl: lighten($brand-warning, 8%); - -$btn-danger-bg: $red; -$btn-danger-bg-hl: darken($red, 8%); +$btn-danger-bg: $lobster-base; +$btn-danger-bg-hl: $lobster-shade; $btn-inverse-bg: $dark-3; $btn-inverse-bg-hl: lighten($dark-3, 4%); $btn-inverse-text-color: $link-color; $btn-inverse-text-shadow: 0px 1px 0 rgba(0, 0, 0, 0.1); -$btn-active-bg: $gray-4; -$btn-active-text-color: $blue-dark; - $btn-link-color: $gray-3; $iconContainerBackground: $black; @@ -281,11 +280,11 @@ $toolbar-bg: $input-black; // ------------------------- $warning-text-color: $warn; $error-text-color: #e84d4d; -$success-text-color: #12d95a; -$info-text-color: $blue-dark; +$success-text-color: $forest-light; +//$info-text-color: $blue-dark; $alert-error-bg: linear-gradient(90deg, #d44939, #e0603d); -$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); +$alert-success-bg: linear-gradient(90deg, $forest-base, $forest-shade); $alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); $alert-info-bg: linear-gradient(100deg, #1a4552, #00374a); @@ -317,7 +316,7 @@ $tooltipBackgroundBrand: $brand-primary; $checkboxImageUrl: '../img/checkbox.png'; // info box -$info-box-border-color: darken($blue, 12%); +$info-box-border-color: $sapphire-base; // footer $footer-link-color: $gray-2; @@ -348,8 +347,8 @@ $diff-arrow-color: $white; $diff-json-bg: $dark-4; $diff-json-fg: $gray-5; -$diff-json-added: #457740; -$diff-json-deleted: #a04338; +$diff-json-added: $sapphire-shade; +$diff-json-deleted: $lobster-shade; $diff-json-old: #a04338; $diff-json-new: #457740; @@ -360,21 +359,21 @@ $diff-json-changed-num: $text-color; $diff-json-icon: $gray-7; //Submenu -$variable-option-bg: $blue-dark; +$variable-option-bg: $sapphire-shade; //Switch Slider // ------------------------- $switch-bg: $input-bg; $switch-slider-color: $dark-2; $switch-slider-off-bg: $gray-1; -$switch-slider-on-bg: linear-gradient(90deg, $orange, $red); +$switch-slider-on-bg: linear-gradient(90deg, #eb7b18, #d44a3a); $switch-slider-shadow: 0 0 3px black; //Checkbox // ------------------------- $checkbox-bg: $dark-1; $checkbox-border: 1px solid $gray-1; -$checkbox-checked-bg: linear-gradient(0deg, $orange, $red); +$checkbox-checked-bg: linear-gradient(0deg, #eb7b18, #d44a3a); $checkbox-color: $dark-1; //Panel Edit @@ -385,24 +384,24 @@ $panel-editor-side-menu-shadow: drop-shadow(0 0 10px $black); $panel-editor-toolbar-view-bg: $input-black; $panel-editor-viz-item-shadow: 0 0 8px $dark-5; $panel-editor-viz-item-border: 1px solid $dark-5; -$panel-editor-viz-item-shadow-hover: 0 0 4px $blue; -$panel-editor-viz-item-border-hover: 1px solid $blue; +$panel-editor-viz-item-shadow-hover: 0 0 4px $sapphire-shade; +$panel-editor-viz-item-border-hover: 1px solid $sapphire-shade; $panel-editor-viz-item-bg: $input-black; $panel-editor-tabs-line-color: #e3e3e3; -$panel-editor-viz-item-bg-hover: darken($blue, 47%); +$panel-editor-viz-item-bg-hover: $sapphire-faint; $panel-editor-viz-item-bg-hover-active: darken($orange, 45%); $panel-options-group-border: none; $panel-options-group-header-bg: $gray-blue; -$panel-grid-placeholder-bg: darken($blue, 47%); -$panel-grid-placeholder-shadow: 0 0 4px $blue; +$panel-grid-placeholder-bg: $sapphire-faint; +$panel-grid-placeholder-shadow: 0 0 4px $sapphire-shade; // logs $logs-color-unkown: $gray-2; // toggle-group -$button-toggle-group-btn-active-bg: linear-gradient(90deg, $orange, $red); +$button-toggle-group-btn-active-bg: linear-gradient(90deg, #eb7b18, #d44a3a); $button-toggle-group-btn-active-shadow: inset 0 0 4px $black; $button-toggle-group-btn-seperator-border: 1px solid $page-bg; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 10c074e1481..85cb047be25 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -7,6 +7,19 @@ $theme-name: light; +// New Colors +// ------------------------- +$sapphire-faint: #F5F9FF; +$sapphire-light: #A8CAFF; +$sapphire-base: #3274D9; +$sapphire-shade: #1F60C4; +$lobster-base: #E02F44; +$lobster-shade: #C4162A; +$green-base: #37872D; +$green-shade: #19730E; +$purple-shade: #8F3BB8; +$yellow-base: #F2CC0C; + // Grays // ------------------------- $black: #000; @@ -31,32 +44,29 @@ $white: #fff; // Accent colors // ------------------------- $blue: #0083b3; -$blue-dark: #005f81; -$blue-light: #00a8e6; $green: #3aa655; -$red: #d44939; +$red: $lobster-base; $yellow: #ff851b; $orange: #ff7941; -$pink: #e671b8; $purple: #9954bb; -$variable: $blue; +$variable: $purple-shade; $brand-primary: $orange; $brand-success: $green; $brand-warning: $orange; -$brand-danger: $red; +$brand-danger: $lobster-base; -$query-red: $red; +$query-red: $lobster-base; $query-green: $green; $query-purple: $purple; $query-orange: $orange; -$query-keyword: $blue; +$query-keyword: $sapphire-base; // Status colors // ------------------------- -$online: #01a64f; +$online: $green-shade; $warn: #f79520; -$critical: #ec2128; +$critical: $lobster-shade; // Scaffolding // ------------------------- @@ -70,9 +80,7 @@ $text-color-weak: $gray-2; $text-color-faint: $gray-4; $text-color-emphasis: $dark-5; -$text-shadow-strong: none; $text-shadow-faint: none; -$textShadow: none; // gradients $brand-gradient: linear-gradient(to right, rgba(255, 213, 0, 1) 0%, rgba(255, 68, 0, 1) 99%, rgba(255, 68, 0, 1) 100%); @@ -84,7 +92,7 @@ $edit-gradient: linear-gradient(-60deg, $gray-7, #f5f6f9 70%, $gray-7 98%); $link-color: $gray-1; $link-color-disabled: lighten($link-color, 30%); $link-hover-color: darken($link-color, 20%); -$external-link-color: $blue-light; +$external-link-color: $sapphire-shade; // Typography // ------------------------- @@ -98,8 +106,7 @@ $hr-border-color: $dark-3 !default; // ------------------------- $panel-bg: $white; -$panel-border-color: $gray-5; -$panel-border: solid 1px $panel-border-color; +$panel-border: solid 1px $gray-5; $panel-header-hover-bg: $gray-6; $panel-corner: $gray-4; @@ -150,29 +157,20 @@ $scrollbarBorder: $gray-4; // Buttons // ------------------------- -$btn-primary-bg: $brand-primary; -$btn-primary-bg-hl: lighten($brand-primary, 8%); +$btn-primary-bg: $sapphire-base; +$btn-primary-bg-hl: $sapphire-shade; -$btn-secondary-bg: $blue; -$btn-secondary-bg-hl: lighten($blue, 4%); +$btn-secondary-bg: rgba(0,0,0,0); +$btn-secondary-bg-hl: rgba(0,0,0,0); -$btn-success-bg: lighten($green, 3%); -$btn-success-bg-hl: darken($green, 3%); - -$btn-warning-bg: lighten($orange, 3%); -$btn-warning-bg-hl: darken($orange, 3%); - -$btn-danger-bg: lighten($red, 3%); -$btn-danger-bg-hl: darken($red, 3%); +$btn-danger-bg: $lobster-base; +$btn-danger-bg-hl: $lobster-shade; $btn-inverse-bg: $gray-6; $btn-inverse-bg-hl: darken($gray-6, 5%); $btn-inverse-text-color: $gray-1; $btn-inverse-text-shadow: 0 1px 0 rgba(255, 255, 255, 0.4); -$btn-active-bg: $white; -$btn-active-text-color: $blue; - $btn-link-color: $gray-1; $btn-divider-left: $gray-4; @@ -189,8 +187,8 @@ $input-bg-disabled: $gray-5; $input-color: $dark-3; $input-border-color: $gray-5; $input-box-shadow: none; -$input-border-focus: $blue !default; -$input-box-shadow-focus: $blue !default; +$input-border-focus: $sapphire-light !default; +$input-box-shadow-focus: $sapphire-light !default; $input-color-placeholder: $gray-4 !default; $input-label-bg: $gray-5; $input-label-border-color: $gray-5; @@ -285,14 +283,14 @@ $navbar-button-border: $gray-4; // Form states and alerts // ------------------------- $warning-text-color: lighten($orange, 10%); -$error-text-color: lighten($red, 10%); +$error-text-color: $lobster-shade; $success-text-color: lighten($green, 10%); -$info-text-color: $blue; +$info-text-color: $sapphire-shade; -$alert-error-bg: linear-gradient(90deg, #d44939, #e04d3d); -$alert-success-bg: linear-gradient(90deg, #3aa655, #47b274); -$alert-warning-bg: linear-gradient(90deg, #d44939, #e04d3d); -$alert-info-bg: $blue; +$alert-error-bg: linear-gradient(90deg, $lobster-base, $lobster-shade); +$alert-success-bg: linear-gradient(90deg, $green-base, $green-shade); +$alert-warning-bg: linear-gradient(90deg, $lobster-base, $lobster-shade); +$alert-info-bg: $sapphire-base; // popover $popover-bg: $page-bg; @@ -300,7 +298,7 @@ $popover-color: $text-color; $popover-border-color: $gray-5; $popover-shadow: 0 0 20px $white; -$popover-help-bg: $blue; +$popover-help-bg: $sapphire-base; $popover-help-color: $gray-6; $popover-error-bg: $btn-danger-bg; @@ -321,7 +319,7 @@ $tooltipBackgroundBrand: $brand-primary; $checkboxImageUrl: '../img/checkbox_white.png'; // info box -$info-box-border-color: lighten($blue, 20%); +$info-box-border-color: $sapphire-base; // footer $footer-link-color: $gray-3; @@ -332,16 +330,16 @@ $footer-link-hover: $dark-5; // json explorer $json-explorer-default-color: black; $json-explorer-string-color: green; -$json-explorer-number-color: blue; -$json-explorer-boolean-color: red; +$json-explorer-number-color: $sapphire-base; +$json-explorer-boolean-color: $lobster-base; $json-explorer-null-color: #855a00; $json-explorer-undefined-color: rgb(202, 11, 105); $json-explorer-function-color: #ff20ed; $json-explorer-rotate-time: 100ms; $json-explorer-toggler-opacity: 0.6; -$json-explorer-bracket-color: blue; +$json-explorer-bracket-color: $sapphire-base; $json-explorer-key-color: #00008b; -$json-explorer-url-color: blue; +$json-explorer-url-color: $sapphire-base; // Changelog and diff // ------------------------- @@ -355,34 +353,34 @@ $diff-arrow-color: $dark-3; $diff-group-bg: $gray-7; $diff-json-bg: $gray-5; -$diff-json-fg: $gray-2; +$diff-json-fg: $gray-1; -$diff-json-added: lighten(desaturate($green, 30%), 10%); -$diff-json-deleted: desaturate($red, 35%); +$diff-json-added: $sapphire-shade; +$diff-json-deleted: $lobster-shade; $diff-json-old: #5a372a; $diff-json-new: #664e33; -$diff-json-changed-fg: $gray-6; +$diff-json-changed-fg: $gray-7; $diff-json-changed-num: $gray-4; $diff-json-icon: $gray-4; //Submenu -$variable-option-bg: $blue-light; +$variable-option-bg: $sapphire-light; //Switch Slider // ------------------------- $switch-bg: $white; $switch-slider-color: $gray-7; $switch-slider-off-bg: $gray-5; -$switch-slider-on-bg: linear-gradient(90deg, $yellow, $red); +$switch-slider-on-bg: linear-gradient(90deg, #FF9830, #E55400); $switch-slider-shadow: 0 0 3px $dark-5; //Checkbox // ------------------------- $checkbox-bg: $gray-6; $checkbox-border: 1px solid $gray-3; -$checkbox-checked-bg: linear-gradient(0deg, $yellow, $red); +$checkbox-checked-bg: linear-gradient(0deg, #FF9830, #E55400); $checkbox-color: $gray-7; //Panel Edit @@ -393,18 +391,18 @@ $panel-editor-side-menu-shadow: drop-shadow(0 0 2px $gray-3); $panel-editor-toolbar-view-bg: $white; $panel-editor-viz-item-shadow: 0 0 4px $gray-3; $panel-editor-viz-item-border: 1px solid $gray-3; -$panel-editor-viz-item-shadow-hover: 0 0 4px $blue-light; -$panel-editor-viz-item-border-hover: 1px solid $blue-light; +$panel-editor-viz-item-shadow-hover: 0 0 4px $sapphire-light; +$panel-editor-viz-item-border-hover: 1px solid $sapphire-light; $panel-editor-viz-item-bg: $white; $panel-editor-tabs-line-color: $dark-5; -$panel-editor-viz-item-bg-hover: lighten($blue, 62%); +$panel-editor-viz-item-bg-hover: $sapphire-faint; $panel-editor-viz-item-bg-hover-active: lighten($orange, 34%); $panel-options-group-border: none; $panel-options-group-header-bg: $gray-5; -$panel-grid-placeholder-bg: lighten($blue, 62%); -$panel-grid-placeholder-shadow: 0 0 4px $blue-light; +$panel-grid-placeholder-bg: $sapphire-faint; +$panel-grid-placeholder-shadow: 0 0 4px $sapphire-light; // logs $logs-color-unkown: $gray-5; diff --git a/public/sass/base/_type.scss b/public/sass/base/_type.scss index 1a005b0d511..e5a20a80659 100644 --- a/public/sass/base/_type.scss +++ b/public/sass/base/_type.scss @@ -59,13 +59,13 @@ a.text-error:focus { color: darken($error-text-color, 10%); } -.text-info { +/*.text-info { color: $info-text-color; } a.text-info:hover, a.text-info:focus { color: darken($info-text-color, 10%); -} +}*/ .text-success { color: $success-text-color; diff --git a/public/sass/components/_buttons.scss b/public/sass/components/_buttons.scss index 4e032d7b9d1..84e2665f582 100644 --- a/public/sass/components/_buttons.scss +++ b/public/sass/components/_buttons.scss @@ -89,35 +89,12 @@ .btn-secondary { @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); } -// Warning appears are orange -.btn-warning { - @include buttonBackground($btn-warning-bg, $btn-warning-bg-hl); -} // Danger and error appear as red .btn-danger { @include buttonBackground($btn-danger-bg, $btn-danger-bg-hl); } -// Success appears as green -.btn-success { - @include buttonBackground($btn-success-bg, $btn-success-bg-hl); - &--processing { - @include button-outline-variant($gray-1); - @include box-shadow(none); - cursor: default; - - &:hover, - &:active, - &:active:hover, - &:focus, - &:disabled { - color: $gray-1; - background-color: transparent; - border-color: $gray-1; - } - } -} // Info appears as a neutral blue .btn-secondary { @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); @@ -138,20 +115,15 @@ @include button-outline-variant($btn-primary-bg); } .btn-outline-secondary { - @include button-outline-variant($btn-secondary-bg); + @include button-outline-variant($btn-secondary-bg-hl); } .btn-outline-inverse { @include button-outline-variant($btn-inverse-bg); } -.btn-outline-success { - @include button-outline-variant($btn-success-bg); -} -.btn-outline-warning { - @include button-outline-variant($btn-warning-bg); -} .btn-outline-danger { - @include button-outline-variant($btn-danger-bg); + @include button-outline-variant(green); } + .btn-outline-disabled { @include button-outline-variant($gray-1); @include box-shadow(none); diff --git a/public/sass/components/_navbar.scss b/public/sass/components/_navbar.scss index b3733b694fc..088dd72f37b 100644 --- a/public/sass/components/_navbar.scss +++ b/public/sass/components/_navbar.scss @@ -158,7 +158,7 @@ } &--primary { - @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl); + @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl); } } diff --git a/public/sass/components/_panel_gettingstarted.scss b/public/sass/components/_panel_gettingstarted.scss index 5bbc4ba29ca..b51bd3a9ef9 100644 --- a/public/sass/components/_panel_gettingstarted.scss +++ b/public/sass/components/_panel_gettingstarted.scss @@ -118,7 +118,7 @@ $path-position: $marker-size-half - ($path-height / 2); .progress-step-cta { @include button-size($btn-padding-y-sm, $btn-padding-x-sm, $font-size-sm, $btn-border-radius); - @include buttonBackground($btn-success-bg, $btn-success-bg-hl); + @include buttonBackground($btn-primary-bg, $btn-primary-bg-hl); display: none; } diff --git a/public/vendor/angular-ui/ui-bootstrap-tpls.js b/public/vendor/angular-ui/ui-bootstrap-tpls.js index 87120b66ce1..ad6f3b4b4bc 100644 --- a/public/vendor/angular-ui/ui-bootstrap-tpls.js +++ b/public/vendor/angular-ui/ui-bootstrap-tpls.js @@ -1245,7 +1245,7 @@ angular.module("template/datepicker/popup.html", []).run(["$templateCache", func " \n" + " \n" + " \n" + - " \n" + + " \n" + " \n" + "\n" + ""); From fd1ef0a2be8ae3115ddeb65f44b1a50dbd5cb650 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 12:10:42 +0100 Subject: [PATCH 047/228] Added custom scrollbar and remember scroll pos to jump back to same scroll pos when going back to dashboard from edit mode --- .../CustomScrollbar/CustomScrollbar.tsx | 1 + public/app/core/components/Page/Page.tsx | 7 --- .../dashboard/containers/DashboardPage.tsx | 45 +++++++++++++------ public/app/routes/GrafanaCtrl.ts | 1 + public/app/routes/ReactContainer.tsx | 3 ++ 5 files changed, 36 insertions(+), 21 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 40f6c6c3c37..17c511826fb 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -45,6 +45,7 @@ export class CustomScrollbar extends PureComponent { if (this.props.scrollTop > 10000) { ref.scrollToBottom(); } else { + console.log('scrollbar set scrollTop'); ref.scrollTop(this.props.scrollTop); } } diff --git a/public/app/core/components/Page/Page.tsx b/public/app/core/components/Page/Page.tsx index c4846ecf85d..997f02b700c 100644 --- a/public/app/core/components/Page/Page.tsx +++ b/public/app/core/components/Page/Page.tsx @@ -17,13 +17,10 @@ interface Props { } class Page extends Component { - private bodyClass = 'is-react'; - private body = document.body; static Header = PageHeader; static Contents = PageContents; componentDidMount() { - this.body.classList.add(this.bodyClass); this.updateTitle(); } @@ -33,10 +30,6 @@ class Page extends Component { } } - componentWillUnmount() { - this.body.classList.remove(this.bodyClass); - } - updateTitle = () => { const title = this.getPageTitle; document.title = title ? title + ' - Grafana' : 'Grafana'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 404c953eecb..33f2a602b0c 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -1,6 +1,6 @@ // Libraries import $ from 'jquery'; -import React, { PureComponent } from 'react'; +import React, { PureComponent, MouseEvent } from 'react'; import { hot } from 'react-hot-loader'; import { connect } from 'react-redux'; import classNames from 'classnames'; @@ -9,11 +9,11 @@ import classNames from 'classnames'; import { createErrorNotification } from 'app/core/copy/appNotification'; // Components -import { LoadingPlaceholder } from '@grafana/ui'; import { DashboardGrid } from '../dashgrid/DashboardGrid'; import { DashNav } from '../components/DashNav'; import { SubMenu } from '../components/SubMenu'; import { DashboardSettings } from '../components/DashboardSettings'; +import { CustomScrollbar } from '@grafana/ui'; // Redux import { initDashboard } from '../state/initDashboard'; @@ -50,6 +50,8 @@ interface State { isEditing: boolean; isFullscreen: boolean; fullscreenPanel: PanelModel | null; + scrollTop: number; + rememberScrollTop: number; } export class DashboardPage extends PureComponent { @@ -58,6 +60,8 @@ export class DashboardPage extends PureComponent { isEditing: false, isFullscreen: false, fullscreenPanel: null, + scrollTop: 0, + rememberScrollTop: 0, }; async componentDidMount() { @@ -121,6 +125,7 @@ export class DashboardPage extends PureComponent { isEditing: urlEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel, + rememberScrollTop: this.state.scrollTop, }); this.setPanelFullscreenClass(urlFullscreen); } else { @@ -135,9 +140,17 @@ export class DashboardPage extends PureComponent { dashboard.setViewMode(this.state.fullscreenPanel, false, false); } - this.setState({ isEditing: false, isFullscreen: false, fullscreenPanel: null }, () => { - dashboard.render(); - }); + this.setState( + { + isEditing: false, + isFullscreen: false, + fullscreenPanel: null, + scrollTop: this.state.rememberScrollTop, + }, + () => { + dashboard.render(); + } + ); this.setPanelFullscreenClass(false); } @@ -160,9 +173,10 @@ export class DashboardPage extends PureComponent { $('body').toggleClass('panel-in-fullscreen', isFullscreen); } - renderLoadingState() { - return ; - } + setScrollTop = (e: MouseEvent): void => { + const target = e.target as HTMLElement; + this.setState({ scrollTop: target.scrollTop }); + }; renderDashboard() { const { dashboard, editview } = this.props; @@ -186,7 +200,7 @@ export class DashboardPage extends PureComponent { render() { const { dashboard, editview, $injector } = this.props; - const { isSettingsOpening, isEditing, isFullscreen } = this.state; + const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state; if (!dashboard) { return null; @@ -201,6 +215,7 @@ export class DashboardPage extends PureComponent { 'dashboard-container': true, 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, }); + return (
{ $injector={$injector} />
- {dashboard && editview && } + + {dashboard && editview && } -
- {dashboard.meta.submenuEnabled && } - -
+
+ {dashboard.meta.submenuEnabled && } + +
+
); diff --git a/public/app/routes/GrafanaCtrl.ts b/public/app/routes/GrafanaCtrl.ts index 07d99725113..9157c189ab2 100644 --- a/public/app/routes/GrafanaCtrl.ts +++ b/public/app/routes/GrafanaCtrl.ts @@ -45,6 +45,7 @@ export class GrafanaCtrl { }; $rootScope.colors = colors; + $rootScope.onAppEvent = function(name, callback, localScope) { const unbind = $rootScope.$on(name, callback); let callerScope = this; diff --git a/public/app/routes/ReactContainer.tsx b/public/app/routes/ReactContainer.tsx index a56c8878fb1..d64e74e3949 100644 --- a/public/app/routes/ReactContainer.tsx +++ b/public/app/routes/ReactContainer.tsx @@ -47,9 +47,12 @@ export function reactContainer( routeInfo: $route.current.$$route.routeInfo, }; + document.body.classList.add('is-react'); + ReactDOM.render(WrapInProvider(store, component, props), elem[0]); scope.$on('$destroy', () => { + document.body.classList.remove('is-react'); ReactDOM.unmountComponentAtNode(elem[0]); }); }, From 181b4f9e80fbe7a6aa39c957dc79c863dcdbf11a Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 Feb 2019 14:39:24 +0300 Subject: [PATCH 048/228] azuremonitor: improve autocomplete experence --- .../editor/KustoQueryField.tsx | 24 ++++++++++--------- .../editor/query_field.tsx | 6 ++--- 2 files changed, 16 insertions(+), 14 deletions(-) diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx index bbe34b8f46a..0a484794e8f 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/KustoQueryField.tsx @@ -65,7 +65,7 @@ export default class KustoQueryField extends QueryField { this.fetchSchema(); } - onTypeahead = () => { + onTypeahead = (force = false) => { const selection = window.getSelection(); if (selection.anchorNode) { const wrapperNode = selection.anchorNode.parentElement; @@ -140,14 +140,8 @@ export default class KustoQueryField extends QueryField { suggestionGroups = this.getTableSuggestions(db); prefix = prefix.replace('.', ''); - // built-in - } else if (prefix && !wrapperClasses.contains('argument')) { - if (modelPrefix.match(/\s$/i)) { - prefix = ''; - } - typeaheadContext = 'context-builtin'; - suggestionGroups = this.getKeywordSuggestions(); - } else if (Plain.serialize(this.state.value) === '') { + // new + } else if (normalizeQuery(Plain.serialize(this.state.value)).match(/^\s*\w*$/i)) { typeaheadContext = 'context-new'; if (this.schema) { suggestionGroups = this.getInitialSuggestions(); @@ -156,7 +150,15 @@ export default class KustoQueryField extends QueryField { setTimeout(this.onTypeahead, 0); return; } - } else { + + // built-in + } else if (prefix && !wrapperClasses.contains('argument')) { + if (modelPrefix.match(/\s$/i)) { + prefix = ''; + } + typeaheadContext = 'context-builtin'; + suggestionGroups = this.getKeywordSuggestions(); + } else if (force === true) { typeaheadContext = 'context-builtin'; if (modelPrefix.match(/\s$/i)) { prefix = ''; @@ -181,7 +183,7 @@ export default class KustoQueryField extends QueryField { .filter(group => group.items.length > 0); // console.log('onTypeahead', selection.anchorNode, wrapperClasses, text, offset, prefix, typeaheadContext); - console.log('onTypeahead', modelPrefix, prefix, typeaheadContext); + // console.log('onTypeahead', prefix, typeaheadContext); this.setState({ typeaheadPrefix: prefix, diff --git a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx index 0acd53cabff..e62337c4982 100644 --- a/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx +++ b/public/app/plugins/datasource/grafana-azure-monitor-datasource/editor/query_field.tsx @@ -104,11 +104,11 @@ class QueryField extends React.Component { const changed = value.document !== this.state.value.document; this.setState({ value }, () => { if (changed) { + // call typeahead only if query changed + window.requestAnimationFrame(this.onTypeahead); this.onChangeQuery(); } }); - - window.requestAnimationFrame(this.onTypeahead); }; request = (url?) => { @@ -143,7 +143,7 @@ class QueryField extends React.Component { case ' ': { if (event.ctrlKey) { event.preventDefault(); - this.onTypeahead(); + this.onTypeahead(true); return true; } break; From bbc5dff7bd719b4410eb685a8ff31dbd81ab96d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 12:56:03 +0100 Subject: [PATCH 049/228] Fixed add panel should scroll to top --- .../dashboard/components/DashNav/DashNav.tsx | 137 ++++++++---------- .../dashboard/containers/DashboardPage.tsx | 31 ++-- 2 files changed, 77 insertions(+), 91 deletions(-) diff --git a/public/app/features/dashboard/components/DashNav/DashNav.tsx b/public/app/features/dashboard/components/DashNav/DashNav.tsx index 374fd6dcd36..297d7ca7ea7 100644 --- a/public/app/features/dashboard/components/DashNav/DashNav.tsx +++ b/public/app/features/dashboard/components/DashNav/DashNav.tsx @@ -23,6 +23,7 @@ export interface Props { isFullscreen: boolean; $injector: any; updateLocation: typeof updateLocation; + onAddPanel: () => void; } export class DashNav extends PureComponent { @@ -39,7 +40,8 @@ export class DashNav extends PureComponent { componentDidMount() { const loader = getAngularLoader(); - const template = ''; + const template = + ''; const scopeProps = { dashboard: this.props.dashboard }; this.timepickerCmp = loader.load(this.timePickerEl, scopeProps, template); @@ -55,21 +57,6 @@ export class DashNav extends PureComponent { appEvents.emit('show-dash-search'); }; - onAddPanel = () => { - const { dashboard } = this.props; - - // Return if the "Add panel" exists already - if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') { - return; - } - - dashboard.addPanel({ - type: 'add-panel', - gridPos: { x: 0, y: 0, w: 12, h: 8 }, - title: 'Panel Title', - }); - }; - onClose = () => { if (this.props.editview) { this.props.updateLocation({ @@ -137,7 +124,7 @@ export class DashNav extends PureComponent { }; render() { - const { dashboard, isFullscreen, editview } = this.props; + const { dashboard, isFullscreen, editview, onAddPanel } = this.props; const { canStar, canSave, canShare, folderTitle, showSettings, isStarred } = dashboard.meta; const { snapshot } = dashboard; @@ -186,73 +173,73 @@ export class DashNav extends PureComponent { tooltip="Add panel" classSuffix="add-panel" icon="gicon gicon-add-panel" - onClick={this.onAddPanel} + onClick={onAddPanel} /> - )} + )} - {canStar && ( - - )} + {canStar && ( + + )} - {canShare && ( - - )} + {canShare && ( + + )} - {canSave && ( - - )} + {canSave && ( + + )} - {snapshotUrl && ( - - )} + {snapshotUrl && ( + + )} - {showSettings && ( - - )} -
+ {showSettings && ( + + )} + -
- -
+
+ +
-
(this.timePickerEl = element)} /> +
(this.timePickerEl = element)} /> - {(isFullscreen || editview) && ( -
- -
- )} -
+ {(isFullscreen || editview) && ( +
+ +
+ )} +
); } } diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 33f2a602b0c..1d1882f277d 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -178,25 +178,23 @@ export class DashboardPage extends PureComponent { this.setState({ scrollTop: target.scrollTop }); }; - renderDashboard() { - const { dashboard, editview } = this.props; - const { isEditing, isFullscreen } = this.state; + onAddPanel = () => { + const { dashboard } = this.props; - const classes = classNames({ - 'dashboard-container': true, - 'dashboard-container--has-submenu': dashboard.meta.submenuEnabled, + // Return if the "Add panel" exists already + if (dashboard.panels.length > 0 && dashboard.panels[0].type === 'add-panel') { + return; + } + + dashboard.addPanel({ + type: 'add-panel', + gridPos: { x: 0, y: 0, w: 12, h: 8 }, + title: 'Panel Title', }); - return ( -
- {dashboard && editview && } - -
- -
-
- ); - } + // scroll to top after adding panel + this.setState({ scrollTop: 0 }); + }; render() { const { dashboard, editview, $injector } = this.props; @@ -224,6 +222,7 @@ export class DashboardPage extends PureComponent { isFullscreen={isFullscreen} editview={editview} $injector={$injector} + onAddPanel={this.onAddPanel} />
From 08925ffad89a7a99f42a2ae604251e49a3409e1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 13:49:35 +0100 Subject: [PATCH 050/228] Basic loading state for slow dashboards --- .../CustomScrollbar/CustomScrollbar.tsx | 1 - public/app/core/redux/index.ts | 6 ++---- .../dashboard/containers/DashboardPage.tsx | 19 ++++++++++++++++++- .../app/features/dashboard/state/actions.ts | 4 ++-- .../features/dashboard/state/initDashboard.ts | 10 +++++++++- .../app/features/dashboard/state/reducers.ts | 13 +++++++++++-- public/app/types/dashboard.ts | 1 + public/sass/pages/_dashboard.scss | 11 +++++++++++ 8 files changed, 54 insertions(+), 11 deletions(-) diff --git a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx index 17c511826fb..40f6c6c3c37 100644 --- a/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx +++ b/packages/grafana-ui/src/components/CustomScrollbar/CustomScrollbar.tsx @@ -45,7 +45,6 @@ export class CustomScrollbar extends PureComponent { if (this.props.scrollTop > 10000) { ref.scrollToBottom(); } else { - console.log('scrollbar set scrollTop'); ref.scrollTop(this.props.scrollTop); } } diff --git a/public/app/core/redux/index.ts b/public/app/core/redux/index.ts index 359f160b9ce..bf45d7d22df 100644 --- a/public/app/core/redux/index.ts +++ b/public/app/core/redux/index.ts @@ -1,4 +1,2 @@ -import { actionCreatorFactory } from './actionCreatorFactory'; -import { reducerFactory } from './reducerFactory'; - -export { actionCreatorFactory, reducerFactory }; +export { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from './actionCreatorFactory'; +export { reducerFactory } from './reducerFactory'; diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 1d1882f277d..5fa48f45375 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -38,6 +38,7 @@ interface Props { urlEdit: boolean; urlFullscreen: boolean; loadingState: DashboardLoadingState; + isLoadingSlow: boolean; dashboard: DashboardModel; initDashboard: typeof initDashboard; setDashboardModel: typeof setDashboardModel; @@ -52,6 +53,7 @@ interface State { fullscreenPanel: PanelModel | null; scrollTop: number; rememberScrollTop: number; + showLoadingState: boolean; } export class DashboardPage extends PureComponent { @@ -59,6 +61,7 @@ export class DashboardPage extends PureComponent { isSettingsOpening: false, isEditing: false, isFullscreen: false, + showLoadingState: false, fullscreenPanel: null, scrollTop: 0, rememberScrollTop: 0, @@ -196,11 +199,24 @@ export class DashboardPage extends PureComponent { this.setState({ scrollTop: 0 }); }; + renderLoadingState() { + return ( +
+
+ Dashboard {this.props.loadingState} +
+
+ ); + } + render() { - const { dashboard, editview, $injector } = this.props; + const { dashboard, editview, $injector, isLoadingSlow } = this.props; const { isSettingsOpening, isEditing, isFullscreen, scrollTop } = this.state; if (!dashboard) { + if (isLoadingSlow) { + return this.renderLoadingState(); + } return null; } @@ -249,6 +265,7 @@ const mapStateToProps = (state: StoreState) => ({ urlFullscreen: state.location.query.fullscreen === true, urlEdit: state.location.query.edit === true, loadingState: state.dashboard.loadingState, + isLoadingSlow: state.dashboard.isLoadingSlow, dashboard: state.dashboard.model as DashboardModel, }); diff --git a/public/app/features/dashboard/state/actions.ts b/public/app/features/dashboard/state/actions.ts index bc57b8e5f10..da4c195c953 100644 --- a/public/app/features/dashboard/state/actions.ts +++ b/public/app/features/dashboard/state/actions.ts @@ -3,8 +3,7 @@ import { ThunkAction } from 'redux-thunk'; // Services & Utils import { getBackendSrv } from 'app/core/services/backend_srv'; -import { actionCreatorFactory } from 'app/core/redux'; -import { ActionOf } from 'app/core/redux/actionCreatorFactory'; +import { actionCreatorFactory, noPayloadActionCreatorFactory, ActionOf } from 'app/core/redux'; import { createSuccessNotification } from 'app/core/copy/appNotification'; // Actions @@ -25,6 +24,7 @@ import { DashboardLoadingState, MutableDashboard } from 'app/types/dashboard'; export const loadDashboardPermissions = actionCreatorFactory('LOAD_DASHBOARD_PERMISSIONS').create(); export const setDashboardLoadingState = actionCreatorFactory('SET_DASHBOARD_LOADING_STATE').create(); export const setDashboardModel = actionCreatorFactory('SET_DASHBOARD_MODEL').create(); +export const setDashboardLoadingSlow = noPayloadActionCreatorFactory('SET_DASHBOARD_LOADING_SLOW').create(); export type Action = ActionOf; export type ThunkResult = ThunkAction; diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index 14428bfa290..d529ca0b531 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -12,7 +12,7 @@ import { config } from 'app/core/config'; import { updateLocation } from 'app/core/actions'; import { notifyApp } from 'app/core/actions'; import locationUtil from 'app/core/utils/location_util'; -import { setDashboardLoadingState, ThunkResult, setDashboardModel } from './actions'; +import { setDashboardLoadingState, ThunkResult, setDashboardModel, setDashboardLoadingSlow } from './actions'; import { removePanel } from '../utils/panel'; // Types @@ -71,6 +71,14 @@ export function initDashboard({ // set fetching state dispatch(setDashboardLoadingState(DashboardLoadingState.Fetching)); + // Detect slow loading / initializing and set state flag + // This is in order to not show loading indication for fast loading dashboards as it creates blinking/flashing + setTimeout(() => { + if (getState().dashboard.model === null) { + dispatch(setDashboardLoadingSlow()); + } + }, 500); + try { switch (routeInfo) { // handle old urls with no uid diff --git a/public/app/features/dashboard/state/reducers.ts b/public/app/features/dashboard/state/reducers.ts index 2f4e5df5c14..5566363c996 100644 --- a/public/app/features/dashboard/state/reducers.ts +++ b/public/app/features/dashboard/state/reducers.ts @@ -1,10 +1,11 @@ import { DashboardState, DashboardLoadingState } from 'app/types/dashboard'; -import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel } from './actions'; +import { loadDashboardPermissions, setDashboardLoadingState, setDashboardModel, setDashboardLoadingSlow } from './actions'; import { reducerFactory } from 'app/core/redux'; import { processAclItems } from 'app/core/utils/acl'; export const initialState: DashboardState = { loadingState: DashboardLoadingState.NotStarted, + isLoadingSlow: false, model: null, permissions: [], }; @@ -28,7 +29,15 @@ export const dashboardReducer = reducerFactory(initialState) filter: setDashboardModel, mapper: (state, action) => ({ ...state, - model: action.payload + model: action.payload, + isLoadingSlow: false, + }), + }) + .addMapper({ + filter: setDashboardLoadingSlow, + mapper: (state, action) => ({ + ...state, + isLoadingSlow: true, }), }) .create(); diff --git a/public/app/types/dashboard.ts b/public/app/types/dashboard.ts index 36c0a420f28..39d7e3cba8a 100644 --- a/public/app/types/dashboard.ts +++ b/public/app/types/dashboard.ts @@ -25,5 +25,6 @@ export enum DashboardLoadingState { export interface DashboardState { model: MutableDashboard | null; loadingState: DashboardLoadingState; + isLoadingSlow: boolean; permissions: DashboardAcl[] | null; } diff --git a/public/sass/pages/_dashboard.scss b/public/sass/pages/_dashboard.scss index 9ca4e092f02..0f37ffc850e 100644 --- a/public/sass/pages/_dashboard.scss +++ b/public/sass/pages/_dashboard.scss @@ -276,3 +276,14 @@ div.flot-text { .panel-full-edit { padding-top: $dashboard-padding; } + +.dashboard-loading { + height: 60vh; + display: flex; + align-items: center; + justify-content: center; +} + +.dashboard-loading__text { + font-size: $font-size-lg; +} From aa2bf07c71d0e42e9e2df7ef40ae939f2a60c016 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 14:12:32 +0100 Subject: [PATCH 051/228] Expand rows for panels in collapsed rows --- .../dashboard/containers/DashboardPage.tsx | 7 ++++++- .../dashboard/containers/SoloPanelPage.tsx | 8 +++++++- .../app/features/dashboard/state/DashboardModel.ts | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 5fa48f45375..0d28ccb19a2 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -120,7 +120,12 @@ export class DashboardPage extends PureComponent { onEnterFullscreen() { const { dashboard, urlEdit, urlFullscreen, urlPanelId } = this.props; - const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + const panelId = parseInt(urlPanelId, 10); + + // need to expand parent row if this panel is inside a row + dashboard.expandParentRowFor(panelId); + + const panel = dashboard.getPanelById(panelId); if (panel) { dashboard.setViewMode(panel, urlFullscreen, urlEdit); diff --git a/public/app/features/dashboard/containers/SoloPanelPage.tsx b/public/app/features/dashboard/containers/SoloPanelPage.tsx index beb45b6904d..915d2e03965 100644 --- a/public/app/features/dashboard/containers/SoloPanelPage.tsx +++ b/public/app/features/dashboard/containers/SoloPanelPage.tsx @@ -59,7 +59,13 @@ export class SoloPanelPage extends Component { // we just got the dashboard! if (!prevProps.dashboard) { - const panel = dashboard.getPanelById(parseInt(urlPanelId, 10)); + const panelId = parseInt(urlPanelId, 10); + + // need to expand parent row if this panel is inside a row + dashboard.expandParentRowFor(panelId); + + const panel = dashboard.getPanelById(panelId); + if (!panel) { this.setState({ notFound: true }); return; diff --git a/public/app/features/dashboard/state/DashboardModel.ts b/public/app/features/dashboard/state/DashboardModel.ts index 8756af2ceea..743eb61f97d 100644 --- a/public/app/features/dashboard/state/DashboardModel.ts +++ b/public/app/features/dashboard/state/DashboardModel.ts @@ -904,4 +904,18 @@ export class DashboardModel { this.processRepeats(); this.events.emit('template-variable-value-updated'); } + + expandParentRowFor(panelId: number) { + for (const panel of this.panels) { + if (panel.collapsed) { + for (const rowPanel of panel.panels) { + if (rowPanel.id === panelId) { + this.toggleRow(panel); + return; + } + } + } + } + } + } From da531032812565840c2e29c90b2f6e280adcb454 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 14:18:35 +0100 Subject: [PATCH 052/228] Prevent viewers from going into edit mode --- public/app/features/dashboard/containers/DashboardPage.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/containers/DashboardPage.tsx b/public/app/features/dashboard/containers/DashboardPage.tsx index 0d28ccb19a2..1bd5218fd60 100644 --- a/public/app/features/dashboard/containers/DashboardPage.tsx +++ b/public/app/features/dashboard/containers/DashboardPage.tsx @@ -130,7 +130,7 @@ export class DashboardPage extends PureComponent { if (panel) { dashboard.setViewMode(panel, urlFullscreen, urlEdit); this.setState({ - isEditing: urlEdit, + isEditing: urlEdit && dashboard.meta.canEdit, isFullscreen: urlFullscreen, fullscreenPanel: panel, rememberScrollTop: this.state.scrollTop, From ae0b027d69ce0fe2946aabfe55267150151a4038 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 09:34:04 +0100 Subject: [PATCH 053/228] chore: Replace sizeMe with AutoSizer in DashboardGrid --- .../dashboard/dashgrid/DashboardGrid.tsx | 26 ++++++++++++++----- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 658bfad3816..5a65fadd74b 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -5,13 +5,12 @@ import { GRID_CELL_HEIGHT, GRID_CELL_VMARGIN, GRID_COLUMN_COUNT } from 'app/core import { DashboardPanel } from './DashboardPanel'; import { DashboardModel, PanelModel } from '../state'; import classNames from 'classnames'; -import sizeMe from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; let lastGridWidth = 1200; let ignoreNextWidthChange = false; -interface GridWrapperProps { - size: { width: number; }; +interface SizedReactLayoutGridProps { layout: ReactGridLayout.Layout[]; onLayoutChange: (layout: ReactGridLayout.Layout[]) => void; children: JSX.Element | JSX.Element[]; @@ -25,8 +24,12 @@ interface GridWrapperProps { isFullscreen?: boolean; } +interface GridWrapperProps extends SizedReactLayoutGridProps { + sizedWidth: number; +} + function GridWrapper({ - size, + sizedWidth, layout, onLayoutChange, children, @@ -38,8 +41,8 @@ function GridWrapper({ isResizable, isDraggable, isFullscreen, -}: GridWrapperProps) { - const width = size.width > 0 ? size.width : lastGridWidth; +}: GridWrapperProps) { + const width = sizedWidth > 0 ? sizedWidth : lastGridWidth; // logic to ignore width changes (optimization) if (width !== lastGridWidth) { @@ -74,7 +77,16 @@ function GridWrapper({ ); } -const SizedReactLayoutGrid = sizeMe({ monitorWidth: true })(GridWrapper); +const SizedReactLayoutGrid = (props: SizedReactLayoutGridProps) => ( + + {({width}) => ( + + )} + +); export interface DashboardGridProps { dashboard: DashboardModel; From 097396c517e1db4e5633f7625168c255511deb1c Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 09:57:54 +0100 Subject: [PATCH 054/228] chore: Replace withSize with AutoSizer in explore/Graph.tsx --- public/app/features/explore/Graph.test.tsx | 1 + public/app/features/explore/Graph.tsx | 27 ++++++++++++++++++---- 2 files changed, 23 insertions(+), 5 deletions(-) diff --git a/public/app/features/explore/Graph.test.tsx b/public/app/features/explore/Graph.test.tsx index fe4deaf17aa..8976c677592 100644 --- a/public/app/features/explore/Graph.test.tsx +++ b/public/app/features/explore/Graph.test.tsx @@ -5,6 +5,7 @@ import { mockData } from './__mocks__/mockData'; const setup = (propOverrides?: object) => { const props = { + size: { width: 10, height: 20 }, data: mockData().slice(0, 19), range: { from: 'now-6h', to: 'now' }, ...propOverrides, diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index 5d64dde28ce..b087f6a457d 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -1,7 +1,7 @@ import $ from 'jquery'; import React, { PureComponent } from 'react'; import moment from 'moment'; -import { withSize } from 'react-sizeme'; +import { AutoSizer } from 'react-virtualized'; import 'vendor/flot/jquery.flot'; import 'vendor/flot/jquery.flot.time'; @@ -80,12 +80,15 @@ interface GraphProps { id?: string; range: RawTimeRange; split?: boolean; - size?: { width: number; height: number }; userOptions?: any; onChangeTime?: (range: RawTimeRange) => void; onToggleSeries?: (alias: string, hiddenSeries: Set) => void; } +interface SizedGraphProps extends GraphProps { + size: { width: number; height: number }; +} + interface GraphState { /** * Type parameter refers to the `alias` property of a `TimeSeries`. @@ -95,7 +98,7 @@ interface GraphState { showAllTimeSeries: boolean; } -export class Graph extends PureComponent { +export class Graph extends PureComponent { $el: any; dynamicOptions = null; @@ -116,7 +119,7 @@ export class Graph extends PureComponent { this.$el.bind('plotselected', this.onPlotSelected); } - componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { + componentDidUpdate(prevProps: SizedGraphProps, prevState: GraphState) { if ( prevProps.data !== this.props.data || prevProps.range !== this.props.range || @@ -261,4 +264,18 @@ export class Graph extends PureComponent { } } -export default withSize()(Graph); +export default (props: GraphProps) => ( + + {({width, height}) => { + return ( + + ); + }} + +); From d68df9d704d2fb637ba302c86e11107fe5e1953e Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 12:09:24 +0100 Subject: [PATCH 055/228] fix: Calculation issue with AutoSizer in explore --- public/app/features/explore/Graph.tsx | 28 ++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/public/app/features/explore/Graph.tsx b/public/app/features/explore/Graph.tsx index b087f6a457d..10006557349 100644 --- a/public/app/features/explore/Graph.tsx +++ b/public/app/features/explore/Graph.tsx @@ -265,17 +265,19 @@ export class Graph extends PureComponent { } export default (props: GraphProps) => ( - - {({width, height}) => { - return ( - - ); - }} - +
{/* div needed for AutoSizer to calculate, https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#observation */} + + {({width, height}) => ( +
+ {width > 0 && } +
+ )} +
+
); From 260b6f5de83133f668aa15a3874eeeb20d46cbe7 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 12:12:24 +0100 Subject: [PATCH 056/228] chore: Remove react-sizeme --- package.json | 1 - yarn.lock | 28 ---------------------------- 2 files changed, 29 deletions(-) diff --git a/package.json b/package.json index 77fd92baf57..18c0a56f0c4 100644 --- a/package.json +++ b/package.json @@ -177,7 +177,6 @@ "react-highlight-words": "0.11.0", "react-popper": "^1.3.0", "react-redux": "^5.0.7", - "react-sizeme": "^2.3.6", "react-table": "^6.8.6", "react-transition-group": "^2.2.1", "react-virtualized": "^9.21.0", diff --git a/yarn.lock b/yarn.lock index 169abd40ee4..fd0c446fbce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3972,11 +3972,6 @@ base@^0.11.1: mixin-deep "^1.2.0" pascalcase "^0.1.1" -batch-processor@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/batch-processor/-/batch-processor-1.0.0.tgz#75c95c32b748e0850d10c2b168f6bdbe9891ace8" - integrity sha1-dclcMrdI4IUNEMKxaPa9vpiRrOg= - batch@0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16" @@ -6613,13 +6608,6 @@ elegant-spinner@^1.0.1: resolved "https://registry.yarnpkg.com/elegant-spinner/-/elegant-spinner-1.0.1.tgz#db043521c95d7e303fd8f345bedc3349cfb0729e" integrity sha1-2wQ1IcldfjA/2PNFvtwzSc+wcp4= -element-resize-detector@^1.1.12: - version "1.2.0" - resolved "https://registry.yarnpkg.com/element-resize-detector/-/element-resize-detector-1.2.0.tgz#63344fd6f4e5ecff6f018d027e17b281fd4fa338" - integrity sha512-UmhNB8sIJVZeg56gEjgmMd6p37sCg8j8trVW0LZM7Wzv+kxQ5CnRHcgRKBTB/kFUSn3e7UP59kl2V2U8Du1hmg== - dependencies: - batch-processor "1.0.0" - elliptic@^6.0.0: version "6.4.1" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.1.tgz#c2d0b7776911b86722c632c3c06c60f2f819939a" @@ -10900,11 +10888,6 @@ lodash.tail@^4.1.1: resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= -lodash.throttle@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.throttle/-/lodash.throttle-4.1.1.tgz#c23e91b710242ac70c37f1e1cda9274cc39bf2f4" - integrity sha1-wj6RtxAkKscMN/HhzaknTMOb8vQ= - lodash.union@4.6.0, lodash.union@~4.6.0: version "4.6.0" resolved "https://registry.yarnpkg.com/lodash.union/-/lodash.union-4.6.0.tgz#48bb5088409f16f1821666641c44dd1aaae3cd88" @@ -14215,17 +14198,6 @@ react-resizable@1.x: prop-types "15.x" react-draggable "^2.2.6 || ^3.0.3" -react-sizeme@^2.3.6: - version "2.5.2" - resolved "https://registry.yarnpkg.com/react-sizeme/-/react-sizeme-2.5.2.tgz#e7041390cfb895ed15d896aa91d76e147e3b70b5" - integrity sha512-hYvcncV1FxVzPm2EhVwlOLf7Tk+k/ttO6rI7bfKUL/aL1gYzrY3DXJsdZ6nFaFgGSU/i8KC6gCoptOhBbRJpXQ== - dependencies: - element-resize-detector "^1.1.12" - invariant "^2.2.2" - lodash.debounce "^4.0.8" - lodash.throttle "^4.1.1" - shallowequal "^1.0.2" - react-split-pane@^0.1.84: version "0.1.85" resolved "https://registry.yarnpkg.com/react-split-pane/-/react-split-pane-0.1.85.tgz#64819946a99b617ffa2d20f6f45a0056b6ee4faa" From f5431f521082743d930f54d1d93f6dcaface7f7a Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 5 Feb 2019 13:48:00 +0100 Subject: [PATCH 057/228] chore: Explore: Remove inner AutoSizer, spread the size-object to width/height, change height type to number --- public/app/features/explore/Explore.tsx | 2 +- public/app/features/explore/Graph.tsx | 38 +++++-------------- .../app/features/explore/GraphContainer.tsx | 6 ++- public/app/features/explore/Logs.tsx | 2 +- 4 files changed, 15 insertions(+), 33 deletions(-) diff --git a/public/app/features/explore/Explore.tsx b/public/app/features/explore/Explore.tsx index b210bcccc18..437b50db63c 100644 --- a/public/app/features/explore/Explore.tsx +++ b/public/app/features/explore/Explore.tsx @@ -211,7 +211,7 @@ export class Explore extends React.PureComponent { {showingStartPage && } {!showingStartPage && ( <> - {supportsGraph && !supportsLogs && } + {supportsGraph && !supportsLogs && } {supportsTable && } {supportsLogs && ( ) => void; } -interface SizedGraphProps extends GraphProps { - size: { width: number; height: number }; -} - interface GraphState { /** * Type parameter refers to the `alias` property of a `TimeSeries`. @@ -98,7 +94,7 @@ interface GraphState { showAllTimeSeries: boolean; } -export class Graph extends PureComponent { +export class Graph extends PureComponent { $el: any; dynamicOptions = null; @@ -119,13 +115,13 @@ export class Graph extends PureComponent { this.$el.bind('plotselected', this.onPlotSelected); } - componentDidUpdate(prevProps: SizedGraphProps, prevState: GraphState) { + componentDidUpdate(prevProps: GraphProps, prevState: GraphState) { if ( prevProps.data !== this.props.data || prevProps.range !== this.props.range || prevProps.split !== this.props.split || prevProps.height !== this.props.height || - (prevProps.size && prevProps.size.width !== this.props.size.width) || + prevProps.width !== this.props.width || !equal(prevState.hiddenSeries, this.state.hiddenSeries) ) { this.draw(); @@ -147,8 +143,8 @@ export class Graph extends PureComponent { }; getDynamicOptions() { - const { range, size } = this.props; - const ticks = (size.width || 0) / 100; + const { range, width } = this.props; + const ticks = (width || 0) / 100; let { from, to } = range; if (!moment.isMoment(from)) { from = dateMath.parse(from, false); @@ -240,7 +236,7 @@ export class Graph extends PureComponent { } render() { - const { height = '100px', id = 'graph' } = this.props; + const { height = 100, id = 'graph' } = this.props; const { hiddenSeries } = this.state; const data = this.getGraphData(); @@ -264,20 +260,4 @@ export class Graph extends PureComponent { } } -export default (props: GraphProps) => ( -
{/* div needed for AutoSizer to calculate, https://github.com/bvaughn/react-virtualized/blob/master/docs/usingAutoSizer.md#observation */} - - {({width, height}) => ( -
- {width > 0 && } -
- )} -
-
-); +export default Graph; diff --git a/public/app/features/explore/GraphContainer.tsx b/public/app/features/explore/GraphContainer.tsx index 7263fd09288..3950d89c11f 100644 --- a/public/app/features/explore/GraphContainer.tsx +++ b/public/app/features/explore/GraphContainer.tsx @@ -20,6 +20,7 @@ interface GraphContainerProps { split: boolean; toggleGraph: typeof toggleGraph; changeTime: typeof changeTime; + width: number; } export class GraphContainer extends PureComponent { @@ -32,8 +33,8 @@ export class GraphContainer extends PureComponent { }; render() { - const { exploreId, graphResult, loading, showingGraph, showingTable, range, split } = this.props; - const graphHeight = showingGraph && showingTable ? '200px' : '400px'; + const { exploreId, graphResult, loading, showingGraph, showingTable, range, split, width } = this.props; + const graphHeight = showingGraph && showingTable ? 200 : 400; if (!graphResult) { return null; @@ -48,6 +49,7 @@ export class GraphContainer extends PureComponent { onChangeTime={this.onChangeTime} range={range} split={split} + width={width} /> ); diff --git a/public/app/features/explore/Logs.tsx b/public/app/features/explore/Logs.tsx index 490257cb9a9..b6c903bc504 100644 --- a/public/app/features/explore/Logs.tsx +++ b/public/app/features/explore/Logs.tsx @@ -214,7 +214,7 @@ export default class Logs extends PureComponent {
Date: Tue, 5 Feb 2019 14:09:25 +0100 Subject: [PATCH 058/228] fix: Update snapshot --- .../app/features/explore/__snapshots__/Graph.test.tsx.snap | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap index a7ec6deb22c..c38fb26a252 100644 --- a/public/app/features/explore/__snapshots__/Graph.test.tsx.snap +++ b/public/app/features/explore/__snapshots__/Graph.test.tsx.snap @@ -7,7 +7,7 @@ exports[`Render should render component 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> @@ -480,7 +480,7 @@ exports[`Render should render component with disclaimer 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> @@ -962,7 +962,7 @@ exports[`Render should show query return no time series 1`] = ` id="graph" style={ Object { - "height": "100px", + "height": 100, } } /> From 6d874dd1f160f8e3124dec7ed892cf0f3e54c82e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 14:42:29 +0100 Subject: [PATCH 059/228] Improved error handling --- public/app/core/copy/appNotification.ts | 26 ++++++++++++++----- .../features/dashboard/state/initDashboard.ts | 7 ++--- 2 files changed, 24 insertions(+), 9 deletions(-) diff --git a/public/app/core/copy/appNotification.ts b/public/app/core/copy/appNotification.ts index c34480d7aad..0062cd08fa6 100644 --- a/public/app/core/copy/appNotification.ts +++ b/public/app/core/copy/appNotification.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import { AppNotification, AppNotificationSeverity, AppNotificationTimeout } from 'app/types'; const defaultSuccessNotification: AppNotification = { @@ -31,12 +32,25 @@ export const createSuccessNotification = (title: string, text?: string): AppNoti id: Date.now(), }); -export const createErrorNotification = (title: string, text?: string): AppNotification => ({ - ...defaultErrorNotification, - title: title, - text: text, - id: Date.now(), -}); +export const createErrorNotification = (title: string, text?: any): AppNotification => { + // Handling if text is an error object + if (text && !_.isString(text)) { + if (text.message) { + text = text.message; + } else if (text.data && text.data.message) { + text = text.data.message; + } else { + text = text.toString(); + } + } + + return { + ...defaultErrorNotification, + title: title, + text: text, + id: Date.now(), + }; +}; export const createWarningNotification = (title: string, text?: string): AppNotification => ({ ...defaultWarningNotification, diff --git a/public/app/features/dashboard/state/initDashboard.ts b/public/app/features/dashboard/state/initDashboard.ts index d529ca0b531..b8eed6c4e64 100644 --- a/public/app/features/dashboard/state/initDashboard.ts +++ b/public/app/features/dashboard/state/initDashboard.ts @@ -81,7 +81,6 @@ export function initDashboard({ try { switch (routeInfo) { - // handle old urls with no uid case DashboardRouteInfo.Home: { // load home dash dashDTO = await getBackendSrv().get('/api/dashboards/home'); @@ -130,6 +129,7 @@ export function initDashboard({ } } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + dispatch(notifyApp(createErrorNotification('Dashboard fetch failed', err))); console.log(err); return; } @@ -143,6 +143,7 @@ export function initDashboard({ dashboard = new DashboardModel(dashDTO.dashboard, dashDTO.meta); } catch (err) { dispatch(setDashboardLoadingState(DashboardLoadingState.Error)); + dispatch(notifyApp(createErrorNotification('Dashboard model initializing failure', err))); console.log(err); return; } @@ -168,7 +169,7 @@ export function initDashboard({ try { await variableSrv.init(dashboard); } catch (err) { - dispatch(notifyApp(createErrorNotification('Templating init failed'))); + dispatch(notifyApp(createErrorNotification('Templating init failed', err))); console.log(err); } @@ -194,7 +195,7 @@ export function initDashboard({ keybindingSrv.setupDashboardBindings($scope, dashboard, onRemovePanel); } catch (err) { - dispatch(notifyApp(createErrorNotification('Dashboard init failed', err.toString()))); + dispatch(notifyApp(createErrorNotification('Dashboard init failed', err))); console.log(err); } From a624c9713aac9e1132099223e07e7b06ab0223ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Feb 2019 15:09:56 +0100 Subject: [PATCH 060/228] Removed unused controllers and services --- public/app/core/components/gf_page.ts | 40 ----- .../app/core/components/scroll/page_scroll.ts | 43 ----- public/app/core/core.ts | 4 - .../dashboard/containers/DashboardCtrl.ts | 150 ----------------- public/app/features/dashboard/index.ts | 2 - .../services/DashboardViewStateSrv.ts | 155 ------------------ public/app/partials/dashboard.html | 17 -- public/views/index-template.html | 2 +- 8 files changed, 1 insertion(+), 412 deletions(-) delete mode 100644 public/app/core/components/gf_page.ts delete mode 100644 public/app/core/components/scroll/page_scroll.ts delete mode 100644 public/app/features/dashboard/containers/DashboardCtrl.ts delete mode 100644 public/app/features/dashboard/services/DashboardViewStateSrv.ts delete mode 100644 public/app/partials/dashboard.html diff --git a/public/app/core/components/gf_page.ts b/public/app/core/components/gf_page.ts deleted file mode 100644 index 057a307f205..00000000000 --- a/public/app/core/components/gf_page.ts +++ /dev/null @@ -1,40 +0,0 @@ -import coreModule from 'app/core/core_module'; - -const template = ` -
- -
- - -
-
-
-
-`; - -export function gfPageDirective() { - return { - restrict: 'E', - template: template, - scope: { - model: '=', - }, - transclude: { - header: '?gfPageHeader', - body: 'gfPageBody', - }, - link: (scope, elem, attrs) => { - console.log(scope); - }, - }; -} - -coreModule.directive('gfPage', gfPageDirective); diff --git a/public/app/core/components/scroll/page_scroll.ts b/public/app/core/components/scroll/page_scroll.ts deleted file mode 100644 index 2d6e27f8b22..00000000000 --- a/public/app/core/components/scroll/page_scroll.ts +++ /dev/null @@ -1,43 +0,0 @@ -import coreModule from 'app/core/core_module'; -import appEvents from 'app/core/app_events'; - -export function pageScrollbar() { - return { - restrict: 'A', - link: (scope, elem, attrs) => { - let lastPos = 0; - - appEvents.on( - 'dash-scroll', - evt => { - if (evt.restore) { - elem[0].scrollTop = lastPos; - return; - } - - lastPos = elem[0].scrollTop; - - if (evt.animate) { - elem.animate({ scrollTop: evt.pos }, 500); - } else { - elem[0].scrollTop = evt.pos; - } - }, - scope - ); - - scope.$on('$routeChangeSuccess', () => { - lastPos = 0; - elem[0].scrollTop = 0; - // Focus page to enable scrolling by keyboard - elem[0].focus({ preventScroll: true }); - }); - - elem[0].tabIndex = -1; - // Focus page to enable scrolling by keyboard - elem[0].focus({ preventScroll: true }); - }, - }; -} - -coreModule.directive('pageScrollbar', pageScrollbar); diff --git a/public/app/core/core.ts b/public/app/core/core.ts index fb38cefd435..1f289fc4b27 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -43,8 +43,6 @@ import { helpModal } from './components/help/help'; import { JsonExplorer } from './components/json_explorer/json_explorer'; import { NavModelSrv, NavModel } from './nav_model_srv'; import { geminiScrollbar } from './components/scroll/scroll'; -import { pageScrollbar } from './components/scroll/page_scroll'; -import { gfPageDirective } from './components/gf_page'; import { orgSwitcher } from './components/org_switcher'; import { profiler } from './profiler'; import { registerAngularDirectives } from './angular_wrappers'; @@ -79,8 +77,6 @@ export { NavModelSrv, NavModel, geminiScrollbar, - pageScrollbar, - gfPageDirective, orgSwitcher, manageDashboardsDirective, TimeSeries, diff --git a/public/app/features/dashboard/containers/DashboardCtrl.ts b/public/app/features/dashboard/containers/DashboardCtrl.ts deleted file mode 100644 index 0151f8f7331..00000000000 --- a/public/app/features/dashboard/containers/DashboardCtrl.ts +++ /dev/null @@ -1,150 +0,0 @@ -// Utils -import config from 'app/core/config'; -import appEvents from 'app/core/app_events'; -import coreModule from 'app/core/core_module'; -import { removePanel } from 'app/features/dashboard/utils/panel'; - -// Services -import { AnnotationsSrv } from '../../annotations/annotations_srv'; - -// Types -import { DashboardModel } from '../state/DashboardModel'; - -export class DashboardCtrl { - dashboard: DashboardModel; - dashboardViewState: any; - loadedFallbackDashboard: boolean; - editTab: number; - - /** @ngInject */ - constructor( - private $scope, - private keybindingSrv, - private timeSrv, - private variableSrv, - private dashboardSrv, - private unsavedChangesSrv, - private dashboardViewStateSrv, - private annotationsSrv: AnnotationsSrv, - public playlistSrv - ) { - // temp hack due to way dashboards are loaded - // can't use controllerAs on route yet - $scope.ctrl = this; - } - - setupDashboard(data) { - try { - this.setupDashboardInternal(data); - } catch (err) { - this.onInitFailed(err, 'Dashboard init failed', true); - } - } - - setupDashboardInternal(data) { - const dashboard = this.dashboardSrv.create(data.dashboard, data.meta); - this.dashboardSrv.setCurrent(dashboard); - - // init services - this.timeSrv.init(dashboard); - this.annotationsSrv.init(dashboard); - - // template values service needs to initialize completely before - // the rest of the dashboard can load - this.variableSrv - .init(dashboard) - // template values failes are non fatal - .catch(this.onInitFailed.bind(this, 'Templating init failed', false)) - // continue - .finally(() => { - this.dashboard = dashboard; - this.dashboard.processRepeats(); - this.dashboard.updateSubmenuVisibility(); - this.dashboard.autoFitPanels(window.innerHeight); - - this.unsavedChangesSrv.init(dashboard, this.$scope); - - // TODO refactor ViewStateSrv - this.$scope.dashboard = dashboard; - this.dashboardViewState = this.dashboardViewStateSrv.create(this.$scope); - - this.keybindingSrv.setupDashboardBindings(this.$scope, dashboard); - this.setWindowTitleAndTheme(); - - appEvents.emit('dashboard-initialized', dashboard); - }) - .catch(this.onInitFailed.bind(this, 'Dashboard init failed', true)); - } - - onInitFailed(msg, fatal, err) { - console.log(msg, err); - - if (err.data && err.data.message) { - err.message = err.data.message; - } else if (!err.message) { - err = { message: err.toString() }; - } - - this.$scope.appEvent('alert-error', [msg, err.message]); - - // protect against recursive fallbacks - if (fatal && !this.loadedFallbackDashboard) { - this.loadedFallbackDashboard = true; - this.setupDashboard({ dashboard: { title: 'Dashboard Init failed' } }); - } - } - - templateVariableUpdated() { - this.dashboard.processRepeats(); - } - - setWindowTitleAndTheme() { - window.document.title = config.windowTitlePrefix + this.dashboard.title; - } - - showJsonEditor(evt, options) { - const model = { - object: options.object, - updateHandler: options.updateHandler, - }; - - this.$scope.appEvent('show-dash-editor', { - src: 'public/app/partials/edit_json.html', - model: model, - }); - } - - getDashboard() { - return this.dashboard; - } - - getPanelContainer() { - return this; - } - - onRemovingPanel(evt, options) { - options = options || {}; - if (!options.panelId) { - return; - } - - const panelInfo = this.dashboard.getPanelInfoById(options.panelId); - removePanel(this.dashboard, panelInfo.panel, true); - } - - onDestroy() { - if (this.dashboard) { - this.dashboard.destroy(); - } - } - - init(dashboard) { - this.$scope.onAppEvent('show-json-editor', this.showJsonEditor.bind(this)); - this.$scope.onAppEvent('template-variable-value-updated', this.templateVariableUpdated.bind(this)); - this.$scope.onAppEvent('panel-remove', this.onRemovingPanel.bind(this)); - this.$scope.$on('$destroy', this.onDestroy.bind(this)); - this.setupDashboard(dashboard); - } -} - -coreModule.controller('DashboardCtrl', DashboardCtrl); diff --git a/public/app/features/dashboard/index.ts b/public/app/features/dashboard/index.ts index 9f2935660ef..d9a03b0aad6 100644 --- a/public/app/features/dashboard/index.ts +++ b/public/app/features/dashboard/index.ts @@ -1,8 +1,6 @@ -import './containers/DashboardCtrl'; import './dashgrid/DashboardGridDirective'; // Services -import './services/DashboardViewStateSrv'; import './services/UnsavedChangesSrv'; import './services/DashboardLoaderSrv'; import './services/DashboardSrv'; diff --git a/public/app/features/dashboard/services/DashboardViewStateSrv.ts b/public/app/features/dashboard/services/DashboardViewStateSrv.ts deleted file mode 100644 index 7cb4c1de7ab..00000000000 --- a/public/app/features/dashboard/services/DashboardViewStateSrv.ts +++ /dev/null @@ -1,155 +0,0 @@ -import angular from 'angular'; -import _ from 'lodash'; -import config from 'app/core/config'; -import appEvents from 'app/core/app_events'; -import { DashboardModel } from '../state/DashboardModel'; - -// represents the transient view state -// like fullscreen panel & edit -export class DashboardViewStateSrv { - state: any; - panelScopes: any; - $scope: any; - dashboard: DashboardModel; - fullscreenPanel: any; - oldTimeRange: any; - - /** @ngInject */ - constructor($scope, private $location, private $timeout) { - const self = this; - self.state = {}; - self.panelScopes = []; - self.$scope = $scope; - self.dashboard = $scope.dashboard; - - $scope.onAppEvent('$routeUpdate', () => { - // const urlState = self.getQueryStringState(); - // if (self.needsSync(urlState)) { - // self.update(urlState, true); - // } - }); - - $scope.onAppEvent('panel-change-view', (evt, payload) => { - // self.update(payload); - }); - - // this marks changes to location during this digest cycle as not to add history item - // don't want url changes like adding orgId to add browser history - // $location.replace(); - // this.update(this.getQueryStringState()); - } - - needsSync(urlState) { - return _.isEqual(this.state, urlState) === false; - } - - getQueryStringState() { - const state = this.$location.search(); - state.panelId = parseInt(state.panelId, 10) || null; - state.fullscreen = state.fullscreen ? true : null; - state.edit = state.edit === 'true' || state.edit === true || null; - state.editview = state.editview || null; - state.orgId = config.bootData.user.orgId; - return state; - } - - serializeToUrl() { - const urlState = _.clone(this.state); - urlState.fullscreen = this.state.fullscreen ? true : null; - urlState.edit = this.state.edit ? true : null; - return urlState; - } - - update(state, fromRouteUpdated?) { - // implement toggle logic - if (state.toggle) { - delete state.toggle; - if (this.state.fullscreen && state.fullscreen) { - if (this.state.edit === state.edit) { - state.fullscreen = !state.fullscreen; - } - } - } - - _.extend(this.state, state); - - if (!this.state.fullscreen) { - this.state.fullscreen = null; - this.state.edit = null; - // clear panel id unless in solo mode - if (!this.dashboard.meta.soloMode) { - this.state.panelId = null; - } - } - - if ((this.state.fullscreen || this.dashboard.meta.soloMode) && this.state.panelId) { - // Trying to render panel in fullscreen when it's in the collapsed row causes an issue. - // So in this case expand collapsed row first. - this.toggleCollapsedPanelRow(this.state.panelId); - } - - // if no edit state cleanup tab parm - if (!this.state.edit) { - delete this.state.tab; - } - - // do not update url params if we are here - // from routeUpdated event - if (fromRouteUpdated !== true) { - this.$location.search(this.serializeToUrl()); - } - } - - toggleCollapsedPanelRow(panelId) { - for (const panel of this.dashboard.panels) { - if (panel.collapsed) { - for (const rowPanel of panel.panels) { - if (rowPanel.id === panelId) { - this.dashboard.toggleRow(panel); - return; - } - } - } - } - } - - leaveFullscreen() { - const panel = this.fullscreenPanel; - - this.dashboard.setViewMode(panel, false, false); - - delete this.fullscreenPanel; - - this.$timeout(() => { - appEvents.emit('dash-scroll', { restore: true }); - - if (this.oldTimeRange !== this.dashboard.time) { - this.dashboard.startRefresh(); - } else { - this.dashboard.render(); - } - }); - } - - enterFullscreen(panel) { - const isEditing = this.state.edit && this.dashboard.meta.canEdit; - - this.oldTimeRange = this.dashboard.time; - this.fullscreenPanel = panel; - - // Firefox doesn't return scrollTop position properly if 'dash-scroll' is emitted after setViewMode() - this.$scope.appEvent('dash-scroll', { animate: false, pos: 0 }); - this.dashboard.setViewMode(panel, true, isEditing); - } -} - -/** @ngInject */ -export function dashboardViewStateSrv($location, $timeout) { - return { - create: $scope => { - return new DashboardViewStateSrv($scope, $location, $timeout); - }, - }; -} - -angular.module('grafana.services').factory('dashboardViewStateSrv', dashboardViewStateSrv); diff --git a/public/app/partials/dashboard.html b/public/app/partials/dashboard.html deleted file mode 100644 index 32acdc435f2..00000000000 --- a/public/app/partials/dashboard.html +++ /dev/null @@ -1,17 +0,0 @@ -
- - -
- - - -
- - - - -
-
-
diff --git a/public/views/index-template.html b/public/views/index-template.html index 770ab74eccc..895b0e4ae19 100644 --- a/public/views/index-template.html +++ b/public/views/index-template.html @@ -192,7 +192,7 @@
-
+
-
+ ); } diff --git a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap index f40894426ae..9a9daab76c3 100644 --- a/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap +++ b/public/app/features/api-keys/__snapshots__/ApiKeysPage.test.tsx.snap @@ -35,118 +35,114 @@ exports[`Render should render CTA if there are no API keys 1`] = ` -
- - + +
-
- -
- Add API Key -
-
+ +
+ Add API Key +
+ +
-
- - Key name - - + +
+
+ + Role + + + - - - - - -
-
- -
+ Viewer + + + + +
- -
- -
+
+ +
+
+ +
+
`; diff --git a/public/app/features/org/OrgDetailsPage.tsx b/public/app/features/org/OrgDetailsPage.tsx index ee644f0006f..236558db40a 100644 --- a/public/app/features/org/OrgDetailsPage.tsx +++ b/public/app/features/org/OrgDetailsPage.tsx @@ -36,18 +36,16 @@ export class OrgDetailsPage extends PureComponent { return ( - @@ -407,6 +410,7 @@ exports[`Render should render is ready only message 1`] = ` isReadOnly={true} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} />
- {!isLoading && ( -
- this.onOrgNameChange(name)} - onSubmit={this.onUpdateOrganization} - orgName={organization.name} - /> - -
- )} + {!isLoading && ( +
+ this.onOrgNameChange(name)} + onSubmit={this.onUpdateOrganization} + orgName={organization.name} + /> +
+ )} ); diff --git a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap index 9e13a73901e..2339975ca8b 100644 --- a/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap +++ b/public/app/features/org/__snapshots__/OrgDetailsPage.test.tsx.snap @@ -15,11 +15,7 @@ exports[`Render should render component 1`] = ` > -
- + /> `; @@ -39,19 +35,15 @@ exports[`Render should render organization and preferences 1`] = ` -
-
- - -
+
+ +
From 93f1a48641b9e9219fa5f8869757fcdb4d1187ae Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 11 Feb 2019 15:21:02 +0100 Subject: [PATCH 202/228] changelog: adds note for #14623 --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6221b7bcc93..a82fc7050b4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,8 @@ # 6.0.0-beta2 (unreleased) +### New Features +* **AzureMonitor**: Enable alerting by converting Azure Monitor API to Go [#14623](https://github.com/grafana/grafana/issues/14623) + ### Minor * **Alerting**: Adds support for images in pushover notifier [#10780](https://github.com/grafana/grafana/issues/10780), thx [@jpenalbae](https://github.com/jpenalbae) * **Graphite/InfluxDB/OpenTSDB**: Fix always take dashboard timezone into consideration when handle custom time ranges [#15284](https://github.com/grafana/grafana/issues/15284) From c4fa64e6dc082bdb813edb13f34652f4163b9cfe Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 15:23:17 +0100 Subject: [PATCH 203/228] Updated lint-staged --- package.json | 2 +- yarn.lock | 286 ++++++++++++++++++++++----------------------------- 2 files changed, 122 insertions(+), 166 deletions(-) diff --git a/package.json b/package.json index fae51a1d856..2f44291a86a 100644 --- a/package.json +++ b/package.json @@ -68,7 +68,7 @@ "husky": "^0.14.3", "jest": "^23.6.0", "jest-date-mock": "^1.0.6", - "lint-staged": "^6.0.0", + "lint-staged": "^8.1.3", "load-grunt-tasks": "3.5.2", "mini-css-extract-plugin": "^0.4.0", "mocha": "^4.0.1", diff --git a/yarn.lock b/yarn.lock index df2e1cea37e..2fb4a5d3ee2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1040,6 +1040,20 @@ resolved "https://registry.yarnpkg.com/@emotion/weak-memoize/-/weak-memoize-0.1.3.tgz#b700d97385fa91affed60c71dfd51c67e9dad762" integrity sha512-QsYGKdhhuDFNq7bjm2r44y0mp5xW3uO3csuTPDWZc0OIiMQv+AIY5Cqwd4mJiC5N8estVl7qlvOx1hbtOuUWbw== +"@iamstarkov/listr-update-renderer@0.4.1": + version "0.4.1" + resolved "https://registry.yarnpkg.com/@iamstarkov/listr-update-renderer/-/listr-update-renderer-0.4.1.tgz#d7c48092a2dcf90fd672b6c8b458649cb350c77e" + integrity sha512-IJyxQWsYDEkf8C8QthBn5N8tIUR9V9je6j3sMIpAkonaadjbvxmRC6RAhpa3RKxndhNnU2M6iNbtJwd7usQYIA== + dependencies: + chalk "^1.1.3" + cli-truncate "^0.2.1" + elegant-spinner "^1.0.1" + figures "^1.7.0" + indent-string "^3.0.0" + log-symbols "^1.0.2" + log-update "^2.3.0" + strip-ansi "^3.0.1" + "@icons/material@^0.2.4": version "0.2.4" resolved "https://registry.yarnpkg.com/@icons/material/-/material-0.2.4.tgz#e90c9f71768b3736e76d7dd6783fc6c2afa88bc8" @@ -2468,7 +2482,7 @@ ansi-colors@^3.0.0: resolved "https://registry.yarnpkg.com/ansi-colors/-/ansi-colors-3.2.3.tgz#57d35b8686e851e2cc04c403f1c00203976a1813" integrity sha512-LEHHyuhlPY3TmuUYMh2oz89lTShfvgbmzaBcxve9t/9Wuy7Dwf4yoAKcND7KFT1HAQfqZ12qtc+DUrBMeKF9nw== -ansi-escapes@^1.0.0, ansi-escapes@^1.1.0: +ansi-escapes@^1.1.0: version "1.4.0" resolved "https://registry.yarnpkg.com/ansi-escapes/-/ansi-escapes-1.4.0.tgz#d3a8a83b319aa67793662b13e761c7911422306e" integrity sha1-06ioOzGapneTZisT52HHkRQiMG4= @@ -2525,11 +2539,6 @@ ansistyles@~0.1.3: resolved "https://registry.yarnpkg.com/ansistyles/-/ansistyles-0.1.3.tgz#5de60415bda071bb37127854c864f41b23254539" integrity sha1-XeYEFb2gcbs3EnhUyGT0GyMlRTk= -any-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.2.0.tgz#c67870058003579009083f54ac0abafb5c33d242" - integrity sha1-xnhwBYADV5AJCD9UrAq6+1wz0kI= - any-observable@^0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/any-observable/-/any-observable-0.3.0.tgz#af933475e5806a67d0d7df090dd5e8bef65d119b" @@ -2548,11 +2557,6 @@ app-root-dir@^1.0.2: resolved "https://registry.yarnpkg.com/app-root-dir/-/app-root-dir-1.0.2.tgz#38187ec2dea7577fff033ffcb12172692ff6e118" integrity sha1-OBh+wt6nV3//Az/8sSFyaS/24Rg= -app-root-path@^2.0.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/app-root-path/-/app-root-path-2.1.0.tgz#98bf6599327ecea199309866e8140368fd2e646a" - integrity sha1-mL9lmTJ+zqGZMJhm6BQDaP0uZGo= - append-transform@^0.4.0: version "0.4.0" resolved "https://registry.yarnpkg.com/append-transform/-/append-transform-0.4.0.tgz#d76ebf8ca94d276e247a36bad44a4b74ab611991" @@ -4588,7 +4592,7 @@ chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3, chalk@~1.1.1: strip-ansi "^3.0.0" supports-color "^2.0.0" -chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: +chalk@^2.0.0, chalk@^2.0.1, chalk@^2.1.0, chalk@^2.3.0, chalk@^2.3.1, chalk@^2.3.2, chalk@^2.4.1, chalk@^2.4.2: version "2.4.2" resolved "https://registry.yarnpkg.com/chalk/-/chalk-2.4.2.tgz#cd42541677a54333cf541a49108c1432b44c9424" integrity sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ== @@ -4783,7 +4787,7 @@ cli-columns@^3.1.2: string-width "^2.0.0" strip-ansi "^3.0.1" -cli-cursor@^1.0.1, cli-cursor@^1.0.2: +cli-cursor@^1.0.1: version "1.0.2" resolved "https://registry.yarnpkg.com/cli-cursor/-/cli-cursor-1.0.2.tgz#64da3f7d56a54412e59794bd62dc35295e8f2987" integrity sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc= @@ -4797,11 +4801,6 @@ cli-cursor@^2.0.0, cli-cursor@^2.1.0: dependencies: restore-cursor "^2.0.0" -cli-spinners@^0.1.2: - version "0.1.2" - resolved "https://registry.yarnpkg.com/cli-spinners/-/cli-spinners-0.1.2.tgz#bb764d88e185fb9e1e6a2a1f19772318f605e31c" - integrity sha1-u3ZNiOGF+54eaiofGXcjGPYF4xw= - cli-table2@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/cli-table2/-/cli-table2-0.2.0.tgz#2d1ef7f218a0e786e214540562d4bd177fe32d97" @@ -5075,7 +5074,7 @@ comma-separated-tokens@^1.0.0: dependencies: trim "0.0.1" -commander@2, commander@^2.11.0, commander@^2.12.1, commander@^2.13.0, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0: +commander@2, commander@^2.12.1, commander@^2.13.0, commander@^2.14.1, commander@^2.19.0, commander@^2.8.1, commander@^2.9.0: version "2.19.0" resolved "https://registry.yarnpkg.com/commander/-/commander-2.19.0.tgz#f6198aa84e5b83c46054b94ddedbfed5ee9ff12a" integrity sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg== @@ -5312,7 +5311,7 @@ cosmiconfig@^4.0.0: parse-json "^4.0.0" require-from-string "^2.0.1" -cosmiconfig@^5.0.5, cosmiconfig@^5.0.7: +cosmiconfig@^5.0.2, cosmiconfig@^5.0.5, cosmiconfig@^5.0.7: version "5.0.7" resolved "https://registry.yarnpkg.com/cosmiconfig/-/cosmiconfig-5.0.7.tgz#39826b292ee0d78eda137dfa3173bd1c21a43b04" integrity sha512-PcLqxTKiDmNT6pSpy4N6KtuPwb53W+2tzNvwOZw0WH9N6O0vLIBq0x8aj8Oj75ere4YcGi48bDFCL+3fRJdlNA== @@ -6085,7 +6084,7 @@ debug@^3.1.0, debug@^3.2.5: dependencies: ms "^2.1.1" -debug@^4.1.0: +debug@^4.0.1, debug@^4.1.0: version "4.1.1" resolved "https://registry.yarnpkg.com/debug/-/debug-4.1.1.tgz#3b72260255109c6b589cee050f1d516139664791" integrity sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw== @@ -6915,7 +6914,7 @@ escape-html@^1.0.3, escape-html@~1.0.3: resolved "https://registry.yarnpkg.com/escape-html/-/escape-html-1.0.3.tgz#0258eae4d3d0c0974de1c169188ef0051d1d1988" integrity sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg= -escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.5: +escape-string-regexp@1.0.5, escape-string-regexp@^1.0.2, escape-string-regexp@^1.0.4, escape-string-regexp@^1.0.5: version "1.0.5" resolved "https://registry.yarnpkg.com/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz#1b61c0562190a8dff6ae3bb2cf0200ca130b86d4" integrity sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ= @@ -7129,19 +7128,6 @@ execa@^0.7.0: signal-exit "^3.0.0" strip-eof "^1.0.0" -execa@^0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/execa/-/execa-0.8.0.tgz#d8d76bbc1b55217ed190fd6dd49d3c774ecfc8da" - integrity sha1-2NdrvBtVIX7RkP1t1J08d07PyNo= - dependencies: - cross-spawn "^5.0.1" - get-stream "^3.0.0" - is-stream "^1.1.0" - npm-run-path "^2.0.0" - p-finally "^1.0.0" - signal-exit "^3.0.0" - strip-eof "^1.0.0" - execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" @@ -7631,6 +7617,11 @@ flush-write-stream@^1.0.0: inherits "^2.0.1" readable-stream "^2.0.4" +fn-name@~2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/fn-name/-/fn-name-2.0.1.tgz#5214d7537a4d06a4a301c0cc262feb84188002e7" + integrity sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc= + follow-redirects@^1.0.0, follow-redirects@^1.2.5: version "1.6.1" resolved "https://registry.yarnpkg.com/follow-redirects/-/follow-redirects-1.6.1.tgz#514973c44b5757368bad8bddfe52f81f015c94cb" @@ -7848,6 +7839,15 @@ fuse.js@^3.0.1, fuse.js@^3.3.0: resolved "https://registry.yarnpkg.com/fuse.js/-/fuse.js-3.3.0.tgz#1e4fe172a60687230fb54a5cb247eb96e2e7e885" integrity sha512-ESBRkGLWMuVkapqYCcNO1uqMg5qbCKkgb+VS6wsy17Rix0/cMS9kSOZoYkjH8Ko//pgJ/EEGu0GTjk2mjX2LGQ== +g-status@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/g-status/-/g-status-2.0.2.tgz#270fd32119e8fc9496f066fe5fe88e0a6bc78b97" + integrity sha512-kQoE9qH+T1AHKgSSD0Hkv98bobE90ILQcXAF4wvGgsr7uFqNvwmh8j+Lq3l0RVt3E3HjSbv2B9biEGcEtpHLCA== + dependencies: + arrify "^1.0.1" + matcher "^1.0.0" + simple-git "^1.85.0" + gauge@~2.7.3: version "2.7.4" resolved "https://registry.yarnpkg.com/gauge/-/gauge-2.7.4.tgz#2c03405c7538c39d7eb37b317022e325fb018bf7" @@ -9497,13 +9497,6 @@ is-object@^1.0.1: resolved "https://registry.yarnpkg.com/is-object/-/is-object-1.0.1.tgz#8952688c5ec2ffd6b03ecc85e769e02903083470" integrity sha1-iVJojF7C/9awPsyF52ngKQMINHA= -is-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-0.2.0.tgz#b361311d83c6e5d726cabf5e250b0237106f5ae2" - integrity sha1-s2ExHYPG5dcmyr9eJQsCNxBvWuI= - dependencies: - symbol-observable "^0.2.2" - is-observable@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/is-observable/-/is-observable-1.1.0.tgz#b3e986c8f44de950867cab5403f5a3465005975e" @@ -9917,11 +9910,6 @@ jest-environment-node@^23.4.0: jest-mock "^23.2.0" jest-util "^23.4.0" -jest-get-type@^21.2.0: - version "21.2.0" - resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-21.2.0.tgz#f6376ab9db4b60d81e39f30749c6c466f40d4a23" - integrity sha512-y2fFw3C+D0yjNSDp7ab1kcd6NUYfy3waPTlD8yWkAtiocJdBRQqNoRqVfMNxgj+IjT0V5cBIHJO0z9vuSSZ43Q== - jest-get-type@^22.1.0: version "22.4.3" resolved "https://registry.yarnpkg.com/jest-get-type/-/jest-get-type-22.4.3.tgz#e3a8504d8479342dd4420236b322869f18900ce4" @@ -10094,16 +10082,6 @@ jest-util@^23.4.0: slash "^1.0.0" source-map "^0.6.0" -jest-validate@^21.1.0: - version "21.2.1" - resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-21.2.1.tgz#cc0cbca653cd54937ba4f2a111796774530dd3c7" - integrity sha512-k4HLI1rZQjlU+EC682RlQ6oZvLrE5SCh3brseQc24vbZTxzT/k/3urar5QMCVgjadmSO7lECeGdc6YxnM3yEGg== - dependencies: - chalk "^2.0.1" - jest-get-type "^21.2.0" - leven "^2.1.0" - pretty-format "^21.2.1" - jest-validate@^23.6.0: version "23.6.0" resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-23.6.0.tgz#36761f99d1ed33fcd425b4e4c5595d62b6597474" @@ -10566,51 +10544,42 @@ libnpx@^10.2.0: y18n "^4.0.0" yargs "^11.0.0" -lint-staged@^6.0.0: - version "6.1.1" - resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-6.1.1.tgz#cd08c4d9b8ccc2d37198d1c47ce77d22be6cf324" - integrity sha512-M/7bwLdXbeG7ZNLcasGeLMBDg60/w6obj3KOtINwJyxAxb53XGY0yH5FSZlWklEzuVbTtqtIfAajh6jYIN90AA== +lint-staged@^8.1.3: + version "8.1.3" + resolved "https://registry.yarnpkg.com/lint-staged/-/lint-staged-8.1.3.tgz#bb069db5466c0fe16710216e633a84f2b362fa60" + integrity sha512-6TGkikL1B+6mIOuSNq2TV6oP21IhPMnV8q0cf9oYZ296ArTVNcbFh1l1pfVOHHbBIYLlziWNsQ2q45/ffmJ4AA== dependencies: - app-root-path "^2.0.0" - chalk "^2.1.0" - commander "^2.11.0" - cosmiconfig "^4.0.0" + "@iamstarkov/listr-update-renderer" "0.4.1" + chalk "^2.3.1" + commander "^2.14.1" + cosmiconfig "^5.0.2" debug "^3.1.0" dedent "^0.7.0" - execa "^0.8.0" + del "^3.0.0" + execa "^1.0.0" find-parent-dir "^0.3.0" + g-status "^2.0.2" is-glob "^4.0.0" - jest-validate "^21.1.0" - listr "^0.13.0" - lodash "^4.17.4" - log-symbols "^2.0.0" - minimatch "^3.0.0" + is-windows "^1.0.2" + listr "^0.14.2" + lodash "^4.17.5" + log-symbols "^2.2.0" + micromatch "^3.1.8" npm-which "^3.0.1" p-map "^1.1.1" path-is-inside "^1.0.2" pify "^3.0.0" - staged-git-files "1.0.0" - stringify-object "^3.2.0" + please-upgrade-node "^3.0.2" + staged-git-files "1.1.2" + string-argv "^0.0.2" + stringify-object "^3.2.2" + yup "^0.26.10" listr-silent-renderer@^1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/listr-silent-renderer/-/listr-silent-renderer-1.1.1.tgz#924b5a3757153770bf1a8e3fbf74b8bbf3f9242e" integrity sha1-kktaN1cVN3C/Go4/v3S4u/P5JC4= -listr-update-renderer@^0.4.0: - version "0.4.0" - resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.4.0.tgz#344d980da2ca2e8b145ba305908f32ae3f4cc8a7" - integrity sha1-NE2YDaLKLosUW6MFkI8yrj9MyKc= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - elegant-spinner "^1.0.1" - figures "^1.7.0" - indent-string "^3.0.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - strip-ansi "^3.0.1" - listr-update-renderer@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/listr-update-renderer/-/listr-update-renderer-0.5.0.tgz#4ea8368548a7b8aecb7e06d8c95cb45ae2ede6a2" @@ -10625,16 +10594,6 @@ listr-update-renderer@^0.5.0: log-update "^2.3.0" strip-ansi "^3.0.1" -listr-verbose-renderer@^0.4.0: - version "0.4.1" - resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.4.1.tgz#8206f4cf6d52ddc5827e5fd14989e0e965933a35" - integrity sha1-ggb0z21S3cWCfl/RSYng6WWTOjU= - dependencies: - chalk "^1.1.3" - cli-cursor "^1.0.2" - date-fns "^1.27.2" - figures "^1.7.0" - listr-verbose-renderer@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/listr-verbose-renderer/-/listr-verbose-renderer-0.5.0.tgz#f1132167535ea4c1261102b9f28dac7cba1e03db" @@ -10645,30 +10604,7 @@ listr-verbose-renderer@^0.5.0: date-fns "^1.27.2" figures "^2.0.0" -listr@^0.13.0: - version "0.13.0" - resolved "https://registry.yarnpkg.com/listr/-/listr-0.13.0.tgz#20bb0ba30bae660ee84cc0503df4be3d5623887d" - integrity sha1-ILsLowuuZg7oTMBQPfS+PVYjiH0= - dependencies: - chalk "^1.1.3" - cli-truncate "^0.2.1" - figures "^1.7.0" - indent-string "^2.1.0" - is-observable "^0.2.0" - is-promise "^2.1.0" - is-stream "^1.1.0" - listr-silent-renderer "^1.1.1" - listr-update-renderer "^0.4.0" - listr-verbose-renderer "^0.4.0" - log-symbols "^1.0.2" - log-update "^1.0.2" - ora "^0.2.3" - p-map "^1.1.1" - rxjs "^5.4.2" - stream-to-observable "^0.2.0" - strip-ansi "^3.0.1" - -listr@^0.14.1: +listr@^0.14.1, listr@^0.14.2: version "0.14.3" resolved "https://registry.yarnpkg.com/listr/-/listr-0.14.3.tgz#2fea909604e434be464c50bddba0d496928fa586" integrity sha512-RmAl7su35BFd/xoMamRjpIE4j3v+L28o8CT5YhAXQJm1fD+1l9ngXY8JAQRJ+tFK2i5njvi0iRUKV09vPwA0iA== @@ -10949,14 +10885,6 @@ log-symbols@^2.0.0, log-symbols@^2.1.0, log-symbols@^2.2.0: dependencies: chalk "^2.0.1" -log-update@^1.0.2: - version "1.0.2" - resolved "https://registry.yarnpkg.com/log-update/-/log-update-1.0.2.tgz#19929f64c4093d2d2e7075a1dad8af59c296b8d1" - integrity sha1-GZKfZMQJPS0ucHWh2tivWcKWuNE= - dependencies: - ansi-escapes "^1.0.0" - cli-cursor "^1.0.2" - log-update@^2.3.0: version "2.3.0" resolved "https://registry.yarnpkg.com/log-update/-/log-update-2.3.0.tgz#88328fd7d1ce7938b29283746f0b1bc126b24708" @@ -11154,6 +11082,13 @@ marksy@^6.1.0: he "^1.1.1" marked "^0.3.12" +matcher@^1.0.0: + version "1.1.1" + resolved "https://registry.yarnpkg.com/matcher/-/matcher-1.1.1.tgz#51d8301e138f840982b338b116bb0c09af62c1c2" + integrity sha512-+BmqxWIubKTRKNWx/ahnCkk3mG8m7OturVlqq6HiojGJTd5hVYbgZm6WzcYPCoB+KBT4Vd6R7WSRG2OADNaCjg== + dependencies: + escape-string-regexp "^1.0.4" + material-colors@^1.2.1: version "1.2.6" resolved "https://registry.yarnpkg.com/material-colors/-/material-colors-1.2.6.tgz#6d1958871126992ceecc72f4bcc4d8f010865f46" @@ -12512,16 +12447,6 @@ optionator@^0.8.1: type-check "~0.3.2" wordwrap "~1.0.0" -ora@^0.2.3: - version "0.2.3" - resolved "https://registry.yarnpkg.com/ora/-/ora-0.2.3.tgz#37527d220adcd53c39b73571d754156d5db657a4" - integrity sha1-N1J9Igrc1Tw5tzVx11QVbV22V6Q= - dependencies: - chalk "^1.1.1" - cli-cursor "^1.0.2" - cli-spinners "^0.1.2" - object-assign "^4.0.1" - ordered-ast-traverse@~1.1.1: version "1.1.1" resolved "https://registry.yarnpkg.com/ordered-ast-traverse/-/ordered-ast-traverse-1.1.1.tgz#6843a170bc0eee8b520cc8ddc1ddd3aa30fa057c" @@ -13023,6 +12948,13 @@ pkg-up@^1.0.0: dependencies: find-up "^1.0.0" +please-upgrade-node@^3.0.2: + version "3.1.1" + resolved "https://registry.yarnpkg.com/please-upgrade-node/-/please-upgrade-node-3.1.1.tgz#ed320051dfcc5024fae696712c8288993595e8ac" + integrity sha512-KY1uHnQ2NlQHqIJQpnh/i54rKkuxCEBx+voJIS/Mvb+L2iYd2NMotwduhKTMjfC1uKoX3VXOxLjIYG66dfJTVQ== + dependencies: + semver-compare "^1.0.0" + pluralize@^1.2.1: version "1.2.1" resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-1.2.1.tgz#d1a21483fd22bb41e58a12fa3421823140897c45" @@ -13560,14 +13492,6 @@ pretty-error@^2.0.2, pretty-error@^2.1.1: renderkid "^2.0.1" utila "~0.4" -pretty-format@^21.2.1: - version "21.2.1" - resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-21.2.1.tgz#ae5407f3cf21066cd011aa1ba5fce7b6a2eddb36" - integrity sha512-ZdWPGYAnYfcVP8yKA3zFjCn8s4/17TeYH28MXuC8vTp0o21eXjbFGcOAXZEaDaOFJjc3h2qa7HQNHNshhvoh2A== - dependencies: - ansi-regex "^3.0.0" - ansi-styles "^3.2.0" - pretty-format@^23.6.0: version "23.6.0" resolved "https://registry.yarnpkg.com/pretty-format/-/pretty-format-23.6.0.tgz#5eaac8eeb6b33b987b7fe6097ea6a8a146ab5760" @@ -13670,6 +13594,11 @@ prop-types@15.x, prop-types@^15.5.10, prop-types@^15.5.4, prop-types@^15.5.8, pr loose-envify "^1.3.1" object-assign "^4.1.1" +property-expr@^1.5.0: + version "1.5.1" + resolved "https://registry.yarnpkg.com/property-expr/-/property-expr-1.5.1.tgz#22e8706894a0c8e28d58735804f6ba3a3673314f" + integrity sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g== + property-information@^5.0.0, property-information@^5.0.1: version "5.0.1" resolved "https://registry.yarnpkg.com/property-information/-/property-information-5.0.1.tgz#c3b09f4f5750b1634c0b24205adbf78f18bdf94f" @@ -15078,7 +15007,7 @@ rx-lite@^3.1.2: resolved "https://registry.yarnpkg.com/rx-lite/-/rx-lite-3.1.2.tgz#19ce502ca572665f3b647b10939f97fd1615f102" integrity sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI= -rxjs@^5.4.2, rxjs@^5.5.2: +rxjs@^5.5.2: version "5.5.12" resolved "https://registry.yarnpkg.com/rxjs/-/rxjs-5.5.12.tgz#6fa61b8a77c3d793dbaf270bee2f43f652d741cc" integrity sha512-xx2itnL5sBbqeeiVgNPVuQQ1nC8Jp2WfNJhXWHmElW9YmrpS9UVnNzhP3EH3HFqexO5Tlp8GhYY+WEcqcVMvGw== @@ -15247,6 +15176,11 @@ selfsigned@^1.9.1: dependencies: node-forge "0.7.5" +semver-compare@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/semver-compare/-/semver-compare-1.0.0.tgz#0dee216a1c941ab37e9efb1788f6afc5ff5537fc" + integrity sha1-De4hahyUGrN+nvsXiPavxf9VN/w= + semver-diff@^2.0.0: version "2.1.0" resolved "https://registry.yarnpkg.com/semver-diff/-/semver-diff-2.1.0.tgz#4bbb8437c8d37e4b0cf1a68fd726ec6d645d6d36" @@ -15482,6 +15416,13 @@ simple-get@^2.7.0: once "^1.3.1" simple-concat "^1.0.0" +simple-git@^1.85.0: + version "1.107.0" + resolved "https://registry.yarnpkg.com/simple-git/-/simple-git-1.107.0.tgz#12cffaf261c14d6f450f7fdb86c21ccee968b383" + integrity sha512-t4OK1JRlp4ayKRfcW6owrWcRVLyHRUlhGd0uN6ZZTqfDq8a5XpcUdOKiGRNobHEuMtNqzp0vcJNvhYWwh5PsQA== + dependencies: + debug "^4.0.1" + simple-is@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/simple-is/-/simple-is-0.2.0.tgz#2abb75aade39deb5cc815ce10e6191164850baf0" @@ -15929,10 +15870,10 @@ stack-utils@^1.0.1: resolved "https://registry.yarnpkg.com/stack-utils/-/stack-utils-1.0.2.tgz#33eba3897788558bebfc2db059dc158ec36cebb8" integrity sha512-MTX+MeG5U994cazkjd/9KNAapsHnibjMLnfXodlkXw76JEea0UiNzrqidzo1emMwk7w5Qhc9jd4Bn9TBb1MFwA== -staged-git-files@1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.0.0.tgz#cdb847837c1fcc52c08a872d4883cc0877668a80" - integrity sha1-zbhHg3wfzFLAioctSIPMCHdmioA= +staged-git-files@1.1.2: + version "1.1.2" + resolved "https://registry.yarnpkg.com/staged-git-files/-/staged-git-files-1.1.2.tgz#4326d33886dc9ecfa29a6193bf511ba90a46454b" + integrity sha512-0Eyrk6uXW6tg9PYkhi/V/J4zHp33aNyi2hOCmhFLqLTIhbgqWn5jlSzI+IU0VqrZq6+DbHcabQl/WP6P3BG0QA== static-extend@^0.1.1: version "0.1.2" @@ -16009,13 +15950,6 @@ stream-shift@^1.0.0: resolved "https://registry.yarnpkg.com/stream-shift/-/stream-shift-1.0.0.tgz#d5c752825e5367e786f78e18e445ea223a155952" integrity sha1-1cdSgl5TZ+eG944Y5EXqIjoVWVI= -stream-to-observable@^0.2.0: - version "0.2.0" - resolved "https://registry.yarnpkg.com/stream-to-observable/-/stream-to-observable-0.2.0.tgz#59d6ea393d87c2c0ddac10aa0d561bc6ba6f0e10" - integrity sha1-WdbqOT2HwsDdrBCqDVYbxrpvDhA= - dependencies: - any-observable "^0.2.0" - strict-uri-encode@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-1.1.0.tgz#279b225df1d582b1f54e65addd4352e18faa0713" @@ -16026,6 +15960,11 @@ strict-uri-encode@^2.0.0: resolved "https://registry.yarnpkg.com/strict-uri-encode/-/strict-uri-encode-2.0.0.tgz#b9c7330c7042862f6b142dc274bbcc5866ce3546" integrity sha1-ucczDHBChi9rFC3CdLvMWGbONUY= +string-argv@^0.0.2: + version "0.0.2" + resolved "https://registry.yarnpkg.com/string-argv/-/string-argv-0.0.2.tgz#dac30408690c21f3c3630a3ff3a05877bdcbd736" + integrity sha1-2sMECGkMIfPDYwo/86BYd73L1zY= + string-length@^2.0.0: version "2.0.0" resolved "https://registry.yarnpkg.com/string-length/-/string-length-2.0.0.tgz#d40dbb686a3ace960c1cffca562bf2c45f8363ed" @@ -16131,7 +16070,7 @@ stringifier@^1.3.0: traverse "^0.6.6" type-name "^2.0.1" -stringify-object@^3.2.0: +stringify-object@^3.2.2: version "3.3.0" resolved "https://registry.yarnpkg.com/stringify-object/-/stringify-object-3.3.0.tgz#703065aefca19300d3ce88af4f5b3956d7556629" integrity sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw== @@ -16336,11 +16275,6 @@ symbol-observable@1.0.1: resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.0.1.tgz#8340fc4702c3122df5d22288f88283f513d3fdd4" integrity sha1-g0D8RwLDEi310iKI+IKD9RPT/dQ= -symbol-observable@^0.2.2: - version "0.2.4" - resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-0.2.4.tgz#95a83db26186d6af7e7a18dbd9760a2f86d08f40" - integrity sha1-lag9smGG1q9+ehjb2XYKL4bQj0A= - symbol-observable@^1.1.0, symbol-observable@^1.2.0: version "1.2.0" resolved "https://registry.yarnpkg.com/symbol-observable/-/symbol-observable-1.2.0.tgz#c22688aed4eab3cdc2dfeacbb561660560a00804" @@ -16358,6 +16292,11 @@ symbol.prototype.description@^1.0.0: dependencies: has-symbols "^1.0.0" +synchronous-promise@^2.0.5: + version "2.0.6" + resolved "https://registry.yarnpkg.com/synchronous-promise/-/synchronous-promise-2.0.6.tgz#de76e0ea2b3558c1e673942e47e714a930fa64aa" + integrity sha512-TyOuWLwkmtPL49LHCX1caIwHjRzcVd62+GF6h8W/jHOeZUFHpnd2XJDVuUlaTaLPH1nuu2M69mfHr5XbQJnf/g== + systemjs-plugin-css@^0.1.36: version "0.1.37" resolved "https://registry.yarnpkg.com/systemjs-plugin-css/-/systemjs-plugin-css-0.1.37.tgz#684847252ca69b7da24a1201094c86274324e82f" @@ -16649,6 +16588,11 @@ toposort@^1.0.0: resolved "https://registry.yarnpkg.com/toposort/-/toposort-1.0.7.tgz#2e68442d9f64ec720b8cc89e6443ac6caa950029" integrity sha1-LmhELZ9k7HILjMieZEOsbKqVACk= +toposort@^2.0.2: + version "2.0.2" + resolved "https://registry.yarnpkg.com/toposort/-/toposort-2.0.2.tgz#ae21768175d1559d48bef35420b2f4962f09c330" + integrity sha1-riF2gXXRVZ1IvvNUILL0li8JwzA= + touch@^2.0.1: version "2.0.2" resolved "https://registry.yarnpkg.com/touch/-/touch-2.0.2.tgz#ca0b2a3ae3211246a61b16ba9e6cbf1596287164" @@ -18075,6 +18019,18 @@ yeoman-generator@^2.0.5: through2 "^2.0.0" yeoman-environment "^2.0.5" +yup@^0.26.10: + version "0.26.10" + resolved "https://registry.yarnpkg.com/yup/-/yup-0.26.10.tgz#3545839663289038faf25facfc07e11fd67c0cb1" + integrity sha512-keuNEbNSnsOTOuGCt3UJW69jDE3O4P+UHAakO7vSeFMnjaitcmlbij/a3oNb9g1Y1KvSKH/7O1R2PQ4m4TRylw== + dependencies: + "@babel/runtime" "7.0.0" + fn-name "~2.0.1" + lodash "^4.17.10" + property-expr "^1.5.0" + synchronous-promise "^2.0.5" + toposort "^2.0.2" + zip-stream@^1.1.0: version "1.2.0" resolved "https://registry.yarnpkg.com/zip-stream/-/zip-stream-1.2.0.tgz#a8bc45f4c1b49699c6b90198baacaacdbcd4ba04" From e4e42fcd08c156d556b4896b323580e997edc2f7 Mon Sep 17 00:00:00 2001 From: bergquist Date: Mon, 11 Feb 2019 15:42:12 +0100 Subject: [PATCH 204/228] adds edition to build_info metric --- pkg/metrics/metrics.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/pkg/metrics/metrics.go b/pkg/metrics/metrics.go index 718a63ee768..bab2fb45127 100644 --- a/pkg/metrics/metrics.go +++ b/pkg/metrics/metrics.go @@ -3,6 +3,8 @@ package metrics import ( "runtime" + "github.com/grafana/grafana/pkg/setting" + "github.com/prometheus/client_golang/prometheus" ) @@ -282,7 +284,7 @@ func init() { Name: "build_info", Help: "A metric with a constant '1' value labeled by version, revision, branch, and goversion from which Grafana was built.", Namespace: exporterName, - }, []string{"version", "revision", "branch", "goversion"}) + }, []string{"version", "revision", "branch", "goversion", "edition"}) } // SetBuildInformation sets the build information for this binary @@ -291,8 +293,13 @@ func SetBuildInformation(version, revision, branch string) { // Once this have been released for some time we should be able to remote `M_Grafana_Version` // The reason we added a new one is that its common practice in the prometheus community // to name this metric `*_build_info` so its easy to do aggregation on all programs. + edition := "oss" + if setting.IsEnterprise { + edition = "enterprise" + } + M_Grafana_Version.WithLabelValues(version).Set(1) - grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version()).Set(1) + grafanaBuildVersion.WithLabelValues(version, revision, branch, runtime.Version(), edition).Set(1) } func initMetricVars() { From c332e106a24f800d673a04eddaa4e3bc55a4942e Mon Sep 17 00:00:00 2001 From: Peter Holmberg Date: Mon, 11 Feb 2019 16:00:08 +0100 Subject: [PATCH 205/228] Removing default thresholds values. --- public/vendor/flot/jquery.flot.gauge.js | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/public/vendor/flot/jquery.flot.gauge.js b/public/vendor/flot/jquery.flot.gauge.js index d256a5db7ed..b6468d5824f 100644 --- a/public/vendor/flot/jquery.flot.gauge.js +++ b/public/vendor/flot/jquery.flot.gauge.js @@ -935,16 +935,7 @@ } }, values: [ - { - value: 50, - color: "lightgreen" - }, { - value: 80, - color: "yellow" - }, { - value: 100, - color: "red" - } + ] } } From b93cdf56fb7d5828900ad60f5f2fc42e420adf00 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:26:02 +0100 Subject: [PATCH 206/228] Removed double page container --- public/app/features/teams/TeamList.tsx | 4 +- public/app/features/teams/TeamPages.tsx | 2 +- .../__snapshots__/TeamList.test.tsx.snap | 582 +++++++++--------- .../__snapshots__/TeamPages.test.tsx.snap | 22 +- 4 files changed, 297 insertions(+), 313 deletions(-) diff --git a/public/app/features/teams/TeamList.tsx b/public/app/features/teams/TeamList.tsx index efd279184d4..2e399b34860 100644 --- a/public/app/features/teams/TeamList.tsx +++ b/public/app/features/teams/TeamList.tsx @@ -86,7 +86,7 @@ export class TeamList extends PureComponent { const { teams, searchQuery } = this.props; return ( -
+ <>
-
+ ); } diff --git a/public/app/features/teams/TeamPages.tsx b/public/app/features/teams/TeamPages.tsx index ebbde595601..7a38197ff71 100644 --- a/public/app/features/teams/TeamPages.tsx +++ b/public/app/features/teams/TeamPages.tsx @@ -84,7 +84,7 @@ export class TeamPages extends PureComponent { return ( - {team && Object.keys(team).length !== 0 &&
{this.renderPage()}
} + {team && Object.keys(team).length !== 0 && this.renderPage()}
); diff --git a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap index 812fe05c424..5d969cd9d83 100644 --- a/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamList.test.tsx.snap @@ -36,320 +36,316 @@ exports[`Render should render teams table 1`] = ` isLoading={false} >
-
- -
-
+ - - - - - - - + + + + + + + + + + + + + + + + + + + +
- - Name - - Email - - Members - + +
+ + + + + + + + + + + + + + + - - - + + + - - - - - - + + - - - - - - + + - - - - - - + + - - - - - - + + + + + - - - - - - -
+ + Name + + Email + + Members + +
+ + + + + + test-1 + + + + test-1@test.com + + + + 1 + + + -
- - - - - - - test-1 - - - - test-1@test.com - - - - 1 - - - -
- - - - - - - test-2 - - - - test-2@test.com - - - - 2 - - - -
- - - - - - - test-3 - - - - test-3@test.com - - - - 3 - - - -
- - - - - - - test-4 - - - - test-4@test.com - - - - 4 - - - -
- +
+ - - - - - - test-5 - - - - test-5@test.com - - - - 5 - - - -
-
+ + +
+ + test-3 + + + + test-3@test.com + + + + 3 + + + +
+ + + + + + test-4 + + + + test-4@test.com + + + + 4 + + + +
+ + + + + + test-5 + + + + test-5@test.com + + + + 5 + + + +
diff --git a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap index 0c09eb3f82d..70f37cea4c5 100644 --- a/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap +++ b/public/app/features/teams/__snapshots__/TeamPages.test.tsx.snap @@ -17,11 +17,7 @@ exports[`Render should render group sync page 1`] = ` -
- -
+
`; @@ -33,13 +29,9 @@ exports[`Render should render member page if team not empty 1`] = ` -
- -
+
`; @@ -51,11 +43,7 @@ exports[`Render should render settings and preferences page 1`] = ` -
- -
+
`; From 8e93b68e6d883c8f2bb9ee4942a3888d98afcdf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:38:05 +0100 Subject: [PATCH 207/228] restoring green CTA --- .../PanelOptionsGroup/_PanelOptionsGroup.scss | 3 +-- .../ThresholdsEditor/_ThresholdsEditor.scss | 2 +- .../manage_dashboards/manage_dashboards.html | 11 ++++------- .../alerting/partials/notifications_list.html | 1 - public/app/features/api-keys/ApiKeysPage.tsx | 2 +- public/app/features/playlist/partials/playlists.html | 1 - public/sass/_variables.dark.scss | 11 +++++++---- public/sass/_variables.light.scss | 11 +++++++---- 8 files changed, 21 insertions(+), 21 deletions(-) diff --git a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss index f8a3a408bab..4ce9c5264ea 100644 --- a/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss +++ b/packages/grafana-ui/src/components/PanelOptionsGroup/_PanelOptionsGroup.scss @@ -36,8 +36,7 @@ } .panel-options-group__add-circle { - - @include gradientBar($btn-secondary-bg, $btn-secondary-bg-hl, #fff); + @include gradientBar($btn-success-bg, $btn-success-bg-hl, #fff); border-radius: 50px; width: 20px; diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss index e2cbfc372a9..8ef59bf08af 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss +++ b/packages/grafana-ui/src/components/ThresholdsEditor/_ThresholdsEditor.scss @@ -21,7 +21,7 @@ } .thresholds-row-add-button { - @include buttonBackground($btn-secondary-bg, $btn-secondary-bg-hl, #fff); + @include buttonBackground($btn-success-bg, $btn-success-bg-hl, #fff); align-self: center; margin-right: 5px; diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 6036ead3ef1..4ef2d7c9a66 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -6,15 +6,12 @@
- - Dashboard + New Dashboard - - - Folder + + New Folder - - + Import
diff --git a/public/app/features/alerting/partials/notifications_list.html b/public/app/features/alerting/partials/notifications_list.html index 6624a1d1132..ce4fea9ff49 100644 --- a/public/app/features/alerting/partials/notifications_list.html +++ b/public/app/features/alerting/partials/notifications_list.html @@ -8,7 +8,6 @@
- New Channel
diff --git a/public/app/features/api-keys/ApiKeysPage.tsx b/public/app/features/api-keys/ApiKeysPage.tsx index 7bed498e2ac..2627b1a6862 100644 --- a/public/app/features/api-keys/ApiKeysPage.tsx +++ b/public/app/features/api-keys/ApiKeysPage.tsx @@ -200,7 +200,7 @@ export class ApiKeysPage extends PureComponent {
diff --git a/public/app/features/playlist/partials/playlists.html b/public/app/features/playlist/partials/playlists.html index 2ec919f8157..22e41ac7104 100644 --- a/public/app/features/playlist/partials/playlists.html +++ b/public/app/features/playlist/partials/playlists.html @@ -5,7 +5,6 @@ diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 1ed3ecf2cf7..6181590985f 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -159,11 +159,14 @@ $table-bg-hover: $dark-3; // Buttons // ------------------------- -$btn-primary-bg: $sapphire-base; -$btn-primary-bg-hl: $sapphire-shade; +$btn-secondary-bg: $sapphire-base; +$btn-secondary-bg-hl: $sapphire-shade; -$btn-secondary-bg: $green-base; -$btn-secondary-bg-hl: $green-shade; +$btn-primary-bg: $green-base; +$btn-primary-bg-hl: $green-shade; + +$btn-success-bg: $green-base; +$btn-success-bg-hl: $green-shade; $btn-danger-bg: $lobster-base; $btn-danger-bg-hl: $lobster-shade; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 077c6598a4d..f0e0a535653 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -155,11 +155,14 @@ $table-bg-hover: $gray-5; // Buttons // ------------------------- -$btn-secondary-bg: $green-base; -$btn-secondary-bg-hl: $green-shade; +$btn-primary-bg: $green-base; +$btn-primary-bg-hl: $green-shade; -$btn-primary-bg: $sapphire-base; -$btn-primary-bg-hl: $sapphire-shade; +$btn-secondary-bg: $sapphire-base; +$btn-secondary-bg-hl: $sapphire-shade; + +$btn-success-bg: $green-base; +$btn-success-bg-hl: $green-shade; $btn-danger-bg: $lobster-base; $btn-danger-bg-hl: $lobster-shade; From afc2efa56ddb53334b3a4c73d3f30b3de9ad2c01 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:45:47 +0100 Subject: [PATCH 208/228] Removed plus icons --- .../DashboardPermissions/DashboardPermissions.tsx | 4 +--- public/app/features/folders/FolderPermissions.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx index 8cc26c4a1f2..e5fb0da71fc 100644 --- a/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx +++ b/public/app/features/dashboard/components/DashboardPermissions/DashboardPermissions.tsx @@ -76,9 +76,7 @@ export class DashboardPermissions extends PureComponent {
- +
diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index f8c59d82130..f564991f291 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/public/app/features/folders/FolderPermissions.tsx @@ -73,7 +73,13 @@ export class FolderPermissions extends PureComponent { const { isAdding } = this.state; if (folder.id === 0) { - return ; + return ( + + + + + + ); } const folderInfo = { title: folder.title, url: folder.url, id: folder.id }; @@ -90,7 +96,7 @@ export class FolderPermissions extends PureComponent {
From 5195954681527f277f3ce6f4f52f7b6efe215760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 16:50:12 +0100 Subject: [PATCH 209/228] style tweak to alert --- public/sass/components/_alerts.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_alerts.scss b/public/sass/components/_alerts.scss index dc98cba82bd..1c4f1b7fcb7 100644 --- a/public/sass/components/_alerts.scss +++ b/public/sass/components/_alerts.scss @@ -6,7 +6,7 @@ // ------------------------- .alert { - padding: 1.25rem 2rem 1.25rem 1.5rem; + padding: 15px 20px; margin-bottom: $panel-margin / 2; text-shadow: 0 2px 0 rgba(255, 255, 255, 0.5); background: $alert-error-bg; From 951e5932d4e650e160aeae2bbde850b7985dd237 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 11 Feb 2019 17:00:16 +0100 Subject: [PATCH 210/228] changelog: adds note for #15363 --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a82fc7050b4..6589099e178 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,10 @@ * **AzureMonitor**: improve autocomplete for Log Analytics and App Insights editor [#15131](https://github.com/grafana/grafana/issues/15131) * **LDAP**: Fix IPA/FreeIPA v4.6.4 does not allow LDAP searches with empty attributes [#14432](https://github.com/grafana/grafana/issues/14432) +### Breaking changes + +* **Internal Metrics** Edition has been added to the build_info metric. This will break any Graphite queries using this metric. Edition will be a new label for the Prometheus metric. [#15363](https://github.com/grafana/grafana/pull/15363) + ### 6.0.0-beta1 fixes * **Postgres**: Fix default port not added when port not configured [#15189](https://github.com/grafana/grafana/issues/15189) From 0493d905f17f6b1b7ffbb7afbf181a91798d5bf8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 17:42:31 +0100 Subject: [PATCH 211/228] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6589099e178..7b97da0e81c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,6 @@ -# 6.0.0-beta2 (unreleased) +# 6.0.0-beta3 (unreleased) + +# 6.0.0-beta2 (2019-02-11) ### New Features * **AzureMonitor**: Enable alerting by converting Azure Monitor API to Go [#14623](https://github.com/grafana/grafana/issues/14623) From e38cfc1a7183bd30e6e3eb022077e96fe6167d49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 17:43:02 +0100 Subject: [PATCH 212/228] Update package.json --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2f44291a86a..f004ee07732 100644 --- a/package.json +++ b/package.json @@ -5,7 +5,7 @@ "company": "Grafana Labs" }, "name": "grafana", - "version": "6.0.0-prebeta2", + "version": "6.0.0-pre3", "repository": { "type": "git", "url": "http://github.com/grafana/grafana.git" From fc91e1cf57a22e4a4a5cd1e99f7bee70ad3ec65b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 11 Feb 2019 17:47:47 +0100 Subject: [PATCH 213/228] Fixed issue with gauge requests being cancelled --- public/app/features/dashboard/dashgrid/DataPanel.tsx | 3 +-- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 1 + 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DataPanel.tsx b/public/app/features/dashboard/dashgrid/DataPanel.tsx index 2183548000b..b81d66fa7f5 100644 --- a/public/app/features/dashboard/dashgrid/DataPanel.tsx +++ b/public/app/features/dashboard/dashgrid/DataPanel.tsx @@ -28,7 +28,7 @@ interface RenderProps { export interface Props { datasource: string | null; queries: any[]; - panelId?: number; + panelId: number; dashboardId?: number; isVisible?: boolean; timeRange?: TimeRange; @@ -50,7 +50,6 @@ export interface State { export class DataPanel extends Component { static defaultProps = { isVisible: true, - panelId: 1, dashboardId: 1, }; diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b02d9479dcc..b29be4b389d 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -149,6 +149,7 @@ export class PanelChrome extends PureComponent { this.renderPanel(false, panel.snapshotData, width, height) ) : ( Date: Mon, 11 Feb 2019 21:11:19 +0100 Subject: [PATCH 214/228] Fix error caused by named colors that are not part of named colors palette --- packages/grafana-ui/src/utils/namedColorsPalette.test.ts | 8 ++++---- packages/grafana-ui/src/utils/namedColorsPalette.ts | 3 ++- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts index aa57b46636c..e544548aa4a 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts @@ -44,10 +44,6 @@ describe('colors', () => { }); describe('getColorFromHexRgbOrName', () => { - it('returns undefined for unknown color', () => { - expect(() => getColorFromHexRgbOrName('aruba-sunshine')).toThrow(); - }); - it('returns dark hex variant for known color if theme not specified', () => { expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark); }); @@ -64,5 +60,9 @@ describe('colors', () => { expect(getColorFromHexRgbOrName('rgb(0,0,0)')).toBe('rgb(0,0,0)'); expect(getColorFromHexRgbOrName('rgba(0,0,0,1)')).toBe('rgba(0,0,0,1)'); }); + + it('returns hex for named color that is not a part of named colors palette', () => { + expect(getColorFromHexRgbOrName('lime')).toBe('#00ff00'); + }); }); }); diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.ts b/packages/grafana-ui/src/utils/namedColorsPalette.ts index ee5741e794e..88ae510a6d8 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.ts @@ -1,5 +1,6 @@ import { flatten } from 'lodash'; import { GrafanaThemeType } from '../types'; +import tinycolor from 'tinycolor2'; type Hue = 'green' | 'yellow' | 'red' | 'blue' | 'orange' | 'purple'; @@ -106,7 +107,7 @@ export const getColorFromHexRgbOrName = (color: string, theme?: GrafanaThemeType const colorDefinition = getColorByName(color); if (!colorDefinition) { - throw new Error('Unknown color'); + return new tinycolor(color).toHexString(); } return theme ? colorDefinition.variants[theme] : colorDefinition.variants.dark; From a5d158c014a65fded992bab069368eb61adbe1a8 Mon Sep 17 00:00:00 2001 From: Dominik Prokop Date: Mon, 11 Feb 2019 21:22:53 +0100 Subject: [PATCH 215/228] Added one more test case for color resolving helper --- packages/grafana-ui/src/utils/namedColorsPalette.test.ts | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts index e544548aa4a..19c7d9c84ad 100644 --- a/packages/grafana-ui/src/utils/namedColorsPalette.test.ts +++ b/packages/grafana-ui/src/utils/namedColorsPalette.test.ts @@ -44,6 +44,10 @@ describe('colors', () => { }); describe('getColorFromHexRgbOrName', () => { + it('returns black for unknown color', () => { + expect(getColorFromHexRgbOrName('aruba-sunshine')).toBe("#000000"); + }); + it('returns dark hex variant for known color if theme not specified', () => { expect(getColorFromHexRgbOrName(SemiDarkBlue.name)).toBe(SemiDarkBlue.variants.dark); }); From edd9576f1586178501b1d75726944d1df70fcc1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 12 Feb 2019 08:04:30 +0100 Subject: [PATCH 216/228] Fixed elastic5 docker compose block --- devenv/docker/blocks/elastic5/docker-compose.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/devenv/docker/blocks/elastic5/docker-compose.yaml b/devenv/docker/blocks/elastic5/docker-compose.yaml index 33a7d9855b0..3a2ef39faba 100644 --- a/devenv/docker/blocks/elastic5/docker-compose.yaml +++ b/devenv/docker/blocks/elastic5/docker-compose.yaml @@ -1,6 +1,3 @@ -# You need to run 'sysctl -w vm.max_map_count=262144' on the host machine -version: '2' -services: elasticsearch5: image: elasticsearch:5 command: elasticsearch From 3b9105e1bebb24e518548faf258576e0b30f70a8 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 12 Feb 2019 08:45:21 +0100 Subject: [PATCH 217/228] enable testing provsioned datasources closes #12164 --- .../datasources/settings/ButtonRow.test.tsx | 1 + .../datasources/settings/ButtonRow.tsx | 8 +- .../settings/DataSourceSettingsPage.tsx | 89 ++++++++++--------- .../__snapshots__/ButtonRow.test.tsx.snap | 7 ++ .../DataSourceSettingsPage.test.tsx.snap | 4 + 5 files changed, 68 insertions(+), 41 deletions(-) diff --git a/public/app/features/datasources/settings/ButtonRow.test.tsx b/public/app/features/datasources/settings/ButtonRow.test.tsx index 0acab8941ff..84b16d829d5 100644 --- a/public/app/features/datasources/settings/ButtonRow.test.tsx +++ b/public/app/features/datasources/settings/ButtonRow.test.tsx @@ -7,6 +7,7 @@ const setup = (propOverrides?: object) => { isReadOnly: true, onSubmit: jest.fn(), onDelete: jest.fn(), + onTest: jest.fn(), }; Object.assign(props, propOverrides); diff --git a/public/app/features/datasources/settings/ButtonRow.tsx b/public/app/features/datasources/settings/ButtonRow.tsx index 6b85e21405c..3e8ac060010 100644 --- a/public/app/features/datasources/settings/ButtonRow.tsx +++ b/public/app/features/datasources/settings/ButtonRow.tsx @@ -4,14 +4,20 @@ export interface Props { isReadOnly: boolean; onDelete: () => void; onSubmit: (event) => void; + onTest: (event) => void; } -const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit }) => { +const ButtonRow: FC = ({ isReadOnly, onDelete, onSubmit, onTest }) => { return (
+ {isReadOnly && ( + + )} diff --git a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx index ff840390cf5..fe1121ed73e 100644 --- a/public/app/features/datasources/settings/DataSourceSettingsPage.tsx +++ b/public/app/features/datasources/settings/DataSourceSettingsPage.tsx @@ -72,6 +72,12 @@ export class DataSourceSettingsPage extends PureComponent { this.testDataSource(); }; + onTest = async (evt: React.FormEvent) => { + evt.preventDefault(); + + this.testDataSource(); + }; + onDelete = () => { appEvents.emit('confirm-modal', { title: 'Delete', @@ -180,52 +186,55 @@ export class DataSourceSettingsPage extends PureComponent { return ( - {this.hasDataSource &&
-
-
- {this.isReadOnly() && this.renderIsReadOnlyMessage()} - {this.shouldRenderInfoBox() &&
{this.getInfoText()}
} + {this.hasDataSource && ( +
+
+ + {this.isReadOnly() && this.renderIsReadOnlyMessage()} + {this.shouldRenderInfoBox() &&
{this.getInfoText()}
} - setIsDefault(state)} - onNameChange={name => setDataSourceName(name)} - /> - - {dataSourceMeta.module && ( - setIsDefault(state)} + onNameChange={name => setDataSourceName(name)} /> - )} -
- {testingMessage && ( -
-
- {testingStatus === 'error' ? ( - - ) : ( - - )} -
-
-
{testingMessage}
-
-
+ {dataSourceMeta.module && ( + )} -
- this.onSubmit(event)} - isReadOnly={this.isReadOnly()} - onDelete={this.onDelete} - /> - +
+ {testingMessage && ( +
+
+ {testingStatus === 'error' ? ( + + ) : ( + + )} +
+
+
{testingMessage}
+
+
+ )} +
+ + this.onSubmit(event)} + isReadOnly={this.isReadOnly()} + onDelete={this.onDelete} + onTest={event => this.onTest(event)} + /> + +
-
} + )} ); diff --git a/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap index bd190f60b03..d4ec7eeea1e 100644 --- a/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap +++ b/public/app/features/datasources/settings/__snapshots__/ButtonRow.test.tsx.snap @@ -12,6 +12,13 @@ exports[`Render should render component 1`] = ` > Save & Test +
@@ -202,6 +203,7 @@ exports[`Render should render beta info text 1`] = ` isReadOnly={false} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} />
@@ -302,6 +304,7 @@ exports[`Render should render component 1`] = ` isReadOnly={false} onDelete={[Function]} onSubmit={[Function]} + onTest={[Function]} />
From da80286f97f1075ff59fd9b82601a4f2c40230be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hugo=20H=C3=A4ggmark?= Date: Tue, 12 Feb 2019 11:10:31 +0100 Subject: [PATCH 218/228] Fixes #15372 with number input and parseFloat --- .../ThresholdsEditor/ThresholdsEditor.test.tsx | 6 +++--- .../ThresholdsEditor/ThresholdsEditor.tsx | 17 ++++++++--------- 2 files changed, 11 insertions(+), 12 deletions(-) diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx index 845ff5f6bf4..2b6af67df22 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.test.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React, { ChangeEvent } from 'react'; import { shallow } from 'enzyme'; import { ThresholdsEditor, Props } from './ThresholdsEditor'; @@ -118,7 +118,7 @@ describe('change threshold value', () => { ]; const instance = setup({ thresholds }); - const mockEvent = { target: { value: 12 } }; + const mockEvent = ({ target: { value: '12' } } as any) as ChangeEvent; instance.onChangeThresholdValue(mockEvent, thresholds[0]); @@ -137,7 +137,7 @@ describe('change threshold value', () => { thresholds, }; - const mockEvent = { target: { value: 78 } }; + const mockEvent = ({ target: { value: '78' } } as any) as ChangeEvent; instance.onChangeThresholdValue(mockEvent, thresholds[1]); diff --git a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx index b2a2e07c58d..f4db23d6656 100644 --- a/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx +++ b/packages/grafana-ui/src/components/ThresholdsEditor/ThresholdsEditor.tsx @@ -1,4 +1,4 @@ -import React, { PureComponent } from 'react'; +import React, { PureComponent, ChangeEvent } from 'react'; import { Threshold } from '../../types'; import { ColorPicker } from '../ColorPicker/ColorPicker'; import { PanelOptionsGroup } from '../PanelOptionsGroup/PanelOptionsGroup'; @@ -94,14 +94,15 @@ export class ThresholdsEditor extends PureComponent { ); }; - onChangeThresholdValue = (event: any, threshold: Threshold) => { + onChangeThresholdValue = (event: ChangeEvent, threshold: Threshold) => { if (threshold.index === 0) { return; } const { thresholds } = this.state; - const parsedValue = parseInt(event.target.value, 10); - const value = isNaN(parsedValue) ? null : parsedValue; + const cleanValue = event.target.value.replace(/,/g, '.'); + const parsedValue = parseFloat(cleanValue); + const value = isNaN(parsedValue) ? '' : parsedValue; const newThresholds = thresholds.map(t => { if (t === threshold && t.index !== 0) { @@ -164,16 +165,14 @@ export class ThresholdsEditor extends PureComponent {
{threshold.color && (
- this.onChangeThresholdColor(threshold, color)} - /> + this.onChangeThresholdColor(threshold, color)} />
)}
this.onChangeThresholdValue(event, threshold)} value={value} onBlur={this.onBlur} From 0811fbd6d0909b0d22beecf64f7969b7475af9d8 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 7 Feb 2019 21:22:16 +0100 Subject: [PATCH 219/228] fix: Add missing typing --- public/app/plugins/panel/graph2/GraphPanel.tsx | 4 ++-- public/app/plugins/panel/text2/module.tsx | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/public/app/plugins/panel/graph2/GraphPanel.tsx b/public/app/plugins/panel/graph2/GraphPanel.tsx index 01e5b2d819d..f1fc2b43d51 100644 --- a/public/app/plugins/panel/graph2/GraphPanel.tsx +++ b/public/app/plugins/panel/graph2/GraphPanel.tsx @@ -9,7 +9,7 @@ import { processTimeSeries } from '@grafana/ui/src/utils'; import { Graph } from '@grafana/ui'; // Types -import { PanelProps, NullValueMode } from '@grafana/ui/src/types'; +import { PanelProps, NullValueMode, TimeSeriesVMs } from '@grafana/ui/src/types'; import { Options } from './types'; interface Props extends PanelProps {} @@ -19,7 +19,7 @@ export class GraphPanel extends PureComponent { const { panelData, timeRange, width, height } = this.props; const { showLines, showBars, showPoints } = this.props.options; - let vmSeries; + let vmSeries: TimeSeriesVMs; if (panelData.timeSeries) { vmSeries = processTimeSeries({ timeSeries: panelData.timeSeries, diff --git a/public/app/plugins/panel/text2/module.tsx b/public/app/plugins/panel/text2/module.tsx index 68523ff0880..cc3ec016273 100644 --- a/public/app/plugins/panel/text2/module.tsx +++ b/public/app/plugins/panel/text2/module.tsx @@ -2,7 +2,7 @@ import React, { PureComponent } from 'react'; import { PanelProps } from '@grafana/ui'; export class Text2 extends PureComponent { - constructor(props) { + constructor(props: PanelProps) { super(props); } From c4b2dcefbe88cfe84793fcf0fca48fe1ff781351 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 7 Feb 2019 21:26:23 +0100 Subject: [PATCH 220/228] feat: Introduce IsDataPanel attribute to plugin.json --- pkg/api/frontendsettings.go | 1 + pkg/plugins/models.go | 1 + public/app/plugins/panel/gauge/plugin.json | 1 + public/app/plugins/panel/graph2/plugin.json | 2 +- public/app/plugins/panel/text2/plugin.json | 2 +- public/app/types/plugins.ts | 1 + 6 files changed, 6 insertions(+), 2 deletions(-) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index ed7054050e4..238a3965641 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -145,6 +145,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf "info": panel.Info, "hideFromList": panel.HideFromList, "sort": getPanelSort(panel.Id), + "isDataPanel": panel.IsDataPanel, } } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index 5ac436205c1..e37b1fcf7d9 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -47,6 +47,7 @@ type PluginBase struct { BaseUrl string `json:"baseUrl"` HideFromList bool `json:"hideFromList,omitempty"` State PluginState `json:"state,omitempty"` + IsDataPanel bool `json:"isDataPanel"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` diff --git a/public/app/plugins/panel/gauge/plugin.json b/public/app/plugins/panel/gauge/plugin.json index 58437779d25..733d2281cf4 100644 --- a/public/app/plugins/panel/gauge/plugin.json +++ b/public/app/plugins/panel/gauge/plugin.json @@ -2,6 +2,7 @@ "type": "panel", "name": "Gauge", "id": "gauge", + "isDataPanel": true, "info": { "author": { diff --git a/public/app/plugins/panel/graph2/plugin.json b/public/app/plugins/panel/graph2/plugin.json index 9cb6a1f78a4..9b2a915a597 100644 --- a/public/app/plugins/panel/graph2/plugin.json +++ b/public/app/plugins/panel/graph2/plugin.json @@ -2,7 +2,7 @@ "type": "panel", "name": "React Graph", "id": "graph2", - + "isDataPanel": true, "state": "alpha", "info": { diff --git a/public/app/plugins/panel/text2/plugin.json b/public/app/plugins/panel/text2/plugin.json index 53885dbd0f4..cd4ff424d89 100644 --- a/public/app/plugins/panel/text2/plugin.json +++ b/public/app/plugins/panel/text2/plugin.json @@ -2,8 +2,8 @@ "type": "panel", "name": "Text v2", "id": "text2", - "state": "alpha", + "isDataPanel": false, "info": { "author": { diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 51c3b7b0476..0c5c53eb6f0 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -9,6 +9,7 @@ export interface PanelPlugin { info: any; sort: number; exports?: PluginExports; + isDataPanel?: boolean; } export interface Plugin { From 3f64d61fd28641227e0aee00b1bbf0da70c4a486 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 7 Feb 2019 21:31:05 +0100 Subject: [PATCH 221/228] feat: Add util to convert snapshotData to PanelData --- public/app/features/dashboard/utils/panel.ts | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/utils/panel.ts b/public/app/features/dashboard/utils/panel.ts index c0d753477a7..c60a153d889 100644 --- a/public/app/features/dashboard/utils/panel.ts +++ b/public/app/features/dashboard/utils/panel.ts @@ -4,7 +4,8 @@ import store from 'app/core/store'; // Models import { DashboardModel } from 'app/features/dashboard/state/DashboardModel'; import { PanelModel } from 'app/features/dashboard/state/PanelModel'; -import { TimeRange } from '@grafana/ui'; +import { PanelData, TimeRange, TimeSeries } from '@grafana/ui'; +import { TableData } from '@grafana/ui/src'; // Utils import { isString as _isString } from 'lodash'; @@ -168,3 +169,19 @@ export function getResolution(panel: PanelModel): number { return panel.maxDataPoints ? panel.maxDataPoints : Math.ceil(width * (panel.gridPos.w / 24)); } + +const isTimeSeries = (data: any): data is TimeSeries => data && data.hasOwnProperty('datapoints'); +const isTableData = (data: any): data is TableData => data && data.hasOwnProperty('columns'); +export const snapshotDataToPanelData = (panel: PanelModel): PanelData => { + const snapshotData = panel.snapshotData; + if (isTimeSeries(snapshotData[0])) { + return { + timeSeries: snapshotData + } as PanelData; + } else if (isTableData(snapshotData[0])) { + return { + tableData: snapshotData[0] + } as PanelData; + } + throw new Error('snapshotData is invalid:' + snapshotData.toString()); +}; From d3115325a9b2a797c0de5263bc0b60c7189cb716 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Thu, 7 Feb 2019 21:34:50 +0100 Subject: [PATCH 222/228] feat: Only use the DataPanel component when panel plugin has isDataPanel set to true in plugin.json. And fix PanelData when using snapshots --- .../dashboard/dashgrid/PanelChrome.tsx | 68 ++++++++++++------- 1 file changed, 43 insertions(+), 25 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index b29be4b389d..3b56b9ad07e 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -10,14 +10,14 @@ import { PanelHeader } from './PanelHeader/PanelHeader'; import { DataPanel } from './DataPanel'; // Utils -import { applyPanelTimeOverrides } from 'app/features/dashboard/utils/panel'; +import { applyPanelTimeOverrides, snapshotDataToPanelData } from 'app/features/dashboard/utils/panel'; import { PANEL_HEADER_HEIGHT } from 'app/core/constants'; import { profiler } from 'app/core/profiler'; // Types import { DashboardModel, PanelModel } from '../state'; import { PanelPlugin } from 'app/types'; -import { TimeRange, LoadingState } from '@grafana/ui'; +import { TimeRange, LoadingState, PanelData } from '@grafana/ui'; import variables from 'sass/_variables.scss'; import templateSrv from 'app/features/templating/template_srv'; @@ -94,7 +94,7 @@ export class PanelChrome extends PureComponent { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } - renderPanel(loading, panelData, width, height): JSX.Element { + renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; const PanelComponent = plugin.exports.Panel; @@ -121,11 +121,46 @@ export class PanelChrome extends PureComponent { ); } - render() { - const { panel, dashboard } = this.props; - const { refreshCounter, timeRange, timeInfo } = this.state; + renderHelper = (width: number, height: number): JSX.Element => { + const { panel, plugin } = this.props; + const { refreshCounter, timeRange } = this.state; + const { datasource, targets } = panel; + return ( + <> + {panel.snapshotData && panel.snapshotData.length > 0 ? ( + this.renderPanelPlugin(LoadingState.Done, snapshotDataToPanelData(panel), width, height) + ) : ( + <> + {plugin.isDataPanel === true ? + + {({ loading, panelData }) => { + return this.renderPanelPlugin(loading, panelData, width, height); + }} + + : ( + this.renderPanelPlugin(LoadingState.Done, null, width, height) + )} + + )} + + ); + } + + + render() { + const { dashboard, panel } = this.props; + const { timeInfo } = this.state; + const { transparent } = panel; - const { datasource, targets, transparent } = panel; const containerClassNames = `panel-container panel-container--absolute ${transparent ? 'panel-transparent' : ''}`; return ( @@ -145,24 +180,7 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} /> - {panel.snapshotData ? ( - this.renderPanel(false, panel.snapshotData, width, height) - ) : ( - - {({ loading, panelData }) => { - return this.renderPanel(loading, panelData, width, height); - }} - - )} + {this.renderHelper(width, height)}
); }} From b7b1a79405794b5277325cb90ef89178257af324 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Mon, 11 Feb 2019 16:20:32 +0100 Subject: [PATCH 223/228] chore: Only show Queries tab for panel plugins with isDataPanel set to true --- .../dashboard/panel_editor/PanelEditor.tsx | 53 +++++++++++++------ 1 file changed, 37 insertions(+), 16 deletions(-) diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index bfdc13bc8f2..37240389373 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -30,6 +30,32 @@ interface PanelEditorTab { text: string; } +enum PanelEditorTabIds { + Queries = 'queries', + Visualization = 'visualization', + Advanced = 'advanced', + Alert = 'alert' +} + +interface PanelEditorTab { + id: string; + text: string; +} + +const panelEditorTabTexts = { + [PanelEditorTabIds.Queries]: 'Queries', + [PanelEditorTabIds.Visualization]: 'Visualization', + [PanelEditorTabIds.Advanced]: 'Panel Options', + [PanelEditorTabIds.Alert]: 'Alert', +}; + +const getPanelEditorTab = (tabId: PanelEditorTabIds): PanelEditorTab => { + return { + id: tabId, + text: panelEditorTabTexts[tabId] + }; +}; + export class PanelEditor extends PureComponent { constructor(props) { super(props); @@ -72,31 +98,26 @@ export class PanelEditor extends PureComponent { render() { const { plugin } = this.props; - let activeTab = store.getState().location.query.tab || 'queries'; + let activeTab: PanelEditorTabIds = store.getState().location.query.tab || PanelEditorTabIds.Queries; const tabs: PanelEditorTab[] = [ - { id: 'queries', text: 'Queries' }, - { id: 'visualization', text: 'Visualization' }, - { id: 'advanced', text: 'Panel Options' }, + getPanelEditorTab(PanelEditorTabIds.Queries), + getPanelEditorTab(PanelEditorTabIds.Visualization), + getPanelEditorTab(PanelEditorTabIds.Advanced), ]; // handle panels that do not have queries tab - if (plugin.exports.PanelCtrl) { - if (!plugin.exports.PanelCtrl.prototype.onDataReceived) { - // remove queries tab - tabs.shift(); - // switch tab - if (activeTab === 'queries') { - activeTab = 'visualization'; - } + if (!plugin.isDataPanel) { + // remove queries tab + tabs.shift(); + // switch tab + if (activeTab === PanelEditorTabIds.Queries) { + activeTab = PanelEditorTabIds.Visualization; } } if (config.alertingEnabled && plugin.id === 'graph') { - tabs.push({ - id: 'alert', - text: 'Alert', - }); + tabs.push(getPanelEditorTab(PanelEditorTabIds.Alert)); } return ( From 075fb8e91cdcff2b08580865a71ee7fa8a0910b6 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 12 Feb 2019 09:48:46 +0100 Subject: [PATCH 224/228] chore: Rename isDataPanel to noQueries --- pkg/api/frontendsettings.go | 2 +- pkg/plugins/models.go | 2 +- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 9 +++++---- .../app/features/dashboard/panel_editor/PanelEditor.tsx | 2 +- public/app/plugins/panel/gauge/plugin.json | 1 - public/app/plugins/panel/graph2/plugin.json | 1 - public/app/plugins/panel/text2/plugin.json | 2 +- public/app/types/plugins.ts | 2 +- 8 files changed, 10 insertions(+), 11 deletions(-) diff --git a/pkg/api/frontendsettings.go b/pkg/api/frontendsettings.go index 238a3965641..cb401577140 100644 --- a/pkg/api/frontendsettings.go +++ b/pkg/api/frontendsettings.go @@ -145,7 +145,7 @@ func (hs *HTTPServer) getFrontendSettingsMap(c *m.ReqContext) (map[string]interf "info": panel.Info, "hideFromList": panel.HideFromList, "sort": getPanelSort(panel.Id), - "isDataPanel": panel.IsDataPanel, + "noQueries": panel.NoQueries, } } diff --git a/pkg/plugins/models.go b/pkg/plugins/models.go index e37b1fcf7d9..7584981fc6c 100644 --- a/pkg/plugins/models.go +++ b/pkg/plugins/models.go @@ -47,7 +47,7 @@ type PluginBase struct { BaseUrl string `json:"baseUrl"` HideFromList bool `json:"hideFromList,omitempty"` State PluginState `json:"state,omitempty"` - IsDataPanel bool `json:"isDataPanel"` + NoQueries bool `json:"noQueries"` IncludedInAppId string `json:"-"` PluginDir string `json:"-"` diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 3b56b9ad07e..ca2f96d6044 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -131,8 +131,11 @@ export class PanelChrome extends PureComponent { this.renderPanelPlugin(LoadingState.Done, snapshotDataToPanelData(panel), width, height) ) : ( <> - {plugin.isDataPanel === true ? - { return this.renderPanelPlugin(loading, panelData, width, height); }} - : ( - this.renderPanelPlugin(LoadingState.Done, null, width, height) )} )} diff --git a/public/app/features/dashboard/panel_editor/PanelEditor.tsx b/public/app/features/dashboard/panel_editor/PanelEditor.tsx index 37240389373..31274a6ad26 100644 --- a/public/app/features/dashboard/panel_editor/PanelEditor.tsx +++ b/public/app/features/dashboard/panel_editor/PanelEditor.tsx @@ -107,7 +107,7 @@ export class PanelEditor extends PureComponent { ]; // handle panels that do not have queries tab - if (!plugin.isDataPanel) { + if (plugin.noQueries === true) { // remove queries tab tabs.shift(); // switch tab diff --git a/public/app/plugins/panel/gauge/plugin.json b/public/app/plugins/panel/gauge/plugin.json index 733d2281cf4..58437779d25 100644 --- a/public/app/plugins/panel/gauge/plugin.json +++ b/public/app/plugins/panel/gauge/plugin.json @@ -2,7 +2,6 @@ "type": "panel", "name": "Gauge", "id": "gauge", - "isDataPanel": true, "info": { "author": { diff --git a/public/app/plugins/panel/graph2/plugin.json b/public/app/plugins/panel/graph2/plugin.json index 9b2a915a597..b11f93c9adc 100644 --- a/public/app/plugins/panel/graph2/plugin.json +++ b/public/app/plugins/panel/graph2/plugin.json @@ -2,7 +2,6 @@ "type": "panel", "name": "React Graph", "id": "graph2", - "isDataPanel": true, "state": "alpha", "info": { diff --git a/public/app/plugins/panel/text2/plugin.json b/public/app/plugins/panel/text2/plugin.json index cd4ff424d89..661ac4671ef 100644 --- a/public/app/plugins/panel/text2/plugin.json +++ b/public/app/plugins/panel/text2/plugin.json @@ -3,7 +3,7 @@ "name": "Text v2", "id": "text2", "state": "alpha", - "isDataPanel": false, + "noQueries": true, "info": { "author": { diff --git a/public/app/types/plugins.ts b/public/app/types/plugins.ts index 0c5c53eb6f0..101f649eda9 100644 --- a/public/app/types/plugins.ts +++ b/public/app/types/plugins.ts @@ -9,7 +9,7 @@ export interface PanelPlugin { info: any; sort: number; exports?: PluginExports; - isDataPanel?: boolean; + noQueries?: boolean; } export interface Plugin { From b5dbf26dc4f9513ea0e5a0bbbc83c0196573c987 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 12 Feb 2019 09:55:09 +0100 Subject: [PATCH 225/228] chore: PR feedback, shorten boolean check --- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 3 +-- public/app/features/dashboard/panel_editor/PanelEditor.tsx | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index ca2f96d6044..138f299091b 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -131,8 +131,7 @@ export class PanelChrome extends PureComponent { this.renderPanelPlugin(LoadingState.Done, snapshotDataToPanelData(panel), width, height) ) : ( <> - - {plugin.noQueries === true ? + {plugin.noQueries ? this.renderPanelPlugin(LoadingState.Done, null, width, height) : ( { ]; // handle panels that do not have queries tab - if (plugin.noQueries === true) { + if (plugin.noQueries) { // remove queries tab tabs.shift(); // switch tab From 01208ccd68dace126dc71e4bfb94be225f51fa01 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 12 Feb 2019 12:25:10 +0100 Subject: [PATCH 226/228] chore: Rename renderHelper > renderDataPanel and move logic to smaller functions --- .../dashboard/dashgrid/PanelChrome.tsx | 45 ++++++++++++------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 138f299091b..ffc5ef7b904 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -94,6 +94,25 @@ export class PanelChrome extends PureComponent { return !this.props.dashboard.otherPanelInFullscreen(this.props.panel); } + get hasPanelSnapshot() { + const { panel } = this.props; + return panel.snapshotData && panel.snapshotData.length; + } + + get hasDataPanel() { + return !this.props.plugin.noQueries && !this.hasPanelSnapshot; + } + + get getDataForPanel() { + const { panel, plugin } = this.props; + + if (plugin.noQueries) { + return null; + } + + return this.hasPanelSnapshot ? snapshotDataToPanelData(panel) : null; + } + renderPanelPlugin(loading: LoadingState, panelData: PanelData, width: number, height: number): JSX.Element { const { panel, plugin } = this.props; const { timeRange, renderCounter } = this.state; @@ -121,20 +140,14 @@ export class PanelChrome extends PureComponent { ); } - renderHelper = (width: number, height: number): JSX.Element => { - const { panel, plugin } = this.props; + renderDataPanel = (width: number, height: number): JSX.Element => { + const { panel } = this.props; const { refreshCounter, timeRange } = this.state; const { datasource, targets } = panel; return ( <> - {panel.snapshotData && panel.snapshotData.length > 0 ? ( - this.renderPanelPlugin(LoadingState.Done, snapshotDataToPanelData(panel), width, height) - ) : ( - <> - {plugin.noQueries ? - this.renderPanelPlugin(LoadingState.Done, null, width, height) - : ( - { isVisible={this.isVisible} widthPixels={width} refreshCounter={refreshCounter} - onDataResponse={this.onDataResponse} - > + onDataResponse={this.onDataResponse} > {({ loading, panelData }) => { return this.renderPanelPlugin(loading, panelData, width, height); }} - )} - - )} + ) : ( + this.renderPanelPlugin(LoadingState.Done, this.getDataForPanel, width, height) + )} ); } - render() { const { dashboard, panel } = this.props; const { timeInfo } = this.state; @@ -180,7 +191,7 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} /> - {this.renderHelper(width, height)} + {this.renderDataPanel(width, height)} ); }} From d5918498daa5abe3bdde362c383943016d68a407 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 12 Feb 2019 12:26:34 +0100 Subject: [PATCH 227/228] chore: Rename renderDataPanel to renderPanel --- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index ffc5ef7b904..46880a871a3 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -140,7 +140,7 @@ export class PanelChrome extends PureComponent { ); } - renderDataPanel = (width: number, height: number): JSX.Element => { + renderPanel = (width: number, height: number): JSX.Element => { const { panel } = this.props; const { refreshCounter, timeRange } = this.state; const { datasource, targets } = panel; @@ -191,7 +191,7 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} /> - {this.renderDataPanel(width, height)} + {this.renderPanel(width, height)} ); }} From c3965e332d4402a6ddc5c6a8be78bcdd36cf1093 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Tue, 12 Feb 2019 12:27:49 +0100 Subject: [PATCH 228/228] chore: Rename renderPanel to renderPanelBody --- public/app/features/dashboard/dashgrid/PanelChrome.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/PanelChrome.tsx b/public/app/features/dashboard/dashgrid/PanelChrome.tsx index 46880a871a3..309fa118251 100644 --- a/public/app/features/dashboard/dashgrid/PanelChrome.tsx +++ b/public/app/features/dashboard/dashgrid/PanelChrome.tsx @@ -140,7 +140,7 @@ export class PanelChrome extends PureComponent { ); } - renderPanel = (width: number, height: number): JSX.Element => { + renderPanelBody = (width: number, height: number): JSX.Element => { const { panel } = this.props; const { refreshCounter, timeRange } = this.state; const { datasource, targets } = panel; @@ -191,7 +191,7 @@ export class PanelChrome extends PureComponent { scopedVars={panel.scopedVars} links={panel.links} /> - {this.renderPanel(width, height)} + {this.renderPanelBody(width, height)} ); }}