From 3ff362e446fe4455532cc4362c65d99d87f36481 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 24 Nov 2017 09:11:10 +0100 Subject: [PATCH 01/96] Update LICENSE.md --- LICENSE.md | 208 ++++++++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 198 insertions(+), 10 deletions(-) diff --git a/LICENSE.md b/LICENSE.md index 4c6a79691f0..d6456956733 100644 --- a/LICENSE.md +++ b/LICENSE.md @@ -1,14 +1,202 @@ -Copyright 2014-2017 Torkel Ödegaard, Raintank Inc. -Licensed under the Apache License, Version 2.0 (the "License"); you -may not use this file except in compliance with the License. You may -obtain a copy of the License at + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - http://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied. See the License for the specific language governing -permissions and limitations under the License. + 1. Definitions. + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. From 8ce05a7c24f93ec197a1e9430f7baa78df26538e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 24 Nov 2017 09:14:14 +0100 Subject: [PATCH 02/96] Update NOTICE.md --- NOTICE.md | 16 +++------------- 1 file changed, 3 insertions(+), 13 deletions(-) diff --git a/NOTICE.md b/NOTICE.md index 171332f00d4..ca148971b62 100644 --- a/NOTICE.md +++ b/NOTICE.md @@ -1,16 +1,6 @@ -This software is based on Kibana: -======================================== +Copyright 2014-2017 Grafana Labs + +This software is based on Kibana: Copyright 2012-2013 Elasticsearch BV -Licensed under the Apache License, Version 2.0 (the "License"); you -may not use this file except in compliance with the License. You may -obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -implied. See the License for the specific language governing -permissions and limitations under the License. From b752cfee1f2b43d2ba5621728e39dd0338719b12 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 24 Nov 2017 12:59:36 +0100 Subject: [PATCH 03/96] migrated four files to ts, addd some code to config to make it work (#9980) --- public/app/core/config.ts | 4 ++ .../{invited_ctrl.js => invited_ctrl.ts} | 19 +++---- .../{login_ctrl.js => login_ctrl.ts} | 25 ++++----- .../{shareModalCtrl.js => shareModalCtrl.ts} | 25 ++++----- public/app/features/panel/solo_panel_ctrl.js | 51 ------------------- public/app/features/panel/solo_panel_ctrl.ts | 49 ++++++++++++++++++ 6 files changed, 81 insertions(+), 92 deletions(-) rename public/app/core/controllers/{invited_ctrl.js => invited_ctrl.ts} (75%) rename public/app/core/controllers/{login_ctrl.js => login_ctrl.ts} (85%) rename public/app/features/dashboard/{shareModalCtrl.js => shareModalCtrl.ts} (87%) delete mode 100644 public/app/features/panel/solo_panel_ctrl.js create mode 100644 public/app/features/panel/solo_panel_ctrl.ts diff --git a/public/app/core/config.ts b/public/app/core/config.ts index 63f01bea458..e54d62d7c0e 100644 --- a/public/app/core/config.ts +++ b/public/app/core/config.ts @@ -17,6 +17,10 @@ class Settings { alertingEnabled: boolean; authProxyEnabled: boolean; ldapEnabled: boolean; + oauth: any; + disableUserSignUp: boolean; + loginHint: any; + loginError: any; constructor(options) { var defaults = { diff --git a/public/app/core/controllers/invited_ctrl.js b/public/app/core/controllers/invited_ctrl.ts similarity index 75% rename from public/app/core/controllers/invited_ctrl.js rename to public/app/core/controllers/invited_ctrl.ts index dfcc198f3aa..ed4bd1793b8 100644 --- a/public/app/core/controllers/invited_ctrl.js +++ b/public/app/core/controllers/invited_ctrl.ts @@ -1,14 +1,10 @@ -define([ - 'angular', - '../core_module', - 'app/core/config', -], -function (angular, coreModule, config) { - 'use strict'; +import coreModule from '../core_module'; +import config from 'app/core/config'; - config = config.default; +export class InvitedCtrl { - coreModule.default.controller('InvitedCtrl', function($scope, $routeParams, contextSrv, backendSrv) { + /** @ngInject */ + constructor($scope, $routeParams, contextSrv, backendSrv) { contextSrv.sidemenu = false; $scope.formModel = {}; @@ -35,6 +31,7 @@ function (angular, coreModule, config) { }; $scope.init(); + } +} - }); -}); +coreModule.controller('InvitedCtrl', InvitedCtrl); diff --git a/public/app/core/controllers/login_ctrl.js b/public/app/core/controllers/login_ctrl.ts similarity index 85% rename from public/app/core/controllers/login_ctrl.js rename to public/app/core/controllers/login_ctrl.ts index 5f02811a143..11bebbce8e6 100644 --- a/public/app/core/controllers/login_ctrl.js +++ b/public/app/core/controllers/login_ctrl.ts @@ -1,15 +1,11 @@ -define([ - 'angular', - 'lodash', - '../core_module', - 'app/core/config', -], -function (angular, _, coreModule, config) { - 'use strict'; +import _ from 'lodash'; +import coreModule from '../core_module'; +import config from 'app/core/config'; - config = config.default; +export class LoginCtrl { - coreModule.default.controller('LoginCtrl', function($scope, backendSrv, contextSrv, $location) { + /** @ngInject */ + constructor($scope, backendSrv, contextSrv, $location) { $scope.formModel = { user: '', email: '', @@ -74,8 +70,7 @@ function (angular, _, coreModule, config) { if (params.redirect && params.redirect[0] === '/') { window.location.href = config.appSubUrl + params.redirect; - } - else if (result.redirectUrl) { + } else if (result.redirectUrl) { window.location.href = result.redirectUrl; } else { window.location.href = config.appSubUrl + '/'; @@ -84,5 +79,7 @@ function (angular, _, coreModule, config) { }; $scope.init(); - }); -}); + } +} + +coreModule.controller('LoginCtrl', LoginCtrl); diff --git a/public/app/features/dashboard/shareModalCtrl.js b/public/app/features/dashboard/shareModalCtrl.ts similarity index 87% rename from public/app/features/dashboard/shareModalCtrl.js rename to public/app/features/dashboard/shareModalCtrl.ts index e750b631771..1ddec697eb2 100644 --- a/public/app/features/dashboard/shareModalCtrl.js +++ b/public/app/features/dashboard/shareModalCtrl.ts @@ -1,18 +1,11 @@ -define(['angular', - 'lodash', - 'jquery', - 'moment', - 'app/core/config', -], -function (angular, _, $, moment, config) { - 'use strict'; +import angular from 'angular'; +import moment from 'moment'; +import config from 'app/core/config'; - config = config.default; - - var module = angular.module('grafana.controllers'); - - module.controller('ShareModalCtrl', function($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) { +export class ShareModalCtrl { + /** @ngInject */ + constructor($scope, $rootScope, $location, $timeout, timeSrv, templateSrv, linkSrv) { $scope.options = { forCurrent: true, includeTemplateVars: true, theme: 'current' }; $scope.editor = { index: $scope.tabIndex || 0}; @@ -93,7 +86,7 @@ function (angular, _, $, moment, config) { $scope.getShareUrl = function() { return $scope.shareUrl; }; + } +} - }); - -}); +angular.module('grafana.controllers').controller('ShareModalCtrl', ShareModalCtrl); diff --git a/public/app/features/panel/solo_panel_ctrl.js b/public/app/features/panel/solo_panel_ctrl.js deleted file mode 100644 index a25c5f90c27..00000000000 --- a/public/app/features/panel/solo_panel_ctrl.js +++ /dev/null @@ -1,51 +0,0 @@ -define([ - 'angular', - 'jquery', -], -function (angular, $) { - "use strict"; - - var module = angular.module('grafana.routes'); - - module.controller('SoloPanelCtrl', function($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) { - - var panelId; - - $scope.init = function() { - contextSrv.sidemenu = false; - - var params = $location.search(); - panelId = parseInt(params.panelId); - - $scope.onAppEvent("dashboard-initialized", $scope.initPanelScope); - - dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { - result.meta.soloMode = true; - $scope.initDashboard(result, $scope); - }); - }; - - $scope.initPanelScope = function() { - var panelInfo = $scope.dashboard.getPanelInfoById(panelId); - - // fake row ctrl scope - $scope.ctrl = { - row: panelInfo.row, - dashboard: $scope.dashboard, - }; - - $scope.ctrl.row.height = $(window).height(); - $scope.panel = panelInfo.panel; - $scope.$index = 0; - - if (!$scope.panel) { - $scope.appEvent('alert-error', ['Panel not found', '']); - return; - } - - $scope.panel.span = 12; - }; - - $scope.init(); - }); -}); diff --git a/public/app/features/panel/solo_panel_ctrl.ts b/public/app/features/panel/solo_panel_ctrl.ts new file mode 100644 index 00000000000..2157d840e3e --- /dev/null +++ b/public/app/features/panel/solo_panel_ctrl.ts @@ -0,0 +1,49 @@ +import angular from 'angular'; +import $ from 'jquery'; + +export class SoloPanelCtrl { + + /** @ngInject */ + constructor($scope, $routeParams, $location, dashboardLoaderSrv, contextSrv) { + var panelId; + + $scope.init = function() { + contextSrv.sidemenu = false; + + var params = $location.search(); + panelId = parseInt(params.panelId); + + $scope.onAppEvent("dashboard-initialized", $scope.initPanelScope); + + dashboardLoaderSrv.loadDashboard($routeParams.type, $routeParams.slug).then(function(result) { + result.meta.soloMode = true; + $scope.initDashboard(result, $scope); + }); + }; + + $scope.initPanelScope = function() { + var panelInfo = $scope.dashboard.getPanelInfoById(panelId); + + // fake row ctrl scope + $scope.ctrl = { + row: panelInfo.row, + dashboard: $scope.dashboard, + }; + + $scope.ctrl.row.height = $(window).height(); + $scope.panel = panelInfo.panel; + $scope.$index = 0; + + if (!$scope.panel) { + $scope.appEvent('alert-error', ['Panel not found', '']); + return; + } + + $scope.panel.span = 12; + }; + + $scope.init(); + } +} + +angular.module('grafana.routes').controller('SoloPanelCtrl', SoloPanelCtrl); From 015932fd026566c3fe5561dd2995a4a8d30c2717 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 24 Nov 2017 13:38:54 +0100 Subject: [PATCH 04/96] migrated four files from js to ts --- public/app/core/controllers/all.js | 9 --------- public/app/core/controllers/all.ts | 7 +++++++ .../{error_ctrl.js => error_ctrl.ts} | 19 ++++++++----------- ...son_editor_ctrl.js => json_editor_ctrl.ts} | 18 ++++++++---------- ...assword_ctrl.js => reset_password_ctrl.ts} | 18 ++++++++---------- 5 files changed, 31 insertions(+), 40 deletions(-) delete mode 100644 public/app/core/controllers/all.js create mode 100644 public/app/core/controllers/all.ts rename public/app/core/controllers/{error_ctrl.js => error_ctrl.ts} (54%) rename public/app/core/controllers/{json_editor_ctrl.js => json_editor_ctrl.ts} (58%) rename public/app/core/controllers/{reset_password_ctrl.js => reset_password_ctrl.ts} (80%) diff --git a/public/app/core/controllers/all.js b/public/app/core/controllers/all.js deleted file mode 100644 index 54631586c2f..00000000000 --- a/public/app/core/controllers/all.js +++ /dev/null @@ -1,9 +0,0 @@ -define([ - './inspect_ctrl', - './json_editor_ctrl', - './login_ctrl', - './invited_ctrl', - './signup_ctrl', - './reset_password_ctrl', - './error_ctrl', -], function () {}); diff --git a/public/app/core/controllers/all.ts b/public/app/core/controllers/all.ts new file mode 100644 index 00000000000..0dbcdf4cb28 --- /dev/null +++ b/public/app/core/controllers/all.ts @@ -0,0 +1,7 @@ +import './inspect_ctrl'; +import './json_editor_ctrl'; +import './login_ctrl'; +import './invited_ctrl'; +import './signup_ctrl'; +import './reset_password_ctrl'; +import './error_ctrl'; diff --git a/public/app/core/controllers/error_ctrl.js b/public/app/core/controllers/error_ctrl.ts similarity index 54% rename from public/app/core/controllers/error_ctrl.js rename to public/app/core/controllers/error_ctrl.ts index fd4081186be..fe894a69806 100644 --- a/public/app/core/controllers/error_ctrl.js +++ b/public/app/core/controllers/error_ctrl.ts @@ -1,13 +1,10 @@ -define([ - 'angular', - 'app/core/config', - '../core_module', -], -function (angular, config, coreModule) { - 'use strict'; +import config from 'app/core/config'; +import coreModule from '../core_module'; - coreModule.default.controller('ErrorCtrl', function($scope, contextSrv, navModelSrv) { +export class ErrorCtrl { + /** @ngInject */ + constructor($scope, contextSrv, navModelSrv) { $scope.navModel = navModelSrv.getNotFoundNav(); $scope.appSubUrl = config.appSubUrl; @@ -17,7 +14,7 @@ function (angular, config, coreModule) { $scope.$on('$destroy', function() { contextSrv.sidemenu = showSideMenu; }); + } +} - }); - -}); +coreModule.controller('ErrorCtrl', ErrorCtrl); diff --git a/public/app/core/controllers/json_editor_ctrl.js b/public/app/core/controllers/json_editor_ctrl.ts similarity index 58% rename from public/app/core/controllers/json_editor_ctrl.js rename to public/app/core/controllers/json_editor_ctrl.ts index 7d7d56fa96b..ba6d9abfd74 100644 --- a/public/app/core/controllers/json_editor_ctrl.js +++ b/public/app/core/controllers/json_editor_ctrl.ts @@ -1,12 +1,10 @@ -define([ - 'angular', - '../core_module', -], -function (angular, coreModule) { - 'use strict'; +import angular from 'angular'; +import coreModule from '../core_module'; - coreModule.default.controller('JsonEditorCtrl', function($scope) { +export class JsonEditorCtrl { + /** @ngInject */ + constructor($scope) { $scope.json = angular.toJson($scope.object, true); $scope.canUpdate = $scope.updateHandler !== void 0 && $scope.contextSrv.isEditor; @@ -14,7 +12,7 @@ function (angular, coreModule) { var newObject = angular.fromJson($scope.json); $scope.updateHandler(newObject, $scope.object); }; + } +} - }); - -}); +coreModule.controller('JsonEditorCtrl', JsonEditorCtrl); diff --git a/public/app/core/controllers/reset_password_ctrl.js b/public/app/core/controllers/reset_password_ctrl.ts similarity index 80% rename from public/app/core/controllers/reset_password_ctrl.js rename to public/app/core/controllers/reset_password_ctrl.ts index 4cf014d2482..524cfb7af64 100644 --- a/public/app/core/controllers/reset_password_ctrl.js +++ b/public/app/core/controllers/reset_password_ctrl.ts @@ -1,11 +1,9 @@ -define([ - 'angular', - '../core_module', -], -function (angular, coreModule) { - 'use strict'; +import coreModule from '../core_module'; - coreModule.default.controller('ResetPasswordCtrl', function($scope, contextSrv, backendSrv, $location) { +export class ResetPasswordCtrl { + + /** @ngInject */ + constructor($scope, contextSrv, backendSrv, $location) { contextSrv.sidemenu = false; $scope.formModel = {}; $scope.mode = 'send'; @@ -37,7 +35,7 @@ function (angular, coreModule) { $location.path('login'); }); }; + } +} - }); - -}); +coreModule.controller('ResetPasswordCtrl', ResetPasswordCtrl); From b19b68df08fd27878dbf7f813532bd0aedf2fc02 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 28 Nov 2017 11:27:29 +0100 Subject: [PATCH 05/96] styling changes for light theme --- public/sass/_variables.light.scss | 28 +++++++++++++++++------ public/sass/components/_filter-table.scss | 6 +++-- public/sass/layout/_page.scss | 11 +++++---- 3 files changed, 31 insertions(+), 14 deletions(-) diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 61c2c34962a..621ec2a51a1 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -13,7 +13,7 @@ $theme-name: light; $black: #000; // ------------------------- -$black: #000; +/*$black: #000; $dark-1: #141414; $dark-2: #1d1d1f; $dark-3: #262628; @@ -25,7 +25,21 @@ $gray-3: #b3b3b3; $gray-4: #D8D9DA; $gray-5: #ECECEC; $gray-6: #f4f5f8; -$gray-7: #fbfbfb; +$gray-7: #fbfbfb;*/ + +$black: #000; +$dark-1: #121314; +$dark-2: #1d1d1f; +$dark-3: #272729; +$dark-4: #38383b; +$dark-5: #444547; +$gray-1: #525357; +$gray-2: #7a7c80; +$gray-3: #b2b3b8; +$gray-4: #d5d6db; +$gray-5: #e9eaf0; +$gray-6: #f4f5f8; +$gray-7: #fafbfc; $white: #fff; @@ -185,8 +199,8 @@ $input-invalid-border-color: lighten($red, 5%); // Sidemenu // ------------------------- -$side-menu-bg: $body-bg; -$side-menu-item-hover-bg: $gray-6; +$side-menu-bg: $gray-7; +$side-menu-item-hover-bg: $gray-5; $side-menu-shadow: 0 0 5px #c2c2c2; // Menu dropdowns @@ -198,7 +212,7 @@ $menu-dropdown-shadow: 5px 5px 20px -5px $gray-4; // Breadcrumb // ------------------------- -$page-nav-bg: #eaebee; +$page-nav-bg: $gray-5; $page-nav-shadow: 5px 5px 20px -5px $gray-4; $page-nav-breadcrumb-color: $black; $breadcrumb-hover-hl: #d9dadd; @@ -250,8 +264,8 @@ $wellBackground: $gray-3; // ------------------------- $navbarHeight: 52px; -$navbarBackgroundHighlight: #f8f8f8; -$navbarBackground: #f2f3f7; +$navbarBackgroundHighlight: white; +$navbarBackground: $white; $navbarBorder: 1px solid $gray-4; $navbarShadow: 0 0 3px #c1c1c1; diff --git a/public/sass/components/_filter-table.scss b/public/sass/components/_filter-table.scss index 5b6f8695a31..0d81c4a74d0 100644 --- a/public/sass/components/_filter-table.scss +++ b/public/sass/components/_filter-table.scss @@ -17,7 +17,8 @@ tbody { tr:nth-child(odd) { - background: $dark-2; + //background: $dark-2; + background: $gray-6; } } @@ -34,7 +35,8 @@ padding: $table-cell-padding; line-height: 30px; height: 30px; - border-bottom: 1px solid black; + //border-bottom: 1px solid black; + border-bottom: 1px solid #ECECEC; white-space: nowrap; &.filter-table__switch-cell { diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index 310ecd51532..a13e703717d 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -144,6 +144,7 @@ line-height: 36px; padding: 0 7px 0 37px; @include gradientBar($btn-inverse-bg, $btn-inverse-bg-hl, $btn-inverse-text-color); + //background:linear-gradient(#edeff2, #e3e4e7); position: relative; box-shadow: $card-shadow; @@ -164,14 +165,14 @@ &.active, &:hover { - background: #333; - background: linear-gradient(#333, #000); + background: #fff;//#333; + background: linear-gradient($gray-4, darken($gray-4, 5%)); //linear-gradient(#333, #000); } &.active:after, &:hover:after { - background: #333; - background: linear-gradient(135deg, #333, #000); + background: #fff;//#333; + background: linear-gradient(135deg, $gray-4,darken($gray-4, 5%));////linear-gradient(135deg, #333, #000); } &:after { @@ -192,7 +193,7 @@ background: linear-gradient(135deg, $btn-inverse-bg, $btn-inverse-bg-hl); // stylish arrow design using box shadow - box-shadow: 2px -2px 0 2px rgb(35, 31, 31), 3px -3px 0 2px rgba(255, 255, 255, 0.1); + box-shadow: 2px -2px 0 2px rgb(250, 250, 250), 3px -3px 0 2px rgba(255, 255, 255, 0.1); // 5px - for rounded arrows and // 50px - to prevent hover glitches on the border created using shadows*/ From afd0fc3652c168d5789396a2cf10dfecf085ddba Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 28 Nov 2017 13:27:43 +0100 Subject: [PATCH 06/96] export view json now templatized, fixes #10001 --- public/app/features/dashboard/export/export_modal.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/features/dashboard/export/export_modal.ts b/public/app/features/dashboard/export/export_modal.ts index 826086ebdf0..1b8aa4ae5d8 100644 --- a/public/app/features/dashboard/export/export_modal.ts +++ b/public/app/features/dashboard/export/export_modal.ts @@ -26,7 +26,7 @@ export class DashExportCtrl { } saveJson() { - var clone = this.dashboardSrv.getCurrent().getSaveModelClone(); + var clone = this.dash; this.$scope.$root.appEvent('show-json-editor', { object: clone, From c0e087640bd8227b28b74f357e0d314ff4c79308 Mon Sep 17 00:00:00 2001 From: bergquist Date: Tue, 28 Nov 2017 15:05:53 +0100 Subject: [PATCH 07/96] test: close file before deleting --- pkg/log/file_test.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/log/file_test.go b/pkg/log/file_test.go index 458a39d754b..3e98e0786cc 100644 --- a/pkg/log/file_test.go +++ b/pkg/log/file_test.go @@ -38,6 +38,7 @@ func TestLogFile(t *testing.T) { So(fileLogWrite.maxlines_curlines, ShouldEqual, 3) }) + fileLogWrite.Close() err = os.Remove(fileLogWrite.Filename) So(err, ShouldBeNil) }) From 3f95180c983602e27aa8d7bd3ff6d15d2827c460 Mon Sep 17 00:00:00 2001 From: jomenxiao Date: Tue, 28 Nov 2017 22:20:22 +0800 Subject: [PATCH 08/96] fix render http[get] params error --- pkg/api/render.go | 6 +++++- pkg/util/url.go | 11 ++++++++--- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/pkg/api/render.go b/pkg/api/render.go index 5284c7831bb..fdf53e6730d 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -10,7 +10,11 @@ import ( ) func RenderToPng(c *middleware.Context) { - queryReader := util.NewUrlQueryReader(c.Req.URL) + queryReader, err := util.NewUrlQueryReader(c.Req.URL) + if err != nil { + c.Handle(400, "Rander parameters error", err) + return + } queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery) renderOpts := &renderer.RenderOpts{ diff --git a/pkg/util/url.go b/pkg/util/url.go index ba452596a2b..c82dcef67c5 100644 --- a/pkg/util/url.go +++ b/pkg/util/url.go @@ -9,10 +9,15 @@ type UrlQueryReader struct { values url.Values } -func NewUrlQueryReader(url *url.URL) *UrlQueryReader { - return &UrlQueryReader{ - values: url.Query(), +func NewUrlQueryReader(urlInfo *url.URL) (*UrlQueryReader, error) { + u, err := url.ParseQuery(urlInfo.String()) + if err != nil { + return nil, err } + + return &UrlQueryReader{ + values: u, + }, nil } func (r *UrlQueryReader) Get(name string, def string) string { From 98bb8bf7612a133a47ee171d3a256a29473a4f5f Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Sat, 25 Nov 2017 02:35:29 +0900 Subject: [PATCH 09/96] prometheus nested query support --- .../plugins/datasource/prometheus/datasource.ts | 8 ++++++++ .../prometheus/specs/datasource_specs.ts | 15 +++++++++++++++ 2 files changed, 23 insertions(+) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index c4939b5b2fa..7e21852f476 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -155,6 +155,7 @@ export class PrometheusDatasource { // Only replace vars in expression after having (possibly) updated interval vars query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr); + query.expr = this.replaceNestedQuery(query.expr, options); query.requestId = options.panelId + target.refId; return query; } @@ -269,6 +270,13 @@ export class PrometheusDatasource { }); } + replaceNestedQuery(query, options) { + return query.replace(/\#([A-Z])/g, (match, g1) => { + let replaceTarget = options.targets.find((t) => { return t.refId === g1; }); + return replaceTarget ? replaceTarget.expr : match; + }); + } + transformMetricData(md, options, start, end, step) { var dps = [], metricLabel = null; diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index 9d528c735de..67d861da1c6 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -590,4 +590,19 @@ describe('PrometheusDatasource', function() { expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); }); }); + describe('The nested query', function() { + it('should generate correct query', function() { + let query = 'sum(rate(#A[1m]))'; + let options = { + targets: [ + { + refId: 'A', + expr: 'http_requests_total' + } + ] + }; + let result = ctx.ds.replaceNestedQuery(query, options); + expect(result).to.be('sum(rate(http_requests_total[1m]))'); + }); + }); }); From 08d470d76a94015287e66ff8c7a2d969d1febd74 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Nov 2017 12:51:14 +0300 Subject: [PATCH 10/96] dashboard: initial repeat row implementation --- .../app/features/dashboard/dashboard_model.ts | 78 +++++++++++++------ .../features/dashboard/specs/repeat.jest.ts | 16 ++-- 2 files changed, 67 insertions(+), 27 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 0e37acb201a..9481ef6d63b 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -274,7 +274,7 @@ export class DashboardModel { return sourcePanel; } - var clone = new PanelModel(sourcePanel.getSaveModel()); + let clone = new PanelModel(sourcePanel.getSaveModel()); clone.id = this.getNextPanelId(); if (sourcePanel.type === 'row') { @@ -282,7 +282,7 @@ export class DashboardModel { let rowPanels = this.getRowPanels(sourcePanelIndex); clone.panels = _.map(rowPanels, panel => panel.getSaveModel()); - // insert after preceding row's panels + // insert copied row after preceding row's panels let insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex); this.panels.splice(insertPos, 0, clone); } else { @@ -300,12 +300,12 @@ export class DashboardModel { } repeatPanel(panel: PanelModel, panelIndex: number) { - var variable = _.find(this.templating.list, {name: panel.repeat}); + let variable = _.find(this.templating.list, {name: panel.repeat}); if (!variable) { return; } - var selected; + let selected; if (variable.current.text === 'All') { selected = variable.options.slice(1, variable.options.length); } else { @@ -317,38 +317,72 @@ export class DashboardModel { let yPos = panel.gridPos.y; for (let index = 0; index < selected.length; index++) { - var option = selected[index]; - var copy = this.getPanelRepeatClone(panel, index, panelIndex); + let option = selected[index]; + let copy = this.getPanelRepeatClone(panel, index, panelIndex); copy.scopedVars = {}; copy.scopedVars[variable.name] = option; if (copy.type === 'row') { // place row below row panels - } + let rowHeight = this.getRowHeight(copy); + if (rowHeight) { + copy.gridPos.y += rowHeight * index; + let rowPanels = copy.panels; + // insert after preceding row's panels + let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; + _.each(rowPanels, (rowPanel, i) => { + let cloneRowPanel = new PanelModel(rowPanel); + cloneRowPanel.id = this.getNextPanelId(); + cloneRowPanel.repeatIteration = this.iteration; + cloneRowPanel.repeatPanelId = rowPanel.id; + cloneRowPanel.repeat = null; + cloneRowPanel.gridPos.y += rowHeight * index; + this.panels.splice(insertPos+i, 0, cloneRowPanel); + }); + copy.panels = []; + yPos += rowHeight; - if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { - copy.gridPos.y = yPos; - yPos += copy.gridPos.h; + // Update gridPos for panels below + for (let i = insertPos+rowPanels.length; i< this.panels.length; i++) { + this.panels[i].gridPos.y += yPos; + } + } } else { - // set width based on how many are selected - // assumed the repeated panels should take up full row width - - copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selected.length, minWidth); - copy.gridPos.x = xPos; - copy.gridPos.y = yPos; - - xPos += copy.gridPos.w; - - // handle overflow by pushing down one row - if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { - xPos = 0; + if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { + copy.gridPos.y = yPos; yPos += copy.gridPos.h; + } else { + // set width based on how many are selected + // assumed the repeated panels should take up full row width + + copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selected.length, minWidth); + copy.gridPos.x = xPos; + copy.gridPos.y = yPos; + + xPos += copy.gridPos.w; + + // handle overflow by pushing down one row + if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { + xPos = 0; + yPos += copy.gridPos.h; + } } } } } + getRowHeight(rowPanel: PanelModel): number { + if (!rowPanel.panels || rowPanel.panels.length === 0) { + return 0; + } + const positions = _.map(rowPanel.panels, 'gridPos'); + const maxPos = _.maxBy(positions, (pos) => { + return pos.y + pos.h; + }); + return maxPos.h + 1; + } + removePanel(panel: PanelModel) { var index = _.indexOf(this.panels, panel); this.panels.splice(index, 1); diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 2ad1400990c..7f5d382fe9c 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -1,3 +1,4 @@ +import _ from 'lodash'; import {DashboardModel} from '../dashboard_model'; jest.mock('app/core/services/context_srv', () => ({ @@ -146,7 +147,7 @@ describe('given dashboard with panel repeat in vertical direction', function() { }); }); -describe.skip('given dashboard with row repeat', function() { +describe('given dashboard with row repeat', function() { var dashboard; beforeEach(function() { @@ -177,14 +178,19 @@ describe.skip('given dashboard with row repeat', function() { }); it('should not repeat only row', function() { - expect(dashboard.panels[1].type).toBe('graph'); + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual([ + 'row', 'graph', 'graph', + 'row', 'graph', 'graph', + 'row', 'graph' + ]); }); - // + // it('should set scopedVars on panels', function() { // expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}}) // }); - // - // it.skip('should repeat row and panels below two times', function() { + + // it('should repeat row and panels below two times', function() { // expect(dashboard.panels).toMatchObject([ // // first (original row) // {id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}}, From e6bf266c4be87a54d65940dadecbf5c075e57fb5 Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 29 Nov 2017 11:15:27 +0100 Subject: [PATCH 11/96] formatting in build file --- build.go | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/build.go b/build.go index a1d1d3012ab..1c61c72d5dc 100644 --- a/build.go +++ b/build.go @@ -95,9 +95,9 @@ func main() { case "package": grunt(gruntBuildArg("release")...) - if runtime.GOOS != "windows" { - createLinuxPackages() - } + if runtime.GOOS != "windows" { + createLinuxPackages() + } case "pkg-rpm": grunt(gruntBuildArg("release")...) From 554c7ba96f28c48a3d868a0a663f605aefb4a43c Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Wed, 29 Nov 2017 11:16:45 +0100 Subject: [PATCH 12/96] notifier: Fixes path for uploaded image for Slack notifier Fixes #10012 --- pkg/services/alerting/notifiers/slack.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pkg/services/alerting/notifiers/slack.go b/pkg/services/alerting/notifiers/slack.go index ed1451da419..e051a71740a 100644 --- a/pkg/services/alerting/notifiers/slack.go +++ b/pkg/services/alerting/notifiers/slack.go @@ -6,6 +6,7 @@ import ( "io" "mime/multipart" "os" + "path/filepath" "time" "github.com/grafana/grafana/pkg/bus" @@ -176,7 +177,7 @@ func (this *SlackNotifier) Notify(evalContext *alerting.EvalContext) error { func SlackFileUpload(evalContext *alerting.EvalContext, log log.Logger, url string, recipient string, token string) error { if evalContext.ImageOnDiskPath == "" { - evalContext.ImageOnDiskPath = "public/img/mixed_styles.png" + evalContext.ImageOnDiskPath = filepath.Join(setting.HomePath, "public/img/mixed_styles.png") } log.Info("Uploading to slack via file.upload API") headers, uploadBody, err := GenerateSlackBody(evalContext.ImageOnDiskPath, token, recipient) From dd9c727e6050c515c62b5a97955b4a16c606eec5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Nov 2017 14:14:43 +0300 Subject: [PATCH 13/96] repeat row: handle collapsed rows --- .../app/features/dashboard/dashboard_model.ts | 71 +++++++++++++------ .../features/dashboard/specs/repeat.jest.ts | 56 ++++++++++++++- 2 files changed, 104 insertions(+), 23 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index 9481ef6d63b..cc5c743cb41 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -181,6 +181,14 @@ export class DashboardModel { if (panel.id > max) { max = panel.id; } + + if (panel.collapsed) { + for (let rowPanel of panel.panels) { + if (rowPanel.id > max) { + max = rowPanel.id; + } + } + } } return max + 1; @@ -266,6 +274,7 @@ export class DashboardModel { this.sortPanelsByGridPos(); this.events.emit('repeats-processed'); + console.log(this.panels); } getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { @@ -279,11 +288,18 @@ export class DashboardModel { if (sourcePanel.type === 'row') { // for row clones we need to figure out panels under row to clone and where to insert clone - let rowPanels = this.getRowPanels(sourcePanelIndex); - clone.panels = _.map(rowPanels, panel => panel.getSaveModel()); - - // insert copied row after preceding row's panels - let insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex); + let rowPanels, insertPos; + if (sourcePanel.collapsed) { + rowPanels = sourcePanel.panels; + clone.panels = _.cloneDeep(rowPanels); + // insert copied row after preceding row + insertPos = sourcePanelIndex + valueIndex; + } else { + rowPanels = this.getRowPanels(sourcePanelIndex); + clone.panels = _.map(rowPanels, panel => panel.getSaveModel()); + // insert copied row after preceding row's panels + insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex); + } this.panels.splice(insertPos, 0, clone); } else { // insert after source panel + value index @@ -324,27 +340,42 @@ export class DashboardModel { copy.scopedVars[variable.name] = option; if (copy.type === 'row') { - // place row below row panels let rowHeight = this.getRowHeight(copy); if (rowHeight) { - copy.gridPos.y += rowHeight * index; + let panelsBelowIndex; let rowPanels = copy.panels; - // insert after preceding row's panels + // insert after 'row' panel let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; - _.each(rowPanels, (rowPanel, i) => { - let cloneRowPanel = new PanelModel(rowPanel); - cloneRowPanel.id = this.getNextPanelId(); - cloneRowPanel.repeatIteration = this.iteration; - cloneRowPanel.repeatPanelId = rowPanel.id; - cloneRowPanel.repeat = null; - cloneRowPanel.gridPos.y += rowHeight * index; - this.panels.splice(insertPos+i, 0, cloneRowPanel); - }); - copy.panels = []; - yPos += rowHeight; + + if (copy.collapsed) { + copy.gridPos.y += index; + yPos += index; + panelsBelowIndex = panelIndex + index + 1; + _.each(copy.panels, (panel, i) => { + panel.id = this.getNextPanelId(); + panel.repeatIteration = this.iteration; + panel.repeatPanelId = rowPanels[i].id; + panel.repeat = null; + copy.panels[i] = panel; + }); + } else { + _.each(rowPanels, (rowPanel, i) => { + let cloneRowPanel = new PanelModel(rowPanel); + cloneRowPanel.id = this.getNextPanelId(); + cloneRowPanel.repeatIteration = this.iteration; + cloneRowPanel.repeatPanelId = rowPanel.id; + cloneRowPanel.repeat = null; + cloneRowPanel.gridPos.y += rowHeight * index; + this.panels.splice(insertPos+i, 0, cloneRowPanel); + }); + copy.panels = []; + copy.gridPos.y += rowHeight * index; + yPos += rowHeight; + panelsBelowIndex = insertPos+rowPanels.length; + } // Update gridPos for panels below - for (let i = insertPos+rowPanels.length; i< this.panels.length; i++) { + for (let i = panelsBelowIndex; i< this.panels.length; i++) { this.panels[i].gridPos.y += yPos; } } diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index 7f5d382fe9c..cb1da3e767a 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -148,10 +148,10 @@ describe('given dashboard with panel repeat in vertical direction', function() { }); describe('given dashboard with row repeat', function() { - var dashboard; + let dashboard, dashboardJSON; beforeEach(function() { - dashboard = new DashboardModel({ + dashboardJSON = { panels: [ {id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}}, {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, @@ -173,7 +173,8 @@ describe('given dashboard with row repeat', function() { ] }] } - }); + }; + dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); }); @@ -186,6 +187,55 @@ describe('given dashboard with row repeat', function() { ]); }); + it('should repeat only row if it is collapsed', function() { + dashboardJSON.panels = [ + { + id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}, + panels: [ + {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, + {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, + ] + }, + {id: 4, type: 'row', gridPos: {x: 0, y: 1, h: 1 , w: 24}}, + {id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}}, + ]; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual([ + 'row', 'row', 'row', 'graph' + ]); + expect(dashboard.panels[0].panels).toHaveLength(2); + expect(dashboard.panels[1].panels).toHaveLength(2); + }); + + it('should assign unique ids for repeated panels', function() { + dashboardJSON.panels = [ + { + id: 1, type: 'row', collapsed: true, repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}, + panels: [ + {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, + {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, + ] + }, + {id: 4, type: 'row', gridPos: {x: 0, y: 1, h: 1 , w: 24}}, + {id: 5, type: 'graph', gridPos: {x: 0, y: 2, h: 1 , w: 12}}, + ]; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_ids = _.flattenDeep(_.map(dashboard.panels, (panel) => { + let ids = []; + if (panel.panels && panel.panels.length) { + ids = _.map(panel.panels, 'id'); + } + ids.push(panel.id); + return ids; + })); + expect(panel_ids.length).toEqual(_.uniq(panel_ids).length); + }); + // it('should set scopedVars on panels', function() { // expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}}) // }); From 36ef1865875b677ec3b71de822c250e10b2fc4ac Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 29 Nov 2017 16:40:13 +0300 Subject: [PATCH 14/96] repeat row: expose scopedVars to row panels --- .../app/features/dashboard/dashboard_model.ts | 121 +++++++++++------- .../features/dashboard/specs/repeat.jest.ts | 17 +++ 2 files changed, 93 insertions(+), 45 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index cc5c743cb41..c54a9460c08 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -274,7 +274,6 @@ export class DashboardModel { this.sortPanelsByGridPos(); this.events.emit('repeats-processed'); - console.log(this.panels); } getPanelRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { @@ -286,26 +285,43 @@ export class DashboardModel { let clone = new PanelModel(sourcePanel.getSaveModel()); clone.id = this.getNextPanelId(); - if (sourcePanel.type === 'row') { - // for row clones we need to figure out panels under row to clone and where to insert clone - let rowPanels, insertPos; - if (sourcePanel.collapsed) { - rowPanels = sourcePanel.panels; - clone.panels = _.cloneDeep(rowPanels); - // insert copied row after preceding row - insertPos = sourcePanelIndex + valueIndex; - } else { - rowPanels = this.getRowPanels(sourcePanelIndex); - clone.panels = _.map(rowPanels, panel => panel.getSaveModel()); - // insert copied row after preceding row's panels - insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex); + // insert after source panel + value index + this.panels.splice(sourcePanelIndex+valueIndex, 0, clone); + + clone.repeatIteration = this.iteration; + clone.repeatPanelId = sourcePanel.id; + clone.repeat = null; + return clone; + } + + getRowRepeatClone(sourcePanel, valueIndex, sourcePanelIndex) { + // if first clone return source + if (valueIndex === 0) { + if (!sourcePanel.collapsed) { + let rowPanels = this.getRowPanels(sourcePanelIndex); + sourcePanel.panels = rowPanels; } - this.panels.splice(insertPos, 0, clone); - } else { - // insert after source panel + value index - this.panels.splice(sourcePanelIndex+valueIndex, 0, clone); + return sourcePanel; } + let clone = new PanelModel(sourcePanel.getSaveModel()); + clone.id = this.getNextPanelId(); + + // for row clones we need to figure out panels under row to clone and where to insert clone + let rowPanels, insertPos; + if (sourcePanel.collapsed) { + rowPanels = _.cloneDeep(sourcePanel.panels); + clone.panels = rowPanels; + // insert copied row after preceding row + insertPos = sourcePanelIndex + valueIndex; + } else { + rowPanels = this.getRowPanels(sourcePanelIndex); + clone.panels = _.map(rowPanels, panel => panel.getSaveModel()); + // insert copied row after preceding row's panels + insertPos = sourcePanelIndex + ((rowPanels.length + 1)*valueIndex); + } + this.panels.splice(insertPos, 0, clone); + clone.repeatIteration = this.iteration; clone.repeatPanelId = sourcePanel.id; clone.repeat = null; @@ -334,32 +350,43 @@ export class DashboardModel { for (let index = 0; index < selected.length; index++) { let option = selected[index]; - let copy = this.getPanelRepeatClone(panel, index, panelIndex); + let copy; - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; + if (panel.type === 'row') { + copy = this.getRowRepeatClone(panel, index, panelIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; - if (copy.type === 'row') { let rowHeight = this.getRowHeight(copy); - if (rowHeight) { - let panelsBelowIndex; - let rowPanels = copy.panels; - // insert after 'row' panel - let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; + // if (rowHeight) { + let panelsBelowIndex; + let rowPanels = copy.panels || []; + // insert after 'row' panel + let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; - if (copy.collapsed) { - copy.gridPos.y += index; - yPos += index; - panelsBelowIndex = panelIndex + index + 1; - _.each(copy.panels, (panel, i) => { + if (copy.collapsed) { + copy.gridPos.y += index; + yPos += index; + panelsBelowIndex = panelIndex + index + 1; + + _.each(copy.panels, (panel, i) => { + panel.scopedVars = {}; + panel.scopedVars[variable.name] = option; + + if (index > 0) { panel.id = this.getNextPanelId(); panel.repeatIteration = this.iteration; panel.repeatPanelId = rowPanels[i].id; panel.repeat = null; copy.panels[i] = panel; - }); - } else { - _.each(rowPanels, (rowPanel, i) => { + } + }); + } else { + _.each(rowPanels, (rowPanel, i) => { + rowPanel.scopedVars = {}; + rowPanel.scopedVars[variable.name] = option; + + if (index > 0) { let cloneRowPanel = new PanelModel(rowPanel); cloneRowPanel.id = this.getNextPanelId(); cloneRowPanel.repeatIteration = this.iteration; @@ -367,19 +394,23 @@ export class DashboardModel { cloneRowPanel.repeat = null; cloneRowPanel.gridPos.y += rowHeight * index; this.panels.splice(insertPos+i, 0, cloneRowPanel); - }); - copy.panels = []; - copy.gridPos.y += rowHeight * index; - yPos += rowHeight; - panelsBelowIndex = insertPos+rowPanels.length; - } + } + }); + copy.panels = []; + copy.gridPos.y += rowHeight * index; + yPos += rowHeight; + panelsBelowIndex = insertPos+rowPanels.length; + } - // Update gridPos for panels below - for (let i = panelsBelowIndex; i< this.panels.length; i++) { - this.panels[i].gridPos.y += yPos; - } + // Update gridPos for panels below + for (let i = panelsBelowIndex; i< this.panels.length; i++) { + this.panels[i].gridPos.y += yPos; } } else { + copy = this.getPanelRepeatClone(panel, index, panelIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; + if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { copy.gridPos.y = yPos; yPos += copy.gridPos.h; diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index cb1da3e767a..d44c9b1d610 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -187,6 +187,23 @@ describe('given dashboard with row repeat', function() { ]); }); + it('should set scopedVars for each panel', function() { + dashboardJSON.templating.list[0].options[2].selected = true; + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + const scopedVars = _.compact(_.map(dashboard.panels, (panel) => { + if (panel.scopedVars) { + return panel.scopedVars.apps.value; + } + })); + + expect(scopedVars).toEqual([ + 'se1', 'se1', 'se1', + 'se2', 'se2', 'se2', + 'se3', 'se3', 'se3', + ]); + }); + it('should repeat only row if it is collapsed', function() { dashboardJSON.panels = [ { From c8ac6add166796f0f62075f3fdf70a97ef330a74 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 29 Nov 2017 14:40:23 +0100 Subject: [PATCH 15/96] test: speedup mysql and postgres integration tests by 10-20x Use docker tmpfs mounts for mysql and postgres data volumes --- docker/blocks/mysql_tests/docker-compose.yaml | 1 + docker/blocks/postgres_tests/docker-compose.yaml | 1 + 2 files changed, 2 insertions(+) diff --git a/docker/blocks/mysql_tests/docker-compose.yaml b/docker/blocks/mysql_tests/docker-compose.yaml index 646cc7ee369..c6c3097d463 100644 --- a/docker/blocks/mysql_tests/docker-compose.yaml +++ b/docker/blocks/mysql_tests/docker-compose.yaml @@ -7,3 +7,4 @@ MYSQL_PASSWORD: password ports: - "3306:3306" + tmpfs: /var/lib/mysql:rw diff --git a/docker/blocks/postgres_tests/docker-compose.yaml b/docker/blocks/postgres_tests/docker-compose.yaml index 3d9a82c034c..44b66e8e558 100644 --- a/docker/blocks/postgres_tests/docker-compose.yaml +++ b/docker/blocks/postgres_tests/docker-compose.yaml @@ -5,3 +5,4 @@ POSTGRES_PASSWORD: grafanatest ports: - "5432:5432" + tmpfs: /var/lib/postgresql/data:rw \ No newline at end of file From 1e10fcad83ead00b20e2c24f87277fbc49003410 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 29 Nov 2017 15:17:31 +0100 Subject: [PATCH 16/96] test: fix failing postgres test Should use case insensitive matching when searching for users --- pkg/services/sqlstore/user.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/services/sqlstore/user.go b/pkg/services/sqlstore/user.go index 3781d83dd96..4c199d306f0 100644 --- a/pkg/services/sqlstore/user.go +++ b/pkg/services/sqlstore/user.go @@ -400,7 +400,7 @@ func SearchUsers(query *m.SearchUsersQuery) error { } if query.Query != "" { - whereConditions = append(whereConditions, "(email LIKE ? OR name LIKE ? OR login like ?)") + whereConditions = append(whereConditions, "(email "+dialect.LikeStr()+" ? OR name "+dialect.LikeStr()+" ? OR login "+dialect.LikeStr()+" ?)") whereParams = append(whereParams, queryWithWildcards, queryWithWildcards, queryWithWildcards) } From 88760983ab00c903024f0d277050bdd280762abd Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 30 Nov 2017 10:41:37 +0300 Subject: [PATCH 17/96] repeat row: add more tests --- .../features/dashboard/specs/repeat.jest.ts | 83 +++++++++++++------ 1 file changed, 58 insertions(+), 25 deletions(-) diff --git a/public/app/features/dashboard/specs/repeat.jest.ts b/public/app/features/dashboard/specs/repeat.jest.ts index d44c9b1d610..e6db5725d3d 100644 --- a/public/app/features/dashboard/specs/repeat.jest.ts +++ b/public/app/features/dashboard/specs/repeat.jest.ts @@ -1,5 +1,6 @@ import _ from 'lodash'; import {DashboardModel} from '../dashboard_model'; +import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({ @@ -153,13 +154,13 @@ describe('given dashboard with row repeat', function() { beforeEach(function() { dashboardJSON = { panels: [ - {id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}}, + {id: 1, type: 'row', gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'}, {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, {id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}}, {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}}, ], - templating: { + templating: { list: [{ name: 'apps', current: { @@ -191,10 +192,12 @@ describe('given dashboard with row repeat', function() { dashboardJSON.templating.list[0].options[2].selected = true; dashboard = new DashboardModel(dashboardJSON); dashboard.processRepeats(); + + expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}}); + expect(dashboard.panels[4].scopedVars).toMatchObject({apps: {text: 'se2', value: 'se2'}}); + const scopedVars = _.compact(_.map(dashboard.panels, (panel) => { - if (panel.scopedVars) { - return panel.scopedVars.apps.value; - } + return panel.scopedVars ? panel.scopedVars.apps.value : null; })); expect(scopedVars).toEqual([ @@ -204,6 +207,11 @@ describe('given dashboard with row repeat', function() { ]); }); + it('should repeat only configured row', function() { + expect(dashboard.panels[6].id).toBe(4); + expect(dashboard.panels[7].id).toBe(5); + }); + it('should repeat only row if it is collapsed', function() { dashboardJSON.panels = [ { @@ -227,6 +235,51 @@ describe('given dashboard with row repeat', function() { expect(dashboard.panels[1].panels).toHaveLength(2); }); + it('should properly repeat multiple rows', function() { + dashboardJSON.panels = [ + {id: 1, type: 'row', gridPos: {x: 0, y: 0, h: 1 , w: 24}, repeat: 'apps'}, // repeat + {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, + {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, + {id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}}, // don't touch + {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}}, + {id: 6, type: 'row', gridPos: {x: 0, y: 4, h: 1 , w: 24}, repeat: 'hosts'}, // repeat + {id: 7, type: 'graph', gridPos: {x: 0, y: 5, h: 1 , w: 6}}, + {id: 8, type: 'graph', gridPos: {x: 6, y: 5, h: 1 , w: 6}} + ]; + dashboardJSON.templating.list.push({ + name: 'hosts', + current: { + text: 'backend01, backend02', + value: ['backend01', 'backend02'] + }, + options: [ + {text: 'backend01', value: 'backend01', selected: true}, + {text: 'backend02', value: 'backend02', selected: true}, + {text: 'backend03', value: 'backend03', selected: false} + ] + }); + dashboard = new DashboardModel(dashboardJSON); + dashboard.processRepeats(); + + const panel_types = _.map(dashboard.panels, 'type'); + expect(panel_types).toEqual([ + 'row', 'graph', 'graph', + 'row', 'graph', 'graph', + 'row', 'graph', + 'row', 'graph', 'graph', + 'row', 'graph', 'graph', + ]); + + expect(dashboard.panels[0].scopedVars['apps'].value).toBe('se1'); + expect(dashboard.panels[1].scopedVars['apps'].value).toBe('se1'); + expect(dashboard.panels[3].scopedVars['apps'].value).toBe('se2'); + expect(dashboard.panels[4].scopedVars['apps'].value).toBe('se2'); + expect(dashboard.panels[8].scopedVars['hosts'].value).toBe('backend01'); + expect(dashboard.panels[9].scopedVars['hosts'].value).toBe('backend01'); + expect(dashboard.panels[11].scopedVars['hosts'].value).toBe('backend02'); + expect(dashboard.panels[12].scopedVars['hosts'].value).toBe('backend02'); + }); + it('should assign unique ids for repeated panels', function() { dashboardJSON.panels = [ { @@ -252,26 +305,6 @@ describe('given dashboard with row repeat', function() { })); expect(panel_ids.length).toEqual(_.uniq(panel_ids).length); }); - - // it('should set scopedVars on panels', function() { - // expect(dashboard.panels[1].scopedVars).toMatchObject({apps: {text: 'se1', value: 'se1'}}) - // }); - - // it('should repeat row and panels below two times', function() { - // expect(dashboard.panels).toMatchObject([ - // // first (original row) - // {id: 1, type: 'row', repeat: 'apps', gridPos: {x: 0, y: 0, h: 1 , w: 24}}, - // {id: 2, type: 'graph', gridPos: {x: 0, y: 1, h: 1 , w: 6}}, - // {id: 3, type: 'graph', gridPos: {x: 6, y: 1, h: 1 , w: 6}}, - // // repeated row - // {id: 1, type: 'row', repeatPanelId: 1, gridPos: {x: 0, y: 0, h: 1 , w: 24}}, - // {id: 2, type: 'graph', repeatPanelId: 1, gridPos: {x: 0, y: 1, h: 1 , w: 6}}, - // {id: 3, type: 'graph', repeatPanelId: 1, gridPos: {x: 6, y: 1, h: 1 , w: 6}}, - // // row below dont touch - // {id: 4, type: 'row', gridPos: {x: 0, y: 2, h: 1 , w: 24}}, - // {id: 5, type: 'graph', gridPos: {x: 0, y: 3, h: 1 , w: 12}}, - // ]); - // }); }); From af5ced0e18f4f9948d68742725c821adf02b4b58 Mon Sep 17 00:00:00 2001 From: Carl Bergquist Date: Thu, 30 Nov 2017 15:06:02 +0100 Subject: [PATCH 18/96] Revert "prometheus nested query support" --- .../plugins/datasource/prometheus/datasource.ts | 8 -------- .../prometheus/specs/datasource_specs.ts | 15 --------------- 2 files changed, 23 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/datasource.ts b/public/app/plugins/datasource/prometheus/datasource.ts index 6f41833e22a..ec295760d49 100644 --- a/public/app/plugins/datasource/prometheus/datasource.ts +++ b/public/app/plugins/datasource/prometheus/datasource.ts @@ -157,7 +157,6 @@ export class PrometheusDatasource { // Only replace vars in expression after having (possibly) updated interval vars query.expr = this.templateSrv.replace(target.expr, scopedVars, this.interpolateQueryExpr); - query.expr = this.replaceNestedQuery(query.expr, options); query.requestId = options.panelId + target.refId; return query; } @@ -272,13 +271,6 @@ export class PrometheusDatasource { }); } - replaceNestedQuery(query, options) { - return query.replace(/\#([A-Z])/g, (match, g1) => { - let replaceTarget = options.targets.find((t) => { return t.refId === g1; }); - return replaceTarget ? replaceTarget.expr : match; - }); - } - transformMetricData(md, options, start, end, step) { var dps = [], metricLabel = null; diff --git a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts index a507aa41545..a7d18f3f158 100644 --- a/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/datasource_specs.ts @@ -590,19 +590,4 @@ describe('PrometheusDatasource', function() { expect(query.scopedVars.__interval_ms.value).to.be(5 * 1000); }); }); - describe('The nested query', function() { - it('should generate correct query', function() { - let query = 'sum(rate(#A[1m]))'; - let options = { - targets: [ - { - refId: 'A', - expr: 'http_requests_total' - } - ] - }; - let result = ctx.ds.replaceNestedQuery(query, options); - expect(result).to.be('sum(rate(http_requests_total[1m]))'); - }); - }); }); From 68d4211c50e76686d6a63e92fc647f4d320fec34 Mon Sep 17 00:00:00 2001 From: Andrei Kalasok Date: Thu, 30 Nov 2017 15:10:44 +0100 Subject: [PATCH 19/96] grafana-10039: fix query time range ends in the past --- pkg/tsdb/graphite/graphite.go | 4 ++-- pkg/tsdb/graphite/graphite_test.go | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/tsdb/graphite/graphite.go b/pkg/tsdb/graphite/graphite.go index 7cadf055ff6..73b173813af 100644 --- a/pkg/tsdb/graphite/graphite.go +++ b/pkg/tsdb/graphite/graphite.go @@ -17,7 +17,7 @@ import ( "github.com/grafana/grafana/pkg/models" "github.com/grafana/grafana/pkg/setting" "github.com/grafana/grafana/pkg/tsdb" - opentracing "github.com/opentracing/opentracing-go" + "github.com/opentracing/opentracing-go" ) type GraphiteExecutor struct { @@ -158,7 +158,7 @@ func formatTimeRange(input string) string { if input == "now" { return input } - return strings.Replace(strings.Replace(input, "m", "min", -1), "M", "mon", -1) + return strings.Replace(strings.Replace(strings.Replace(input, "now", "", -1), "m", "min", -1), "M", "mon", -1) } func fixIntervalFormat(target string) string { diff --git a/pkg/tsdb/graphite/graphite_test.go b/pkg/tsdb/graphite/graphite_test.go index c1a2736293b..1704a9b5f55 100644 --- a/pkg/tsdb/graphite/graphite_test.go +++ b/pkg/tsdb/graphite/graphite_test.go @@ -18,14 +18,14 @@ func TestGraphiteFunctions(t *testing.T) { Convey("formatting time range for now-1m", func() { timeRange := formatTimeRange("now-1m") - So(timeRange, ShouldEqual, "now-1min") + So(timeRange, ShouldEqual, "-1min") }) Convey("formatting time range for now-1M", func() { timeRange := formatTimeRange("now-1M") - So(timeRange, ShouldEqual, "now-1mon") + So(timeRange, ShouldEqual, "-1mon") }) From d28ca541292c5fa9230c153999ec729eac98a611 Mon Sep 17 00:00:00 2001 From: Johannes Grassler Date: Tue, 28 Nov 2017 18:16:38 +0100 Subject: [PATCH 20/96] Use systemd notification where applicable With this change in place, the grafana service will signal readiness to serve by writing "READY=1" to the path specified through the NOTIFY_SOCKET environment variable. If this environment variable is not present or empty, no notification will happen. This notification is the standard systemd mechanism for indicating a service is ready to serve. For Grafana this may be a couple of seconds from startup due to database migrations. This change also adjusts the Grafana systemd service definition to make use of this feature. --- packaging/rpm/systemd/grafana-server.service | 2 +- pkg/cmd/grafana-server/server.go | 2 ++ pkg/util/sdnotify.go | 34 ++++++++++++++++++++ 3 files changed, 37 insertions(+), 1 deletion(-) create mode 100644 pkg/util/sdnotify.go diff --git a/packaging/rpm/systemd/grafana-server.service b/packaging/rpm/systemd/grafana-server.service index 3e018e8b176..b23e5196e17 100644 --- a/packaging/rpm/systemd/grafana-server.service +++ b/packaging/rpm/systemd/grafana-server.service @@ -9,7 +9,7 @@ After=postgresql.service mariadb.service mysql.service EnvironmentFile=/etc/sysconfig/grafana-server User=grafana Group=grafana -Type=simple +Type=notify Restart=on-failure WorkingDirectory=/usr/share/grafana RuntimeDirectory=grafana diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 1d3ac092734..476eb2f433f 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -29,6 +29,7 @@ import ( "github.com/grafana/grafana/pkg/social" "github.com/grafana/grafana/pkg/tracing" + "github.com/grafana/grafana/pkg/util" ) func NewGrafanaServer() models.GrafanaServer { @@ -96,6 +97,7 @@ func (g *GrafanaServerImpl) Start() { return } + util.SdNotify("READY=1") g.startHttpServer() } diff --git a/pkg/util/sdnotify.go b/pkg/util/sdnotify.go new file mode 100644 index 00000000000..b5cd4a4a45d --- /dev/null +++ b/pkg/util/sdnotify.go @@ -0,0 +1,34 @@ +package util + +import ( + "errors" + "net" + "os" +) + +var NoNotifySocket = errors.New("NOTIFY_SOCKET environment variable empty or unset.") + +func SdNotify(state string) error { + notifySocket := os.Getenv("NOTIFY_SOCKET") + + if notifySocket == "" { + return NoNotifySocket + } + + socketAddr := &net.UnixAddr{ + Name: notifySocket, + Net: "unixgram", + } + + conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) + + if err != nil { + return err + } + + _, err = conn.Write([]byte(state)) + + conn.Close() + + return err +} From 94446fb85c2c69860c792bc0657600b7df082824 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Dec 2017 13:54:31 +0100 Subject: [PATCH 21/96] changelog: adds note about closing #10024 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7a75ad758c8..1b4adfcdda2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,7 +24,7 @@ * **Cloudwatch**: Fixes broken query inspector for cloudwatch [#9661](https://github.com/grafana/grafana/issues/9661), thx [@mtanda](https://github.com/mtanda) * **Dashboard**: Make it possible to start dashboards from search and dashboard list panel [#1871](https://github.com/grafana/grafana/issues/1871) * **Annotations**: Posting annotations now return the id of the annotation [#9798](https://github.com/grafana/grafana/issues/9798) - +* **Systemd**: Use systemd notification ready flag [#10024](https://github.com/grafana/grafana/issues/10024), thx [@jgrassler](https://github.com/jgrassler) ## Tech * **RabbitMq**: Remove support for publishing events to RabbitMQ [#9645](https://github.com/grafana/grafana/issues/9645) From 7a497fd617b3648bc0ca5b4cd779e93c0d24a02a Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Dec 2017 14:02:05 +0100 Subject: [PATCH 22/96] move systemd ready notification to server.go --- pkg/cmd/grafana-server/server.go | 30 ++++++++++++++++++++++++++-- pkg/util/sdnotify.go | 34 -------------------------------- 2 files changed, 28 insertions(+), 36 deletions(-) delete mode 100644 pkg/util/sdnotify.go diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 476eb2f433f..4d95e77ad29 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -3,7 +3,9 @@ package main import ( "context" "flag" + "fmt" "io/ioutil" + "net" "os" "path/filepath" "strconv" @@ -29,7 +31,6 @@ import ( "github.com/grafana/grafana/pkg/social" "github.com/grafana/grafana/pkg/tracing" - "github.com/grafana/grafana/pkg/util" ) func NewGrafanaServer() models.GrafanaServer { @@ -97,7 +98,7 @@ func (g *GrafanaServerImpl) Start() { return } - util.SdNotify("READY=1") + SendSystemdReady("READY=1") g.startHttpServer() } @@ -171,3 +172,28 @@ func (g *GrafanaServerImpl) writePIDFile() { g.log.Info("Writing PID file", "path", *pidFile, "pid", pid) } + +func SendSystemdReady(state string) error { + notifySocket := os.Getenv("NOTIFY_SOCKET") + + if notifySocket == "" { + return fmt.Errorf("NOTIFY_SOCKET environment variable empty or unset.") + } + + socketAddr := &net.UnixAddr{ + Name: notifySocket, + Net: "unixgram", + } + + conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) + + if err != nil { + return err + } + + _, err = conn.Write([]byte(state)) + + conn.Close() + + return err +} diff --git a/pkg/util/sdnotify.go b/pkg/util/sdnotify.go deleted file mode 100644 index b5cd4a4a45d..00000000000 --- a/pkg/util/sdnotify.go +++ /dev/null @@ -1,34 +0,0 @@ -package util - -import ( - "errors" - "net" - "os" -) - -var NoNotifySocket = errors.New("NOTIFY_SOCKET environment variable empty or unset.") - -func SdNotify(state string) error { - notifySocket := os.Getenv("NOTIFY_SOCKET") - - if notifySocket == "" { - return NoNotifySocket - } - - socketAddr := &net.UnixAddr{ - Name: notifySocket, - Net: "unixgram", - } - - conn, err := net.DialUnix(socketAddr.Net, nil, socketAddr) - - if err != nil { - return err - } - - _, err = conn.Write([]byte(state)) - - conn.Close() - - return err -} From 17bf87fb626ecb7b8a338e486403861e43deae17 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Dec 2017 14:05:47 +0100 Subject: [PATCH 23/96] typo :boom: --- pkg/cmd/grafana-server/server.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/pkg/cmd/grafana-server/server.go b/pkg/cmd/grafana-server/server.go index 4d95e77ad29..f5c6b0d1cee 100644 --- a/pkg/cmd/grafana-server/server.go +++ b/pkg/cmd/grafana-server/server.go @@ -98,7 +98,7 @@ func (g *GrafanaServerImpl) Start() { return } - SendSystemdReady("READY=1") + SendSystemdNotification("READY=1") g.startHttpServer() } @@ -173,7 +173,7 @@ func (g *GrafanaServerImpl) writePIDFile() { g.log.Info("Writing PID file", "path", *pidFile, "pid", pid) } -func SendSystemdReady(state string) error { +func SendSystemdNotification(state string) error { notifySocket := os.Getenv("NOTIFY_SOCKET") if notifySocket == "" { From 8593ca001d90c423afc63febe8569f4577fa810d Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 1 Dec 2017 16:34:49 +0300 Subject: [PATCH 24/96] repeat row: refactor --- .../app/features/dashboard/dashboard_model.ts | 182 ++++++++++-------- 1 file changed, 97 insertions(+), 85 deletions(-) diff --git a/public/app/features/dashboard/dashboard_model.ts b/public/app/features/dashboard/dashboard_model.ts index c54a9460c08..f4cdc26d438 100644 --- a/public/app/features/dashboard/dashboard_model.ts +++ b/public/app/features/dashboard/dashboard_model.ts @@ -305,8 +305,6 @@ export class DashboardModel { } let clone = new PanelModel(sourcePanel.getSaveModel()); - clone.id = this.getNextPanelId(); - // for row clones we need to figure out panels under row to clone and where to insert clone let rowPanels, insertPos; if (sourcePanel.collapsed) { @@ -322,9 +320,7 @@ export class DashboardModel { } this.panels.splice(insertPos, 0, clone); - clone.repeatIteration = this.iteration; - clone.repeatPanelId = sourcePanel.id; - clone.repeat = null; + this.updateRepeatedPanelIds(clone); return clone; } @@ -337,103 +333,119 @@ export class DashboardModel { return; } - let selected; - if (variable.current.text === 'All') { - selected = variable.options.slice(1, variable.options.length); - } else { - selected = _.filter(variable.options, {selected: true}); + if (panel.type === 'row') { + this.repeatRow(panel, panelIndex, variable); + return; } + let selectedOptions = this.getSelectedVariableOptions(variable); let minWidth = panel.minSpan || 6; let xPos = 0; let yPos = panel.gridPos.y; - for (let index = 0; index < selected.length; index++) { - let option = selected[index]; + for (let index = 0; index < selectedOptions.length; index++) { + let option = selectedOptions[index]; let copy; - if (panel.type === 'row') { - copy = this.getRowRepeatClone(panel, index, panelIndex); - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; + copy = this.getPanelRepeatClone(panel, index, panelIndex); + copy.scopedVars = {}; + copy.scopedVars[variable.name] = option; - let rowHeight = this.getRowHeight(copy); - // if (rowHeight) { - let panelsBelowIndex; - let rowPanels = copy.panels || []; - // insert after 'row' panel - let insertPos = panelIndex + ((rowPanels.length + 1) * index) + 1; - - if (copy.collapsed) { - copy.gridPos.y += index; - yPos += index; - panelsBelowIndex = panelIndex + index + 1; - - _.each(copy.panels, (panel, i) => { - panel.scopedVars = {}; - panel.scopedVars[variable.name] = option; - - if (index > 0) { - panel.id = this.getNextPanelId(); - panel.repeatIteration = this.iteration; - panel.repeatPanelId = rowPanels[i].id; - panel.repeat = null; - copy.panels[i] = panel; - } - }); - } else { - _.each(rowPanels, (rowPanel, i) => { - rowPanel.scopedVars = {}; - rowPanel.scopedVars[variable.name] = option; - - if (index > 0) { - let cloneRowPanel = new PanelModel(rowPanel); - cloneRowPanel.id = this.getNextPanelId(); - cloneRowPanel.repeatIteration = this.iteration; - cloneRowPanel.repeatPanelId = rowPanel.id; - cloneRowPanel.repeat = null; - cloneRowPanel.gridPos.y += rowHeight * index; - this.panels.splice(insertPos+i, 0, cloneRowPanel); - } - }); - copy.panels = []; - copy.gridPos.y += rowHeight * index; - yPos += rowHeight; - panelsBelowIndex = insertPos+rowPanels.length; - } - - // Update gridPos for panels below - for (let i = panelsBelowIndex; i< this.panels.length; i++) { - this.panels[i].gridPos.y += yPos; - } + if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { + copy.gridPos.y = yPos; + yPos += copy.gridPos.h; } else { - copy = this.getPanelRepeatClone(panel, index, panelIndex); - copy.scopedVars = {}; - copy.scopedVars[variable.name] = option; + // set width based on how many are selected + // assumed the repeated panels should take up full row width - if (panel.repeatDirection === REPEAT_DIR_VERTICAL) { - copy.gridPos.y = yPos; + copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selectedOptions.length, minWidth); + copy.gridPos.x = xPos; + copy.gridPos.y = yPos; + + xPos += copy.gridPos.w; + + // handle overflow by pushing down one row + if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { + xPos = 0; yPos += copy.gridPos.h; - } else { - // set width based on how many are selected - // assumed the repeated panels should take up full row width - - copy.gridPos.w = Math.max(GRID_COLUMN_COUNT / selected.length, minWidth); - copy.gridPos.x = xPos; - copy.gridPos.y = yPos; - - xPos += copy.gridPos.w; - - // handle overflow by pushing down one row - if (xPos + copy.gridPos.w > GRID_COLUMN_COUNT) { - xPos = 0; - yPos += copy.gridPos.h; - } } } } } + repeatRow(panel: PanelModel, panelIndex: number, variable) { + let selectedOptions = this.getSelectedVariableOptions(variable); + let yPos = panel.gridPos.y; + + function setScopedVars(panel, variableOption) { + panel.scopedVars = {}; + panel.scopedVars[variable.name] = variableOption; + } + + for (let optionIndex = 0; optionIndex < selectedOptions.length; optionIndex++) { + let option = selectedOptions[optionIndex]; + let rowCopy = this.getRowRepeatClone(panel, optionIndex, panelIndex); + setScopedVars(rowCopy, option); + + let rowHeight = this.getRowHeight(rowCopy); + let rowPanels = rowCopy.panels || []; + let panelBelowIndex; + + if (panel.collapsed) { + // For collapsed row just copy its panels and set scoped vars and proper IDs + _.each(rowPanels, (rowPanel, i) => { + setScopedVars(rowPanel, option); + if (optionIndex > 0) { + this.updateRepeatedPanelIds(rowPanel); + } + }); + rowCopy.gridPos.y += optionIndex; + yPos += optionIndex; + panelBelowIndex = panelIndex + optionIndex + 1; + } else { + // insert after 'row' panel + let insertPos = panelIndex + ((rowPanels.length + 1) * optionIndex) + 1; + _.each(rowPanels, (rowPanel, i) => { + setScopedVars(rowPanel, option); + if (optionIndex > 0) { + let cloneRowPanel = new PanelModel(rowPanel); + this.updateRepeatedPanelIds(cloneRowPanel); + // For exposed row additionally set proper Y grid position and add it to dashboard panels + cloneRowPanel.gridPos.y += rowHeight * optionIndex; + this.panels.splice(insertPos+i, 0, cloneRowPanel); + } + }); + rowCopy.panels = []; + rowCopy.gridPos.y += rowHeight * optionIndex; + yPos += rowHeight; + panelBelowIndex = insertPos+rowPanels.length; + } + + // Update gridPos for panels below + for (let i = panelBelowIndex; i< this.panels.length; i++) { + this.panels[i].gridPos.y += yPos; + } + } + } + + updateRepeatedPanelIds(panel: PanelModel) { + panel.repeatPanelId = panel.id; + panel.id = this.getNextPanelId(); + panel.repeatIteration = this.iteration; + panel.repeat = null; + return panel; + } + + getSelectedVariableOptions(variable) { + let selectedOptions; + if (variable.current.text === 'All') { + selectedOptions = variable.options.slice(1, variable.options.length); + } else { + selectedOptions = _.filter(variable.options, {selected: true}); + } + return selectedOptions; + } + getRowHeight(rowPanel: PanelModel): number { if (!rowPanel.panels || rowPanel.panels.length === 0) { return 0; From 5e9f0771c599ea1005f2d9faf7edc0eccf1a67a1 Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Dec 2017 15:49:54 +0100 Subject: [PATCH 25/96] ignore /conf/**/custom.yaml files --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 719f4347779..da222a55b5b 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,8 @@ conf/custom.ini fig.yml docker-compose.yml docker-compose.yaml +/conf/dashboards/custom.yaml +/conf/datasources/custom.yaml profile.cov /grafana .notouch From 92821828055820bcbb6fdcaeba4a2b58eceab85f Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Dec 2017 16:16:49 +0100 Subject: [PATCH 26/96] influxdb: pass tags to alerting from influxdb client closes #10046 --- pkg/tsdb/influxdb/response_parser.go | 1 + 1 file changed, 1 insertion(+) diff --git a/pkg/tsdb/influxdb/response_parser.go b/pkg/tsdb/influxdb/response_parser.go index b7db6182241..8de8dcbb464 100644 --- a/pkg/tsdb/influxdb/response_parser.go +++ b/pkg/tsdb/influxdb/response_parser.go @@ -50,6 +50,7 @@ func (rp *ResponseParser) transformRows(rows []Row, queryResult *tsdb.QueryResul result = append(result, &tsdb.TimeSeries{ Name: rp.formatSerieName(row, column, query), Points: points, + Tags: row.Tags, }) } } From d6d64c53cd0e3bf1fdea4f98b4c2ce7fb8533ccf Mon Sep 17 00:00:00 2001 From: bergquist Date: Fri, 1 Dec 2017 16:23:55 +0100 Subject: [PATCH 27/96] typo :boom: --- pkg/api/render.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/api/render.go b/pkg/api/render.go index fdf53e6730d..cab9c81505d 100644 --- a/pkg/api/render.go +++ b/pkg/api/render.go @@ -12,7 +12,7 @@ import ( func RenderToPng(c *middleware.Context) { queryReader, err := util.NewUrlQueryReader(c.Req.URL) if err != nil { - c.Handle(400, "Rander parameters error", err) + c.Handle(400, "Render parameters error", err) return } queryParams := fmt.Sprintf("?%s", c.Req.URL.RawQuery) From 3e94d804303a3f8dfb750372fcde4edc9f828948 Mon Sep 17 00:00:00 2001 From: paulfantom Date: Fri, 1 Dec 2017 21:26:14 +0100 Subject: [PATCH 28/96] add Cloud Alchemy Ansible role --- docs/sources/administration/provisioning.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/sources/administration/provisioning.md b/docs/sources/administration/provisioning.md index 70d9d7a81f3..119044da6cf 100644 --- a/docs/sources/administration/provisioning.md +++ b/docs/sources/administration/provisioning.md @@ -65,6 +65,7 @@ Currently we do not provide any scripts/manifests for configuring Grafana. Rathe Tool | Project -----|------------ Puppet | [https://forge.puppet.com/puppet/grafana](https://forge.puppet.com/puppet/grafana) +Ansible | [https://github.com/cloudalchemy/ansible-grafana](https://github.com/cloudalchemy/ansible-grafana) Ansible | [https://github.com/picotrading/ansible-grafana](https://github.com/picotrading/ansible-grafana) Chef | [https://github.com/JonathanTron/chef-grafana](https://github.com/JonathanTron/chef-grafana) Saltstack | [https://github.com/salt-formulas/salt-formula-grafana](https://github.com/salt-formulas/salt-formula-grafana) From e8a6af7b22ae7ade575f40b6b48aae9916f26370 Mon Sep 17 00:00:00 2001 From: Mitsuhiro Tanda Date: Sun, 3 Dec 2017 01:21:21 +0900 Subject: [PATCH 29/96] fix templating undefined error (#10004) --- .../datasource/prometheus/metric_find_query.ts | 4 ++-- .../specs/metric_find_query_specs.ts | 18 ++++++++++++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/public/app/plugins/datasource/prometheus/metric_find_query.ts b/public/app/plugins/datasource/prometheus/metric_find_query.ts index 7a82a428939..46d9bf9fccd 100644 --- a/public/app/plugins/datasource/prometheus/metric_find_query.ts +++ b/public/app/plugins/datasource/prometheus/metric_find_query.ts @@ -67,8 +67,8 @@ export default class PrometheusMetricFindQuery { return this.datasource._request("GET", url).then(function(result) { var _labels = _.map(result.data.data, function(metric) { - return metric[label]; - }); + return metric[label] || ''; + }).filter(function(label) { return label !== ''; }); return _.uniq(_labels).map(function(metric) { return { diff --git a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts index bb051d7328d..adc1cc248f8 100644 --- a/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts +++ b/public/app/plugins/datasource/prometheus/specs/metric_find_query_specs.ts @@ -76,6 +76,24 @@ describe('PrometheusMetricFindQuery', function() { ctx.$rootScope.$apply(); expect(results.length).to.be(3); }); + it('label_values(metric, resource) result should not contain empty string', function() { + response = { + status: "success", + data: [ + {__name__: "metric", resource: "value1"}, + {__name__: "metric", resource: "value2"}, + {__name__: "metric", resource: ""} + ] + }; + ctx.$httpBackend.expect('GET', /proxied\/api\/v1\/series\?match\[\]=metric&start=.*&end=.*/).respond(response); + var pm = new PrometheusMetricFindQuery(ctx.ds, 'label_values(metric, resource)', ctx.timeSrv); + pm.process().then(function(data) { results = data; }); + ctx.$httpBackend.flush(); + ctx.$rootScope.$apply(); + expect(results.length).to.be(2); + expect(results[0].text).to.be("value1"); + expect(results[1].text).to.be("value2"); + }); it('metrics(metric.*) should generate metric name query', function() { response = { status: "success", From a47bf7dfd94b62aebe9651d52d94f456546aa37e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Dec 2017 08:11:07 +0100 Subject: [PATCH 30/96] ux: updated modal header design --- public/sass/_variables.dark.scss | 7 ++++++- public/sass/_variables.light.scss | 8 +++++++- public/sass/components/_modals.scss | 13 +++++-------- public/sass/components/_page_header.scss | 6 +++--- public/sass/mixins/_drop_element.scss | 1 - 5 files changed, 21 insertions(+), 14 deletions(-) diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 70493a4363d..1567d8c5d61 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -98,6 +98,11 @@ $panel-drop-zone-bg: repeating-linear-gradient(-128deg, #111, #111 10px, #191 $panel-header-hover-bg: $dark-4; $panel-header-menu-hover-bg: $dark-5; +// page header +$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; // Graphite Target Editor @@ -276,7 +281,7 @@ $alert-warning-bg: linear-gradient(90deg, #d44939, #e0603d); $alert-info-bg: linear-gradient(100deg, #1a4552, #00374a); // popover -$popover-bg: $panel-bg; +$popover-bg: $page-bg; $popover-color: $text-color; $popover-border-color: $dark-4; $popover-shadow: 0 0 20px black; diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index f33ef4debed..78c0ba3bf61 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -70,6 +70,7 @@ $text-shadow-faint: none; // gradients $brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 0%, rgba(255,68,0,1.0) 99%, rgba(255,68,0,1.0) 100%); $page-gradient: linear-gradient(-60deg, transparent 70%, darken($page-bg, 4%) 98%); +$page-header-bg: linear-gradient(90deg, #292a2d, black); // Links // ------------------------- @@ -103,6 +104,11 @@ $panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $panel-header-hover-bg: $gray-6; $panel-header-menu-hover-bg: $gray-4; +// Page header +$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: $gray-2; // Graphite Target Editor @@ -290,7 +296,7 @@ $alert-warning-bg: linear-gradient(90deg, #d44939, #e04d3d); $alert-info-bg: $blue-dark; // popover -$popover-bg: $panel-bg; +$popover-bg: $page-bg; $popover-color: $text-color; $popover-border-color: $gray-5; $popover-shadow: 0 0 20px $white; diff --git a/public/sass/components/_modals.scss b/public/sass/components/_modals.scss index 137afc11bd7..81009d20f3c 100644 --- a/public/sass/components/_modals.scss +++ b/public/sass/components/_modals.scss @@ -23,7 +23,7 @@ position: fixed; z-index: $zindex-modal; width: 100%; - background-color: $panel-bg; + background: $page-bg; @include box-shadow(0 3px 7px rgba(0,0,0,0.3)); @include background-clip(padding-box); outline: none; @@ -37,13 +37,10 @@ } .modal-header { - background-color: $body-bg; - @include brand-bottom-border(); - @include clearfix(); - - .gf-tabs-link.active { - background-color: $panel-bg; - } + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; + @include clearfix(); } .modal-header-title { diff --git a/public/sass/components/_page_header.scss b/public/sass/components/_page_header.scss index 1d5268472bf..b6f44be1c18 100644 --- a/public/sass/components/_page_header.scss +++ b/public/sass/components/_page_header.scss @@ -1,8 +1,8 @@ .page-header-canvas { - background: linear-gradient(90deg, #292a2d, black); - box-shadow: inset 0px -4px 14px $dark-2; - border-bottom: 1px solid $dark-4; + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; } .page-header { diff --git a/public/sass/mixins/_drop_element.scss b/public/sass/mixins/_drop_element.scss index f1bb69efd98..a174cc8f805 100644 --- a/public/sass/mixins/_drop_element.scss +++ b/public/sass/mixins/_drop_element.scss @@ -11,7 +11,6 @@ background: $theme-bg; color: $theme-color; padding: 0.65rem; - font-size: $font-size-sm; word-wrap: break-word; max-width: 20rem; border: 1px solid $border-color; From 15f46b8534197f91f69da8a5c9e722b6ec8d4fa2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Sun, 3 Dec 2017 16:22:59 +0100 Subject: [PATCH 31/96] ux: tabs update --- public/sass/_variables.scss | 2 +- public/sass/components/_tabbed_view.scss | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index 49c5addc72d..ec1460ac584 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -232,5 +232,5 @@ $dashboard-padding: $panel-margin * 2; $panel-padding: 0px 10px 5px 10px; // tabs -$tabs-padding: 10px 15px 10px; +$tabs-padding: 9px 15px 9px; diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index 9eeadf3d61d..dac7408d70f 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -12,8 +12,10 @@ } .tabbed-view-header { + background: $page-header-bg; + box-shadow: $page-header-shadow; + border-bottom: 1px solid $page-header-border-color; @include clearfix(); - @include brand-bottom-border(); } .tabbed-view-title { From 66657d24b8c7763ce8911f58d5326286aa50bf7e Mon Sep 17 00:00:00 2001 From: Daniel Lee Date: Mon, 4 Dec 2017 12:37:00 +0100 Subject: [PATCH 32/96] sidemenu: responsive sidemenu view for smallest breakpoint For the smallest breakpoint, expands the sidemenu to be width 100% and to be toggled on or off rather than visible all the time. --- public/app/core/components/grafana_app.ts | 10 ++ .../core/components/sidemenu/sidemenu.html | 6 + .../app/core/components/sidemenu/sidemenu.ts | 10 ++ public/app/core/services/context_srv.ts | 9 +- public/sass/components/_sidemenu.scss | 148 ++++++++++++++---- 5 files changed, 153 insertions(+), 30 deletions(-) diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index d13c6a6a00e..c60c8a20a4c 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -85,6 +85,16 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } }); + let sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; + body.toggleClass('sidemenu-open--xs', sidemenuOpenSmallBreakpoint); + + scope.$watch('contextSrv.sidemenuSmallBreakpoint', newVal => { + if (sidemenuOpenSmallBreakpoint !== scope.contextSrv.sidemenuSmallBreakpoint) { + sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; + body.toggleClass('sidemenu-open--xs', scope.contextSrv.sidemenuSmallBreakpoint); + } + }); + // tooltip removal fix // manage page classes var pageClass; diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 2869562c5b1..b47b7fc8265 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -2,6 +2,12 @@ + + +

 Close

+
+ +
- diff --git a/public/app/features/dashboard/import/dash_import.ts b/public/app/features/dashboard/import/dash_import.ts deleted file mode 100644 index 8f3de6adc60..00000000000 --- a/public/app/features/dashboard/import/dash_import.ts +++ /dev/null @@ -1,176 +0,0 @@ -/// - -import coreModule from 'app/core/core_module'; -import config from 'app/core/config'; -import _ from 'lodash'; - -export class DashImportCtrl { - step: number; - jsonText: string; - parseError: string; - nameExists: boolean; - dash: any; - inputs: any[]; - inputsValid: boolean; - gnetUrl: string; - gnetError: string; - gnetInfo: any; - - /** @ngInject */ - constructor(private backendSrv, private $location, private $scope, $routeParams) { - this.step = 1; - this.nameExists = false; - - // check gnetId in url - if ($routeParams.gnetId) { - this.gnetUrl = $routeParams.gnetId ; - this.checkGnetDashboard(); - } - } - - onUpload(dash) { - this.dash = dash; - this.dash.id = null; - this.step = 2; - this.inputs = []; - - if (this.dash.__inputs) { - for (let input of this.dash.__inputs) { - var inputModel = { - name: input.name, - label: input.label, - info: input.description, - value: input.value, - type: input.type, - pluginId: input.pluginId, - options: [] - }; - - if (input.type === 'datasource') { - this.setDatasourceOptions(input, inputModel); - } else if (!inputModel.info) { - inputModel.info = 'Specify a string constant'; - } - - this.inputs.push(inputModel); - } - } - - this.inputsValid = this.inputs.length === 0; - this.titleChanged(); - } - - setDatasourceOptions(input, inputModel) { - var sources = _.filter(config.datasources, val => { - return val.type === input.pluginId; - }); - - if (sources.length === 0) { - inputModel.info = "No data sources of type " + input.pluginName + " found"; - } else if (!inputModel.info) { - inputModel.info = "Select a " + input.pluginName + " data source"; - } - - inputModel.options = sources.map(val => { - return {text: val.name, value: val.name}; - }); - } - - inputValueChanged() { - this.inputsValid = true; - for (let input of this.inputs) { - if (!input.value) { - this.inputsValid = false; - } - } - } - - titleChanged() { - this.backendSrv.search({query: this.dash.title}).then(res => { - this.nameExists = false; - for (let hit of res) { - if (this.dash.title === hit.title) { - this.nameExists = true; - break; - } - } - }); - } - - saveDashboard() { - var inputs = this.inputs.map(input => { - return { - name: input.name, - type: input.type, - pluginId: input.pluginId, - value: input.value - }; - }); - - return this.backendSrv.post('api/dashboards/import', { - dashboard: this.dash, - overwrite: true, - inputs: inputs - }).then(res => { - this.$location.url('dashboard/' + res.importedUri); - this.$scope.dismiss(); - }); - } - - loadJsonText() { - try { - this.parseError = ''; - var dash = JSON.parse(this.jsonText); - this.onUpload(dash); - } catch (err) { - console.log(err); - this.parseError = err.message; - return; - } - } - - checkGnetDashboard() { - this.gnetError = ''; - - var match = /(^\d+$)|dashboards\/(\d+)/.exec(this.gnetUrl); - var dashboardId; - - if (match && match[1]) { - dashboardId = match[1]; - } else if (match && match[2]) { - dashboardId = match[2]; - } else { - this.gnetError = 'Could not find dashboard'; - } - - return this.backendSrv.get('api/gnet/dashboards/' + dashboardId).then(res => { - this.gnetInfo = res; - // store reference to grafana.com - res.json.gnetId = res.id; - this.onUpload(res.json); - }).catch(err => { - err.isHandled = true; - this.gnetError = err.data.message || err; - }); - } - - back() { - this.gnetUrl = ''; - this.step = 1; - this.gnetError = ''; - this.gnetInfo = ''; - } - -} - -export function dashImportDirective() { - return { - restrict: 'E', - templateUrl: 'public/app/features/dashboard/import/dash_import.html', - controller: DashImportCtrl, - bindToController: true, - controllerAs: 'ctrl', - }; -} - -coreModule.directive('dashImport', dashImportDirective); diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html index be43078d32a..b740b8f38bc 100644 --- a/public/app/features/dashboard/partials/dashboardImport.html +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -3,7 +3,8 @@
-
+ +
@@ -112,10 +113,10 @@
- - Cancel diff --git a/public/app/features/dashboard/upload.ts b/public/app/features/dashboard/upload.ts index 45a1f1b4a0c..61cca0d4216 100644 --- a/public/app/features/dashboard/upload.ts +++ b/public/app/features/dashboard/upload.ts @@ -4,7 +4,7 @@ import coreModule from 'app/core/core_module'; var template = ` -
{{::section.title}} +
+   +
+
diff --git a/public/app/core/components/search/search_results.ts b/public/app/core/components/search/search_results.ts index de34d5bd5d0..0757a53a517 100644 --- a/public/app/core/components/search/search_results.ts +++ b/public/app/core/components/search/search_results.ts @@ -6,12 +6,26 @@ export class SearchResultsCtrl { onSelectionChanged: any; onTagSelected: any; + /** @ngInject */ + constructor(private $location) { + + } + toggleFolderExpand(section) { if (section.toggle) { section.toggle(section); } } + navigateToFolder(section, evt) { + this.$location.path('/dashboards/folder/' + section.id + '/' + section.uri); + + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + toggleSelection(item, evt) { item.checked = !item.checked; diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index d3ef3d51477..f4132400c7d 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -73,6 +73,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/folder/:folderId/:type/:slug', { + templateUrl: 'public/app/features/dashboard/partials/dashboardList.html', + controller : 'DashboardListCtrl', + controllerAs: 'ctrl', + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index fdd9b8ca77f..c9e6e60e3e3 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -154,12 +154,12 @@ export class SearchSrv { } search(options) { - if (!options.query && (!options.tag || options.tag.length === 0) && !options.starred) { + if (!options.folderIds && !options.query && (!options.tag || options.tag.length === 0) && !options.starred) { return this.browse(options); } let query = _.clone(options); - query.folderIds = []; + query.folderIds = options.folderIds || []; query.type = 'dash-db'; return this.backendSrv.search(query).then(results => { diff --git a/public/app/features/dashboard/dashboard_list_ctrl.ts b/public/app/features/dashboard/dashboard_list_ctrl.ts index ebb7d426089..72861377f71 100644 --- a/public/app/features/dashboard/dashboard_list_ctrl.ts +++ b/public/app/features/dashboard/dashboard_list_ctrl.ts @@ -14,16 +14,29 @@ export class DashboardListCtrl { selectAllChecked = false; starredFilterOptions = [{text: 'Filter by Starred', disabled: true}, {text: 'Yes'}, {text: 'No'}]; selectedStarredFilter: any; + folderTitle = null; /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) { + constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv, private $routeParams) { this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0); this.query = {query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true}; + this.selectedStarredFilter = this.starredFilterOptions[0]; - this.getDashboards().then(() => { - this.getTags(); - }); + if (this.$routeParams.folderId && this.$routeParams.type && this.$routeParams.slug) { + backendSrv.getDashboard(this.$routeParams.type, this.$routeParams.slug).then(result => { + this.folderTitle = result.dashboard.title; + this.query.folderIds = [result.dashboard.id]; + + this.getDashboards().then(() => { + this.getTags(); + }); + }); + } else { + this.getDashboards().then(() => { + this.getTags(); + }); + } } getDashboards() { diff --git a/public/app/features/dashboard/partials/dashboardList.html b/public/app/features/dashboard/partials/dashboardList.html index 8a994f12c8c..7cd523dd671 100644 --- a/public/app/features/dashboard/partials/dashboardList.html +++ b/public/app/features/dashboard/partials/dashboardList.html @@ -1,17 +1,35 @@ -
-
- - -
-
-
+
+ + +
-
No dashboards matching your query were found.
+
No dashboards matching your query were found.
{ } }, q, searchSrvStub); + return new DashboardListCtrl({}, { getNav: () => { } }, q, searchSrvStub, {}); } diff --git a/public/sass/components/_dashboard_list.scss b/public/sass/components/_dashboard_list.scss index 9d0b3144ba6..e1574f46da6 100644 --- a/public/sass/components/_dashboard_list.scss +++ b/public/sass/components/_dashboard_list.scss @@ -1,20 +1,25 @@ .dashboard-list { - height: 75%; - .search-results-container { - padding-left: 0; - padding-right: 0; + padding: 5px 0 0 0; } } .search-results-filter-row { + height: 35px; display: flex; justify-content: space-between; + + .gf-form-button-row { + padding-top: 0; + + button:last-child { + margin-right: 0; + } + } } .search-results-filter-row__filters { display: flex; - width: 300px; } .search-results-filter-row__filters-item { diff --git a/public/sass/components/_switch.scss b/public/sass/components/_switch.scss index 4888f5c9c4a..b612091d171 100644 --- a/public/sass/components/_switch.scss +++ b/public/sass/components/_switch.scss @@ -102,12 +102,23 @@ $switch-height: 1.5rem; } } -.gf-form-switch--search-result__section, .gf-form-switch--search-result__item { - min-width: 2.6rem; - +.gf-form-switch--transparent { input + label { - background-color: inherit; - height: 1.7rem; + background: transparent; + } + + input + label::before, input + label::after { + background: transparent; + } + + &:hover { + input + label::before { + background: transparent; + } + + input + label::after { + background: transparent; + } } } @@ -115,57 +126,24 @@ $switch-height: 1.5rem; min-width: 3.3rem; margin-right: -0.3rem; - &:hover { - input + label::before { - @include buttonBackground($panel-bg, $panel-bg); - } - - input + label::after { - @include buttonBackground($panel-bg, $panel-bg, lighten($orange, 10%)); - } - } - - input + label::before, input + label::after { - @include buttonBackground($panel-bg, $panel-bg); - } - - input + label::before { - color: $gray-2 - } - - input + label::after { - color: $orange + input + label { + height: 1.7rem; } } .gf-form-switch--search-result__item { + min-width: 2.6rem; + input + label { height: 2.7rem; } +} +.gf-form-switch--search-result-filter-row__checkbox { + min-width: 4.7rem; - &:hover { - input + label::before { - @include buttonBackground($list-item-hover-bg, $list-item-hover-bg); - } - - input + label::after { - @include buttonBackground($list-item-hover-bg, $list-item-hover-bg); - color: lighten($orange, 10%); - } - - } - - input + label::before, input + label::after { - @include buttonBackground($list-item-hover-bg, $list-item-hover-bg); - } - - input + label::before { - color: $gray-2 - } - - input + label::after { - color: $orange + input + label { + height: 2.5rem; } } From 0029d96579629f496f819df21a3898825ae3ccfb Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Dec 2017 14:27:07 +0100 Subject: [PATCH 60/96] dashboard: Show CTA for empty lists/folders #10083 --- .../dashboard/partials/dashboardList.html | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/public/app/features/dashboard/partials/dashboardList.html b/public/app/features/dashboard/partials/dashboardList.html index 7cd523dd671..cecd52aa891 100644 --- a/public/app/features/dashboard/partials/dashboardList.html +++ b/public/app/features/dashboard/partials/dashboardList.html @@ -57,7 +57,20 @@
-
+
+ +
+ +
-
No dashboards matching your query were found.
Date: Wed, 6 Dec 2017 14:32:22 +0100 Subject: [PATCH 61/96] dashboard: fix search results tests #10083 --- public/app/core/components/search/search_results.jest.ts | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/public/app/core/components/search/search_results.jest.ts b/public/app/core/components/search/search_results.jest.ts index ab24242860e..fc661dc8472 100644 --- a/public/app/core/components/search/search_results.jest.ts +++ b/public/app/core/components/search/search_results.jest.ts @@ -8,7 +8,7 @@ describe('SearchResultsCtrl', () => { let selectionChanged = false; beforeEach(() => { - ctrl = new SearchResultsCtrl(); + ctrl = new SearchResultsCtrl({}); ctrl.onSelectionChanged = () => selectionChanged = true; ctrl.toggleSelection(item); }); @@ -27,7 +27,7 @@ describe('SearchResultsCtrl', () => { let selectionChanged = false; beforeEach(() => { - ctrl = new SearchResultsCtrl(); + ctrl = new SearchResultsCtrl({}); ctrl.onSelectionChanged = () => selectionChanged = true; ctrl.toggleSelection(item); }); @@ -45,7 +45,7 @@ describe('SearchResultsCtrl', () => { let selectedTag = null; beforeEach(() => { - ctrl = new SearchResultsCtrl(); + ctrl = new SearchResultsCtrl({}); ctrl.onTagSelected = (tag) => selectedTag = tag; ctrl.selectTag('tag-test'); }); @@ -64,7 +64,7 @@ describe('SearchResultsCtrl', () => { }; beforeEach(() => { - ctrl = new SearchResultsCtrl(); + ctrl = new SearchResultsCtrl({}); ctrl.toggleFolderExpand(folder); }); From ea3447fbfd67ead84db7ce869a1cd595338f44b3 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 6 Dec 2017 16:54:24 +0100 Subject: [PATCH 62/96] fixed 404 for grafana5 + now responsive (#10101) * fixed 404 for grafana5 + now resonsive * code formatting fixes --- public/app/partials/error.html | 109 ++++++++++++++++-------------- public/sass/pages/_errorpage.scss | 35 +++++++++- 2 files changed, 91 insertions(+), 53 deletions(-) diff --git a/public/app/partials/error.html b/public/app/partials/error.html index 0750e860dd5..a7c074fea94 100644 --- a/public/app/partials/error.html +++ b/public/app/partials/error.html @@ -1,55 +1,60 @@ - -
- -
-
-
-
-
-
-

100%

-

80%

-

60%

-

40%

-

20%

-

0%

-
-
- -
-

Then

-

Now

-
-
-
-
-
-
-

current

-
-
- -
-
-

Chances you are on the page you are looking for.

-

0%

-
-
-

Sorry for the inconvenience

-

Please go back to your home dashboard and try again.

-

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

-
-
-
-
- -
-
- +
+ +
+

+ Page not found +

+
+ 404 Error +
+
+
+
+
+
+
+
+

100%

+

80%

+

60%

+

40%

+

20%

+

0%

+
+
+ +
+

Then

+

Now

+
+
+
+
+
+
+

current

+
+
+ +
+
+

Chances you are on the page you are looking for.

+

0%

+
+
+

Sorry for the inconvenience

+

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

+

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

+
+
+
+
+ +
diff --git a/public/sass/pages/_errorpage.scss b/public/sass/pages/_errorpage.scss index e18306ea05a..dac2768e89b 100644 --- a/public/sass/pages/_errorpage.scss +++ b/public/sass/pages/_errorpage.scss @@ -3,6 +3,11 @@ // Layout // +.error-container { + display: flex; + flex-direction: row; +} + .error-row { display: flex; flex-direction: row; @@ -22,7 +27,7 @@ .info-box { width: 38%; - padding: 2rem 1rem 6rem; + padding: 2rem 1rem 2rem; } .graph-percentage {padding: 0 0 1.5rem;} @@ -58,3 +63,31 @@ } .graph-text {margin: 0;} + +@include media-breakpoint-down(sm) { + .graph-box { + width: 50%; + } + + .info-box { + width: 50%; + } +} + +@include media-breakpoint-down(xs) { + .error-container { + flex-direction: column; + } + + .graph-box { + width: 100%; + } + + .info-box { + width: 100%; + } + + .error-full-width { + width: 100%; + } +} From 8adaf99bff25a353ef3cdaafb765fdaff7e31905 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Wed, 6 Dec 2017 16:55:12 +0100 Subject: [PATCH 63/96] other panels now hidden, fixes 10088 (#10102) --- public/sass/components/_dashboard_grid.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index a6dd09bcd5a..b7886a3a2dc 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -8,7 +8,7 @@ } .react-grid-item { - display: none; + display: none !important; transition-property: none !important; } From e8807f4bce62db7a7b95a852c081a947eb886f11 Mon Sep 17 00:00:00 2001 From: Johannes Schill Date: Wed, 6 Dec 2017 17:36:44 +0100 Subject: [PATCH 64/96] Fixing tabs for Grafana 5 - #10082 (#10103) * ux: Make new tabs responsive #10082 * ux: Add possibility to manipulate url in angular router outside of angular - and use it in the responsive navigation #10082 --- public/app/core/angular_wrappers.ts | 6 +-- .../{ => PageHeader}/PageHeader.tsx | 45 +++++++++++++++++-- public/app/core/components/grafana_app.ts | 3 +- public/app/core/services/all.js | 1 + public/app/core/services/global_event_srv.ts | 21 +++++++++ public/app/core/utils/react2angular.ts | 3 -- public/app/features/plugins/ds_list_ctrl.ts | 14 +++++- public/sass/_variables.scss | 2 +- public/sass/components/_page_header.scss | 15 +++++++ public/sass/components/_tabs.scss | 14 +++++- 10 files changed, 109 insertions(+), 15 deletions(-) rename public/app/core/components/{ => PageHeader}/PageHeader.tsx (54%) create mode 100644 public/app/core/services/global_event_srv.ts diff --git a/public/app/core/angular_wrappers.ts b/public/app/core/angular_wrappers.ts index 3880be7de8d..4cb3ca513cc 100644 --- a/public/app/core/angular_wrappers.ts +++ b/public/app/core/angular_wrappers.ts @@ -1,12 +1,10 @@ import { react2AngularDirective } from 'app/core/utils/react2angular'; import { PasswordStrength } from './components/PasswordStrength'; -import PageHeader from './components/PageHeader'; +import PageHeader from './components/PageHeader/PageHeader'; import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA'; export function registerAngularDirectives() { - react2AngularDirective('passwordStrength', PasswordStrength, ['password']); - react2AngularDirective('pageHeader', PageHeader, ['model', "noTabs"]); + react2AngularDirective('pageHeader', PageHeader, ['model', 'noTabs']); react2AngularDirective('emptyListCta', EmptyListCTA, ['model']); - } diff --git a/public/app/core/components/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx similarity index 54% rename from public/app/core/components/PageHeader.tsx rename to public/app/core/components/PageHeader/PageHeader.tsx index e0accc03fd4..5371b464ec3 100644 --- a/public/app/core/components/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -1,6 +1,7 @@ import React from 'react'; -import { NavModel, NavModelItem } from '../nav_model_srv'; +import { NavModel, NavModelItem } from '../../nav_model_srv'; import classNames from 'classnames'; +import appEvents from 'app/core/app_events'; export interface IProps { model: NavModel; @@ -26,8 +27,44 @@ function TabItem(tab: NavModelItem) { ); } -function Tabs({main}: {main: NavModelItem}) { - return
    {main.children.map(TabItem)}
; +function SelectOption(navItem: NavModelItem) { + if (navItem.hideFromTabs) { // TODO: Rename hideFromTabs => hideFromNav + return (null); + } + + return ( + + ); +} + +function Navigation({main}: {main: NavModelItem}) { + return (); +} + +function SelectNav({main, customCss}: {main: NavModelItem, customCss: string}) { + const defaultSelectedItem = main.children.find(navItem => { + return navItem.active === true; + }); + + const gotoUrl = evt => { + var element = evt.target; + var url = element.options[element.selectedIndex].value; + appEvents.emit('location-change', {href: url}); + }; + + return (); +} + +function Tabs({main, customCss}: {main: NavModelItem, customCss: string}) { + return
    {main.children.map(TabItem)}
; } export default class PageHeader extends React.Component { @@ -63,7 +100,7 @@ export default class PageHeader extends React.Component {
{this.renderHeaderTitle(this.props.model.main)} - {this.props.model.main.children && } + {this.props.model.main.children && }
diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index c60c8a20a4c..77089b14ddc 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -12,7 +12,7 @@ import Drop from 'tether-drop'; export class GrafanaCtrl { /** @ngInject */ - constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv) { + constructor($scope, alertSrv, utilSrv, $rootScope, $controller, contextSrv, globalEventSrv) { $scope.init = function() { $scope.contextSrv = contextSrv; @@ -23,6 +23,7 @@ export class GrafanaCtrl { profiler.init(config, $rootScope); alertSrv.init(); utilSrv.init(); + globalEventSrv.init(); $scope.dashAlerts = alertSrv; }; diff --git a/public/app/core/services/all.js b/public/app/core/services/all.js index a308febb219..0053d789cbe 100644 --- a/public/app/core/services/all.js +++ b/public/app/core/services/all.js @@ -8,5 +8,6 @@ define([ './segment_srv', './backend_srv', './dynamic_directive_srv', + './global_event_srv' ], function () {}); diff --git a/public/app/core/services/global_event_srv.ts b/public/app/core/services/global_event_srv.ts new file mode 100644 index 00000000000..a4d5865eb63 --- /dev/null +++ b/public/app/core/services/global_event_srv.ts @@ -0,0 +1,21 @@ +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; + +// This service is for registering global events. +// Good for communication react > angular and vice verse +export class GlobalEventSrv { + + /** @ngInject */ + constructor(private $location, private $timeout) { + } + + init() { + appEvents.on('location-change', payload => { + this.$timeout(() => { // A hack to use timeout when we're changing things (in this case the url) from outside of Angular. + this.$location.path(payload.href); + }); + }); + } +} + +coreModule.service('globalEventSrv', GlobalEventSrv); diff --git a/public/app/core/utils/react2angular.ts b/public/app/core/utils/react2angular.ts index ad6f7476d6a..e7ad3502f88 100644 --- a/public/app/core/utils/react2angular.ts +++ b/public/app/core/utils/react2angular.ts @@ -1,10 +1,7 @@ import coreModule from 'app/core/core_module'; export function react2AngularDirective(name: string, component: any, options: any) { - coreModule.directive(name, ['reactDirective', reactDirective => { return reactDirective(component, options); }]); - } - diff --git a/public/app/features/plugins/ds_list_ctrl.ts b/public/app/features/plugins/ds_list_ctrl.ts index 5feadb68fe3..ce9047166ef 100644 --- a/public/app/features/plugins/ds_list_ctrl.ts +++ b/public/app/features/plugins/ds_list_ctrl.ts @@ -1,6 +1,7 @@ /// import coreModule from '../../core/core_module'; +import {appEvents} from 'app/core/core'; export class DataSourcesCtrl { datasources: any; @@ -11,13 +12,24 @@ export class DataSourcesCtrl { private $scope, private backendSrv, private datasourceSrv, + private $location, private navModelSrv) { this.navModel = this.navModelSrv.getNav('cfg', 'datasources', 0); - + this.navigateToUrl = this.navigateToUrl.bind(this); backendSrv.get('/api/datasources').then(result => { this.datasources = result; }); + + appEvents.on('location-change', payload => { + this.navigateToUrl(payload.href); + }); + } + + navigateToUrl(url) { + // debugger; + this.$location.path(url); + this.$location.replace(); } removeDataSourceConfirmed(ds) { diff --git a/public/sass/_variables.scss b/public/sass/_variables.scss index bb4645504aa..9a1e42995b3 100644 --- a/public/sass/_variables.scss +++ b/public/sass/_variables.scss @@ -235,5 +235,5 @@ $dashboard-padding: $panel-margin * 2; $panel-padding: 0px 10px 5px 10px; // tabs -$tabs-padding: 9px 15px 9px; +$tabs-padding: 10px 15px 9px; diff --git a/public/sass/components/_page_header.scss b/public/sass/components/_page_header.scss index b6f44be1c18..dc570e8f13c 100644 --- a/public/sass/components/_page_header.scss +++ b/public/sass/components/_page_header.scss @@ -72,6 +72,21 @@ text-transform: uppercase; } +.page-header__select_nav { + margin-bottom: 10px; + + @include media-breakpoint-up(lg) { + display: none; + } +} + +.page-header__tabs { + display: none; + @include media-breakpoint-up(lg) { + display: block; + } +} + .page-breadcrumbs { display: flex; padding: 10px 0; diff --git a/public/sass/components/_tabs.scss b/public/sass/components/_tabs.scss index 80874241b79..396c3f98880 100644 --- a/public/sass/components/_tabs.scss +++ b/public/sass/components/_tabs.scss @@ -16,7 +16,7 @@ position: relative; display: block; border: solid transparent; - border-width: 2px 1px 1px; + border-width: 0 1px 1px; border-radius: 3px 3px 0 0; i { @@ -34,6 +34,18 @@ border-color: $orange $tab-border-color transparent; background: $page-bg; color: $link-color; + overflow: hidden; + + &::before { + display: block; + content: ' '; + position: absolute; + left: 0; + right: 0; + height: 2px; + top: 0; + background-image: linear-gradient(to right, #ffd500 0%, #ff4400 99%, #ff4400 100%); + } } } From 373389c920bf3eacd13fad7ff617aa8b75613c34 Mon Sep 17 00:00:00 2001 From: Sven Klemm <31455525+svenklemm@users.noreply.github.com> Date: Wed, 6 Dec 2017 18:04:33 +0100 Subject: [PATCH 65/96] treat any text column in timeseries query as metric name unless column (#9985) named metric is present --- pkg/tsdb/postgres/postgres.go | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index dcb60977edc..56d3fcc9460 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -142,8 +142,13 @@ func (e PostgresQueryEndpoint) getTypedRowData(rows *core.Rows) (tsdb.RowValues, func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *core.Rows, result *tsdb.QueryResult) error { pointsBySeries := make(map[string]*tsdb.TimeSeries) seriesByQueryOrder := list.New() - columnNames, err := rows.Columns() + columnNames, err := rows.Columns() + if err != nil { + return err + } + + columnTypes, err := rows.ColumnTypes() if err != nil { return err } @@ -153,13 +158,21 @@ func (e PostgresQueryEndpoint) transformToTimeSeries(query *tsdb.Query, rows *co timeIndex := -1 metricIndex := -1 - // check columns of resultset + // check columns of resultset: a column named time is mandatory + // the first text column is treated as metric name unless a column named metric is present for i, col := range columnNames { switch col { case "time": timeIndex = i case "metric": metricIndex = i + default: + if metricIndex == -1 { + switch columnTypes[i].DatabaseTypeName() { + case "UNKNOWN", "TEXT", "VARCHAR", "CHAR": + metricIndex = i + } + } } } From c80eadcdf4a4e7ae6d10f188b9591844d8b9d47b Mon Sep 17 00:00:00 2001 From: Sven Klemm <31455525+svenklemm@users.noreply.github.com> Date: Wed, 6 Dec 2017 18:12:24 +0100 Subject: [PATCH 66/96] handle native postgres datetime types in annotation queries (#9986) --- pkg/tsdb/postgres/postgres.go | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/pkg/tsdb/postgres/postgres.go b/pkg/tsdb/postgres/postgres.go index 56d3fcc9460..a8c96d8119c 100644 --- a/pkg/tsdb/postgres/postgres.go +++ b/pkg/tsdb/postgres/postgres.go @@ -78,6 +78,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro rowLimit := 1000000 rowCount := 0 + timeIndex := -1 + + // check if there is a column named time + for i, col := range columnNames { + switch col { + case "time": + timeIndex = i + } + } for ; rows.Next(); rowCount++ { if rowCount > rowLimit { @@ -89,6 +98,15 @@ func (e PostgresQueryEndpoint) transformToTable(query *tsdb.Query, rows *core.Ro return err } + // convert column named time to unix timestamp to make + // native datetime postgres types work in annotation queries + if timeIndex != -1 { + switch value := values[timeIndex].(type) { + case time.Time: + values[timeIndex] = float64(value.UnixNano() / 1e9) + } + } + table.Rows = append(table.Rows, values) } From 2cf1c29eae283d9dd3df61a6649e3922ce9c3702 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 6 Dec 2017 21:30:14 +0300 Subject: [PATCH 67/96] graph: refactor (don't render twice) --- public/app/plugins/panel/graph/graph.ts | 36 +++---------------------- 1 file changed, 3 insertions(+), 33 deletions(-) diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 80f829379eb..d24aedc66b9 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -34,7 +34,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { var data; var plot; var sortedSeries; - var legendSideLastValue = null; var rootScope = scope.$root; var panelWidth = 0; var eventManager = new EventManager(ctrl); @@ -95,25 +94,14 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { }, scope); function getLegendHeight(panelHeight) { - const LEGEND_TABLE_LINE_HEIGHT = 21; const LEGEND_PADDING = 23; if (!panel.legend.show || panel.legend.rightSide) { return 0; } - let legendSeries = _.filter(data, function(series) { - return series.hideFromLegend(panel.legend) === false; - }); - - if (panel.legend.alignAsTable) { - let total = LEGEND_PADDING + (LEGEND_TABLE_LINE_HEIGHT * legendSeries.length); - return Math.min(total, Math.floor(panelHeight/2)); - } else { - let legendHeight = getLegendContainerHeight(); - let total = LEGEND_PADDING + (legendHeight); - return Math.min(total, Math.floor(panelHeight/2)); - } + let legendHeight = getLegendContainerHeight() + LEGEND_PADDING; + return Math.min(legendHeight, Math.floor(panelHeight/2)); } function getLegendContainerHeight() { @@ -254,15 +242,7 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { eventManager.addFlotEvents(annotations, options); sortedSeries = sortSeries(data, panel); - - if (shouldDelayDraw(panel)) { - // temp fix for legends on the side, need to render twice to get dimensions right - callPlot(options, false); - setTimeout(function() { callPlot(options, true); }, 50); - legendSideLastValue = panel.legend.rightSide; - } else { - callPlot(options, true); - } + callPlot(options, true); } function buildFlotPairs(data) { @@ -446,16 +426,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { } } - function shouldDelayDraw(panel) { - if (panel.legend.rightSide) { - return true; - } - if (legendSideLastValue !== null && panel.legend.rightSide !== legendSideLastValue) { - return true; - } - return false; - } - function addTimeAxis(options) { var ticks = panelWidth / 100; var min = _.isUndefined(ctrl.range.from) ? null : ctrl.range.from.valueOf(); From f87b9aaa8a3f5e518942eab1a9d8065747980ba1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Dec 2017 20:34:41 +0100 Subject: [PATCH 68/96] dashboard: keyboard nav in dashboard search - closes #10100 Pressing enter/return for a folder toggles it. Pressing enter/return for a dashboard navigates to it. --- .../app/core/components/search/search.jest.ts | 330 ++++++++++++++++++ public/app/core/components/search/search.ts | 93 ++++- .../components/search/search_results.html | 2 +- public/app/core/services/search_srv.ts | 4 - .../features/dashboard/dashboard_list_ctrl.ts | 4 - .../specs/dashboard_list_ctrl.jest.ts | 3 - public/sass/components/_search.scss | 10 +- 7 files changed, 418 insertions(+), 28 deletions(-) create mode 100644 public/app/core/components/search/search.jest.ts diff --git a/public/app/core/components/search/search.jest.ts b/public/app/core/components/search/search.jest.ts new file mode 100644 index 00000000000..6eaec024e5f --- /dev/null +++ b/public/app/core/components/search/search.jest.ts @@ -0,0 +1,330 @@ +import { SearchCtrl } from './search'; + +describe('SearchCtrl', () => { + let ctrl = new SearchCtrl({}, {}, {}, {}, { onAppEvent: () => { } }); + + describe('Given an empty result', () => { + beforeEach(() => { + ctrl.results = []; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); + }); + }); + + describe('Given a result of one selected collapsed folder with no dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [], + selected: true, + expanded: false, + toggle: (i) => i.expanded = !i.expanded + }, + { + id: 0, + title: 'Root', + items: [ + { id: 3, selected: false }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of one selected collapsed folder with 2 dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [ + { id: 2, selected: false }, + { id: 4, selected: false } + ], + selected: true, + expanded: false, + toggle: (i) => i.expanded = !i.expanded + }, + { + id: 0, + title: 'Root', + items: [ + { id: 3, selected: false }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of a search with 2 dashboards where the first is selected', () => { + beforeEach(() => { + ctrl.results = [ + { + hideHeader: true, + items: [ + { id: 3, selected: true }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + }); +}); diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index f8dc871e192..08d050c81bf 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -64,18 +64,70 @@ export class SearchCtrl { this.moveSelection(-1); } if (evt.keyCode === 13) { - var selectedDash = this.results[this.selectedIndex]; - if (selectedDash) { - this.$location.search({}); - this.$location.path(selectedDash.url); + const flattenedResult = this.getFlattenedResultForNavigation(); + const currentItem = flattenedResult[this.selectedIndex]; + + if (currentItem) { + if (currentItem.dashboardIndex !== undefined) { + const selectedDash = this.results[currentItem.folderIndex].items[currentItem.dashboardIndex]; + + if (selectedDash) { + this.$location.search({}); + this.$location.path(selectedDash.url); + } + } else { + const selectedFolder = this.results[currentItem.folderIndex]; + + if (selectedFolder) { + selectedFolder.toggle(selectedFolder); + } + } } } } moveSelection(direction) { - var max = (this.results || []).length; - var newIndex = this.selectedIndex + direction; + if (this.results.length === 0) { + return; + } + + const flattenedResult = this.getFlattenedResultForNavigation(); + const currentItem = flattenedResult[this.selectedIndex]; + + if (currentItem) { + if (currentItem.dashboardIndex !== undefined) { + this.results[currentItem.folderIndex].items[currentItem.dashboardIndex].selected = false; + } else { + this.results[currentItem.folderIndex].selected = false; + } + } + + const max = flattenedResult.length; + let newIndex = this.selectedIndex + direction; this.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex; + const selectedItem = flattenedResult[this.selectedIndex]; + + if (selectedItem.dashboardIndex === undefined && this.results[selectedItem.folderIndex].id === 0) { + this.moveSelection(direction); + return; + } + + if (selectedItem.dashboardIndex !== undefined) { + if (!this.results[selectedItem.folderIndex].expanded) { + this.moveSelection(direction); + return; + } + + this.results[selectedItem.folderIndex].items[selectedItem.dashboardIndex].selected = true; + return; + } + + if (this.results[selectedItem.folderIndex].hideHeader) { + this.moveSelection(direction); + return; + } + + this.results[selectedItem.folderIndex].selected = true; } searchDashboards() { @@ -84,8 +136,9 @@ export class SearchCtrl { return this.searchSrv.search(this.query).then(results => { if (localSearchId < this.currentSearchId) { return; } - this.results = results; + this.results = results || []; this.isLoading = false; + this.moveSelection(1); }); } @@ -125,12 +178,32 @@ export class SearchCtrl { search() { this.showImport = false; - this.selectedIndex = 0; + this.selectedIndex = -1; this.searchDashboards(); } - toggleFolder(section) { - this.searchSrv.toggleSection(section); + private getFlattenedResultForNavigation() { + let folderIndex = 0; + + return _.flatMap(this.results, (s) => { + let result = []; + + result.push({ + folderIndex: folderIndex + }); + + let dashboardIndex = 0; + + result = result.concat(_.map(s.items || [], (i) => { + return { + folderIndex: folderIndex, + dashboardIndex: dashboardIndex++ + }; + })); + + folderIndex++; + return result; + }); } } diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 4a235801ee9..2008910f53a 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -1,5 +1,5 @@
- +
{ this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); diff --git a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts index a84ea009af9..1c13d138b6d 100644 --- a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts @@ -537,9 +537,6 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) { search: (options: any) => { return q.resolve(searchResponse); }, - toggleSection: (section) => { - return; - }, getDashboardTags: () => { return q.resolve(tags || []); } diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 0278fd7afb2..caf6084fb9e 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -120,8 +120,9 @@ display: flex; flex-grow: 1; - &:hover { - color: $text-color-weak; + &:hover, &.selected { + color: $link-hover-color; + .search-section__header__toggle { background: $tight-form-func-bg; color: $link-hover-color; @@ -151,11 +152,8 @@ white-space: nowrap; padding: 0px; - &:hover { + &:hover, &.selected { @include left-brand-border-gradient(); - } - - &.selected { background: $list-item-hover-bg; } } From 58fb35c5cbc3d056093420a2c13a88788cb6134c Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Dec 2017 23:51:39 +0100 Subject: [PATCH 69/96] dashboard: fix linting and formating - #10100 --- .../app/core/components/search/search.jest.ts | 633 +++++++++--------- 1 file changed, 319 insertions(+), 314 deletions(-) diff --git a/public/app/core/components/search/search.jest.ts b/public/app/core/components/search/search.jest.ts index 6eaec024e5f..68b1292f9d4 100644 --- a/public/app/core/components/search/search.jest.ts +++ b/public/app/core/components/search/search.jest.ts @@ -1,330 +1,335 @@ import { SearchCtrl } from './search'; +import { SearchSrv } from 'app/core/services/search_srv'; describe('SearchCtrl', () => { - let ctrl = new SearchCtrl({}, {}, {}, {}, { onAppEvent: () => { } }); + const searchSrvStub = { + search: (options: any) => {}, + getDashboardTags: () => {} + }; + let ctrl = new SearchCtrl({}, {}, {}, searchSrvStub, { onAppEvent: () => { } }); - describe('Given an empty result', () => { - beforeEach(() => { - ctrl.results = []; - }); - - describe('When navigating down one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(1); - }); - - it('should not navigate', () => { - expect(ctrl.selectedIndex).toBe(0); - }); - }); - - describe('When navigating up one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(-1); - }); - - it('should not navigate', () => { - expect(ctrl.selectedIndex).toBe(0); - }); - }); + describe('Given an empty result', () => { + beforeEach(() => { + ctrl.results = []; }); - describe('Given a result of one selected collapsed folder with no dashboards and a root folder with 2 dashboards', () => { - beforeEach(() => { - ctrl.results = [ - { - id: 1, - title: 'folder', - items: [], - selected: true, - expanded: false, - toggle: (i) => i.expanded = !i.expanded - }, - { - id: 0, - title: 'Root', - items: [ - { id: 3, selected: false }, - { id: 5, selected: false } - ], - selected: false, - expanded: true, - toggle: (i) => i.expanded = !i.expanded - } - ]; - }); + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); - describe('When navigating down one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(1); - }); - - it('should select first dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeTruthy(); - expect(ctrl.results[1].items[1].selected).toBeFalsy(); - }); - }); - - describe('When navigating down two steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(1); - ctrl.moveSelection(1); - }); - - it('should select last dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeFalsy(); - expect(ctrl.results[1].items[1].selected).toBeTruthy(); - }); - }); - - describe('When navigating down three steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(1); - ctrl.moveSelection(1); - ctrl.moveSelection(1); - }); - - it('should select first folder', () => { - expect(ctrl.results[0].selected).toBeTruthy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeFalsy(); - expect(ctrl.results[1].items[1].selected).toBeFalsy(); - }); - }); - - describe('When navigating up one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(-1); - }); - - it('should select last dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeFalsy(); - expect(ctrl.results[1].items[1].selected).toBeTruthy(); - }); - }); - - describe('When navigating up two steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(-1); - ctrl.moveSelection(-1); - }); - - it('should select first dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeTruthy(); - expect(ctrl.results[1].items[1].selected).toBeFalsy(); - }); - }); + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); }); - describe('Given a result of one selected collapsed folder with 2 dashboards and a root folder with 2 dashboards', () => { - beforeEach(() => { - ctrl.results = [ - { - id: 1, - title: 'folder', - items: [ - { id: 2, selected: false }, - { id: 4, selected: false } - ], - selected: true, - expanded: false, - toggle: (i) => i.expanded = !i.expanded - }, - { - id: 0, - title: 'Root', - items: [ - { id: 3, selected: false }, - { id: 5, selected: false } - ], - selected: false, - expanded: true, - toggle: (i) => i.expanded = !i.expanded - } - ]; - }); + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); - describe('When navigating down one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(1); - }); + it('should not navigate', () => { + expect(ctrl.selectedIndex).toBe(0); + }); + }); + }); - it('should select first dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeTruthy(); - expect(ctrl.results[1].items[1].selected).toBeFalsy(); - }); - }); - - describe('When navigating down two steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(1); - ctrl.moveSelection(1); - }); - - it('should select last dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeFalsy(); - expect(ctrl.results[1].items[1].selected).toBeTruthy(); - }); - }); - - describe('When navigating down three steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(1); - ctrl.moveSelection(1); - ctrl.moveSelection(1); - }); - - it('should select first folder', () => { - expect(ctrl.results[0].selected).toBeTruthy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeFalsy(); - expect(ctrl.results[1].items[1].selected).toBeFalsy(); - }); - }); - - describe('When navigating up one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(-1); - }); - - it('should select last dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeFalsy(); - expect(ctrl.results[1].items[1].selected).toBeTruthy(); - }); - }); - - describe('When navigating up two steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 0; - ctrl.moveSelection(-1); - ctrl.moveSelection(-1); - }); - - it('should select first dashboard in root folder', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[1].selected).toBeFalsy(); - expect(ctrl.results[1].items[0].selected).toBeTruthy(); - expect(ctrl.results[1].items[1].selected).toBeFalsy(); - }); - }); + describe('Given a result of one selected collapsed folder with no dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [], + selected: true, + expanded: false, + toggle: (i) => i.expanded = !i.expanded + }, + { + id: 0, + title: 'Root', + items: [ + { id: 3, selected: false }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; }); - describe('Given a result of a search with 2 dashboards where the first is selected', () => { - beforeEach(() => { - ctrl.results = [ - { - hideHeader: true, - items: [ - { id: 3, selected: true }, - { id: 5, selected: false } - ], - selected: false, - expanded: true, - toggle: (i) => i.expanded = !i.expanded - } - ]; - }); + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); - describe('When navigating down one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 1; - ctrl.moveSelection(1); - }); - - it('should select last dashboard', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[1].selected).toBeTruthy(); - }); - }); - - describe('When navigating down two steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 1; - ctrl.moveSelection(1); - ctrl.moveSelection(1); - }); - - it('should select first dashboard', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeTruthy(); - expect(ctrl.results[0].items[1].selected).toBeFalsy(); - }); - }); - - describe('When navigating down three steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 1; - ctrl.moveSelection(1); - ctrl.moveSelection(1); - ctrl.moveSelection(1); - }); - - it('should select last dashboard', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[1].selected).toBeTruthy(); - }); - }); - - describe('When navigating up one step', () => { - beforeEach(() => { - ctrl.selectedIndex = 1; - ctrl.moveSelection(-1); - }); - - it('should select last dashboard', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[1].selected).toBeTruthy(); - }); - }); - - describe('When navigating up two steps', () => { - beforeEach(() => { - ctrl.selectedIndex = 1; - ctrl.moveSelection(-1); - ctrl.moveSelection(-1); - }); - - it('should select first dashboard', () => { - expect(ctrl.results[0].selected).toBeFalsy(); - expect(ctrl.results[0].items[0].selected).toBeTruthy(); - expect(ctrl.results[0].items[1].selected).toBeFalsy(); - }); - }); + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of one selected collapsed folder with 2 dashboards and a root folder with 2 dashboards', () => { + beforeEach(() => { + ctrl.results = [ + { + id: 1, + title: 'folder', + items: [ + { id: 2, selected: false }, + { id: 4, selected: false } + ], + selected: true, + expanded: false, + toggle: (i) => i.expanded = !i.expanded + }, + { + id: 0, + title: 'Root', + items: [ + { id: 3, selected: false }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first folder', () => { + expect(ctrl.results[0].selected).toBeTruthy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeFalsy(); + expect(ctrl.results[1].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 0; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard in root folder', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[1].selected).toBeFalsy(); + expect(ctrl.results[1].items[0].selected).toBeTruthy(); + expect(ctrl.results[1].items[1].selected).toBeFalsy(); + }); + }); + }); + + describe('Given a result of a search with 2 dashboards where the first is selected', () => { + beforeEach(() => { + ctrl.results = [ + { + hideHeader: true, + items: [ + { id: 3, selected: true }, + { id: 5, selected: false } + ], + selected: false, + expanded: true, + toggle: (i) => i.expanded = !i.expanded + } + ]; + }); + + describe('When navigating down one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating down two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + + describe('When navigating down three steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(1); + ctrl.moveSelection(1); + ctrl.moveSelection(1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up one step', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + }); + + it('should select last dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[1].selected).toBeTruthy(); + }); + }); + + describe('When navigating up two steps', () => { + beforeEach(() => { + ctrl.selectedIndex = 1; + ctrl.moveSelection(-1); + ctrl.moveSelection(-1); + }); + + it('should select first dashboard', () => { + expect(ctrl.results[0].selected).toBeFalsy(); + expect(ctrl.results[0].items[0].selected).toBeTruthy(); + expect(ctrl.results[0].items[1].selected).toBeFalsy(); + }); + }); + }); }); From 3cdf0dce562692ad93125ee09bbc51fe7530c3bc Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Wed, 6 Dec 2017 23:50:29 +0300 Subject: [PATCH 70/96] graph: move auto decimals calc to ticks.ts and use it for legend values format. --- public/app/core/core.ts | 6 +- public/app/core/time_series2.ts | 38 ++++++++ public/app/core/utils/ticks.ts | 107 +++++++++++++++++++++++ public/app/plugins/panel/graph/graph.ts | 24 +---- public/app/plugins/panel/graph/legend.ts | 7 ++ 5 files changed, 158 insertions(+), 24 deletions(-) diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 602cb49cb68..4bb1e43cc25 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -52,6 +52,8 @@ import {gfPageDirective} from './components/gf_page'; import {orgSwitcher} from './components/org_switcher'; import {profiler} from './profiler'; import {registerAngularDirectives} from './angular_wrappers'; +import {updateLegendValues} from './time_series2'; +import TimeSeries from './time_series2'; export { profiler, @@ -83,5 +85,7 @@ export { userGroupPicker, geminiScrollbar, gfPageDirective, - orgSwitcher + orgSwitcher, + TimeSeries, + updateLegendValues }; diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index 0f3dcbc1171..bf8a38761d3 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -1,4 +1,5 @@ import kbn from 'app/core/utils/kbn'; +import {getFlotTickDecimals} from 'app/core/utils/ticks'; import _ from 'lodash'; function matchSeriesOverride(aliasOrRegex, seriesAlias) { @@ -16,6 +17,43 @@ function translateFillOption(fill) { return fill === 0 ? 0.001 : fill/10; } +/** + * Calculate decimals for legend and update values for each series. + * @param data series data + * @param panel + * @param height Graph height + */ +export function updateLegendValues(data: TimeSeries[], panel, height) { + for (let i = 0; i < data.length; i++) { + let series = data[i]; + let yaxes = panel.yaxes; + let axis = yaxes[series.yaxis - 1]; + let {tickDecimals, scaledDecimals} = getFlotTickDecimals(data, axis, height); + let formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; + + // decimal override + if (_.isNumber(panel.decimals)) { + series.updateLegendValues(formater, panel.decimals, null); + } else { + // auto decimals + // legend and tooltip gets one more decimal precision + // than graph legend ticks + tickDecimals = (tickDecimals || -1) + 1; + series.updateLegendValues(formater, tickDecimals, scaledDecimals + 2); + } + } +} + +export function getDataMinMax(data: TimeSeries[]) { + const datamin = _.minBy(data, (series) => { + return series.stats.min; + }).stats.min; + const datamax = _.maxBy(data, (series: TimeSeries) => { + return series.stats.max; + }).stats.max; + return {datamin, datamax}; +} + export default class TimeSeries { datapoints: any; id: string; diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index b033e9247a1..334090c056f 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -1,3 +1,5 @@ +import {getDataMinMax} from 'app/core/time_series2'; + /** * Calculate tick step. * Implementation from d3-array (ticks.js) @@ -32,6 +34,7 @@ export function getScaledDecimals(decimals, tick_size) { /** * Calculate tick size based on min and max values, number of ticks and precision. + * Implementation from Flot. * @param min Axis minimum * @param max Axis maximum * @param noTicks Number of ticks @@ -65,3 +68,107 @@ export function getFlotTickSize(min: number, max: number, noTicks: number, tickD return size; } + +/** + * Calculate axis range (min and max). + * Implementation from Flot. + */ +export function getFlotRange(panelMin, panelMax, datamin, datamax) { + const autoscaleMargin = 0.02; + + let min = +(panelMin != null ? panelMin : datamin); + let max = +(panelMax != null ? panelMax : datamax); + let delta = max - min; + + if (delta === 0.0) { + // Grafana fix: wide Y min and max using increased wideFactor + // when all series values are the same + var wideFactor = 0.25; + var widen = Math.abs(max === 0 ? 1 : max * wideFactor); + + if (panelMin === null) { + min -= widen; + } + // always widen max if we couldn't widen min to ensure we + // don't fall into min == max which doesn't work + if (panelMax == null || panelMin != null) { + max += widen; + } + } else { + // consider autoscaling + var margin = autoscaleMargin; + if (margin != null) { + if (panelMin == null) { + min -= delta * margin; + // make sure we don't go below zero if all values + // are positive + if (min < 0 && datamin != null && datamin >= 0) { + min = 0; + } + } + if (panelMax == null) { + max += delta * margin; + if (max > 0 && datamax != null && datamax <= 0) { + max = 0; + } + } + } + } + return {min, max}; +} + +/** + * Estimate number of ticks for Y axis. + * Implementation from Flot. + */ +export function getFlotNumberOfTicks(height, ticks?) { + let noTicks; + if (typeof ticks === "number" && ticks > 0) { + noTicks = ticks; + } else { + // heuristic based on the model a*sqrt(x) fitted to + // some data points that seemed reasonable + noTicks = 0.3 * Math.sqrt(height); + } + return noTicks; +} + +/** + * Calculate tick decimals. + * Implementation from Flot. + */ +export function getFlotTickDecimals(data, axis, height) { + let {datamin, datamax} = getDataMinMax(data); + let {min, max} = getFlotRange(axis.min, axis.max, datamin, datamax); + let noTicks = getFlotNumberOfTicks(height); + let tickDecimals, maxDec; + let delta = (max - min) / noTicks; + let dec = -Math.floor(Math.log(delta) / Math.LN10); + + let magn = Math.pow(10, -dec); + // norm is between 1.0 and 10.0 + let norm = delta / magn; + let size; + + if (norm < 1.5) { + size = 1; + } else if (norm < 3) { + size = 2; + // special case for 2.5, requires an extra decimal + if (norm > 2.25 && (maxDec == null || dec + 1 <= maxDec)) { + size = 2.5; + ++dec; + } + } else if (norm < 7.5) { + size = 5; + } else { + size = 10; + } + + size *= magn; + + tickDecimals = Math.max(0, maxDec != null ? maxDec : dec); + // grafana addition + const scaledDecimals = tickDecimals - Math.floor(Math.log(size) / Math.LN10); + return {tickDecimals, scaledDecimals}; +} diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index d24aedc66b9..30761238d82 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -22,7 +22,7 @@ import {EventManager} from 'app/features/annotations/all'; import {convertValuesToHistogram, getSeriesValues} from './histogram'; /** @ngInject **/ -function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { +function graphDirective(timeSrv, popoverSrv, contextSrv) { return { restrict: 'A', template: '', @@ -34,7 +34,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { var data; var plot; var sortedSeries; - var rootScope = scope.$root; var panelWidth = 0; var eventManager = new EventManager(ctrl); var thresholdManager = new ThresholdManager(ctrl); @@ -143,27 +142,6 @@ function graphDirective($rootScope, timeSrv, popoverSrv, contextSrv) { } function drawHook(plot) { - // Update legend values - var yaxis = plot.getYAxes(); - for (var i = 0; i < data.length; i++) { - var series = data[i]; - var axis = yaxis[series.yaxis - 1]; - var formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; - - // decimal override - if (_.isNumber(panel.decimals)) { - series.updateLegendValues(formater, panel.decimals, null); - } else { - // auto decimals - // legend and tooltip gets one more decimal precision - // than graph legend ticks - var tickDecimals = (axis.tickDecimals || -1) + 1; - series.updateLegendValues(formater, tickDecimals, axis.scaledDecimals + 2); - } - - if (!rootScope.$$phase) { scope.$digest(); } - } - // add left axis labels if (panel.yaxes[0].label && panel.yaxes[0].show) { $("
").text(panel.yaxes[0].label).appendTo(elem); diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 1da5aa17f2b..ffb3e390201 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -2,6 +2,7 @@ import angular from 'angular'; import _ from 'lodash'; import $ from 'jquery'; import PerfectScrollbar from 'perfect-scrollbar'; +import {updateLegendValues} from 'app/core/core'; var module = angular.module('grafana.directives'); @@ -26,6 +27,11 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { ctrl.events.emit('legend-rendering-complete'); }); + function updateLegendDecimals() { + let graphHeight = ctrl.height - $container.height(); + updateLegendValues(data, panel, graphHeight); + } + function getSeriesIndexForElement(el) { return el.parents('[data-series-index]').data('series-index'); } @@ -170,6 +176,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { html += '
' + series.aliasEscaped + ''; if (panel.legend.values) { + updateLegendDecimals(); var avg = series.formatValue(series.stats.avg); var current = series.formatValue(series.stats.current); var min = series.formatValue(series.stats.min); From b44c5994100b7a285879c6a2586cd77113cc7207 Mon Sep 17 00:00:00 2001 From: Sven Klemm <31455525+svenklemm@users.noreply.github.com> Date: Thu, 7 Dec 2017 10:05:04 +0100 Subject: [PATCH 71/96] postgres: pass timerange for template variable queries (#10069) * pass timerange for template queries when refresh is set to timerange change * document on time range change refresh mode for template queries --- docs/sources/features/datasources/postgres.md | 6 ++++++ .../app/plugins/datasource/postgres/datasource.ts | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/sources/features/datasources/postgres.md b/docs/sources/features/datasources/postgres.md index 2b111d51f0e..e9d65b8f327 100644 --- a/docs/sources/features/datasources/postgres.md +++ b/docs/sources/features/datasources/postgres.md @@ -139,6 +139,12 @@ A query can return multiple columns and Grafana will automatically create a list SELECT host.hostname, other_host.hostname2 FROM host JOIN other_host ON host.city = other_host.city ``` +To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*. + +```sql +SELECT event_name FROM event_log WHERE $__timeFilter(time_column) +``` + Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value: ```sql diff --git a/public/app/plugins/datasource/postgres/datasource.ts b/public/app/plugins/datasource/postgres/datasource.ts index af3d83f50d8..82354dd4bc6 100644 --- a/public/app/plugins/datasource/postgres/datasource.ts +++ b/public/app/plugins/datasource/postgres/datasource.ts @@ -99,12 +99,21 @@ export class PostgresDatasource { format: 'table', }; + var data = { + queries: [interpolatedQuery], + }; + + if (optionalOptions && optionalOptions.range && optionalOptions.range.from) { + data['from'] = optionalOptions.range.from.valueOf().toString(); + } + if (optionalOptions && optionalOptions.range && 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 58d40eb6a0734233e745339dc98981d1267aa803 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Thu, 7 Dec 2017 12:03:39 +0300 Subject: [PATCH 72/96] graph: fix legend height calculation --- public/app/plugins/panel/graph/legend.ts | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index ffb3e390201..7983910ffb3 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -27,8 +27,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { ctrl.events.emit('legend-rendering-complete'); }); - function updateLegendDecimals() { - let graphHeight = ctrl.height - $container.height(); + function updateLegendDecimals(graphHeight) { updateLegendValues(data, panel, graphHeight); } @@ -155,8 +154,21 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } } - var seriesElements = []; + // render first time for getting proper legend height + if (!panel.legend.rightSide) { + renderLegendElement(tableHeaderElem); + let graphHeight = ctrl.height - $container.height() - 23; + updateLegendDecimals(graphHeight); + $container.empty(); + } else { + updateLegendDecimals(ctrl.height); + } + renderLegendElement(tableHeaderElem); + } + + function renderSeriesLegendElements() { + let seriesElements = []; for (i = 0; i < seriesList.length; i++) { var series = seriesList[i]; @@ -176,7 +188,6 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { html += '' + series.aliasEscaped + ''; if (panel.legend.values) { - updateLegendDecimals(); var avg = series.formatValue(series.stats.avg); var current = series.formatValue(series.stats.current); var min = series.formatValue(series.stats.min); @@ -193,6 +204,11 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { html += '
'; seriesElements.push($(html)); } + return seriesElements; + } + + function renderLegendElement(tableHeaderElem) { + var seriesElements = renderSeriesLegendElements(); if (panel.legend.alignAsTable) { var maxHeight = ctrl.height; From d0a9770b8bf58f1688814a1d10d19a3d799e2f00 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 7 Dec 2017 11:14:43 +0100 Subject: [PATCH 73/96] dashboard: fix edge case with keyboard nav in dashboard search. #10100 If expanding a folder using mouse, unselect current keyboard selection --- public/app/core/components/search/search.html | 4 +- public/app/core/components/search/search.ts | 9 +++++ .../components/search/search_results.jest.ts | 40 ++++++++++++++----- .../core/components/search/search_results.ts | 8 +++- public/app/core/services/search_srv.ts | 5 ++- 5 files changed, 53 insertions(+), 13 deletions(-) diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index 3b0d9c003dd..c458aa51906 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -24,7 +24,9 @@
No dashboards matching your query were found.
+ on-tag-selected="ctrl.filterByTag($tag)" + on-folder-expanding="ctrl.folderExpanding()" + on-folder-expanded="ctrl.folderExpanded($folder)" />
diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 08d050c81bf..ffbec30cc0a 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -102,6 +102,11 @@ export class SearchCtrl { } } + if (direction === 0) { + this.selectedIndex = -1; + return; + } + const max = flattenedResult.length; let newIndex = this.selectedIndex + direction; this.selectedIndex = ((newIndex %= max) < 0) ? newIndex + max : newIndex; @@ -182,6 +187,10 @@ export class SearchCtrl { this.searchDashboards(); } + folderExpanding() { + this.moveSelection(0); + } + private getFlattenedResultForNavigation() { let folderIndex = 0; diff --git a/public/app/core/components/search/search_results.jest.ts b/public/app/core/components/search/search_results.jest.ts index fc661dc8472..514eff21dab 100644 --- a/public/app/core/components/search/search_results.jest.ts +++ b/public/app/core/components/search/search_results.jest.ts @@ -55,21 +55,43 @@ describe('SearchResultsCtrl', () => { }); }); - describe('when toggle a folder', () => { - let folderToggled = false; - let folder = { - toggle: () => { - folderToggled = true; - } - }; + describe('when toggle a collapsed folder', () => { + let folderExpanded = false; beforeEach(() => { ctrl = new SearchResultsCtrl({}); + ctrl.onFolderExpanding = () => { folderExpanded = true; }; + + let folder = { + expanded: false, + toggle: () => {} + }; + ctrl.toggleFolderExpand(folder); }); - it('should trigger folder toggle callback', () => { - expect(folderToggled).toBeTruthy(); + it('should trigger folder expanding callback', () => { + expect(folderExpanded).toBeTruthy(); + }); + }); + + describe('when toggle an expanded folder', () => { + let folderExpanded = false; + + beforeEach(() => { + ctrl = new SearchResultsCtrl({}); + ctrl.onFolderExpanding = () => { folderExpanded = true; }; + + let folder = { + expanded: true, + toggle: () => {} + }; + + ctrl.toggleFolderExpand(folder); + }); + + it('should not trigger folder expanding callback', () => { + expect(folderExpanded).toBeFalsy(); }); }); }); diff --git a/public/app/core/components/search/search_results.ts b/public/app/core/components/search/search_results.ts index 0757a53a517..02b6c9f355b 100644 --- a/public/app/core/components/search/search_results.ts +++ b/public/app/core/components/search/search_results.ts @@ -5,6 +5,7 @@ export class SearchResultsCtrl { results: any; onSelectionChanged: any; onTagSelected: any; + onFolderExpanding: any; /** @ngInject */ constructor(private $location) { @@ -13,6 +14,10 @@ export class SearchResultsCtrl { toggleFolderExpand(section) { if (section.toggle) { + if (!section.expanded && this.onFolderExpanding) { + this.onFolderExpanding(); + } + section.toggle(section); } } @@ -62,7 +67,8 @@ export function searchResultsDirective() { editable: '@', results: '=', onSelectionChanged: '&', - onTagSelected: '&' + onTagSelected: '&', + onFolderExpanding: '&' }, }; } diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 38aeea47b79..52f008e1812 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -51,7 +51,7 @@ export class SearchSrv { store.set('search.sections.recent', this.recentIsOpen); if (!section.expanded || section.items.length) { - return; + return Promise.resolve(); } return this.queryForRecentDashboards().then(result => { @@ -62,6 +62,7 @@ export class SearchSrv { private toggleStarred(section) { this.starredIsOpen = section.expanded = !section.expanded; store.set('search.sections.starred', this.starredIsOpen); + return Promise.resolve(); } private getStarred(sections) { @@ -189,7 +190,7 @@ export class SearchSrv { section.icon = section.expanded ? 'fa fa-folder-open' : 'fa fa-folder'; if (section.items.length) { - return; + return Promise.resolve(); } let query = { From a62ebb3e590cee02297997977b751a5929f8a496 Mon Sep 17 00:00:00 2001 From: Sven Klemm <31455525+svenklemm@users.noreply.github.com> Date: Thu, 7 Dec 2017 11:18:36 +0100 Subject: [PATCH 74/96] mysql: pass timerange for template variable queries (#10071) * mysql: pass timerange for template variable queries * mysql: document time range macro usage in template variables * mysql: docs for on time range change refresh mode for template queries * Revert "mysql: docs for on time range change refresh mode for template queries" This reverts commit 5325972aa4f66257aa20f7ae6d4de8fa8fe628b6. --- docs/sources/features/datasources/mysql.md | 6 ++++++ public/app/plugins/datasource/mysql/datasource.ts | 15 ++++++++++++--- 2 files changed, 18 insertions(+), 3 deletions(-) diff --git a/docs/sources/features/datasources/mysql.md b/docs/sources/features/datasources/mysql.md index 69c6f667062..d9f048e0371 100644 --- a/docs/sources/features/datasources/mysql.md +++ b/docs/sources/features/datasources/mysql.md @@ -127,6 +127,12 @@ A query can returns multiple columns and Grafana will automatically create a lis SELECT my_host.hostname, my_other_host.hostname2 FROM my_host JOIN my_other_host ON my_host.city = my_other_host.city ``` +To use time range dependent macros like `$__timeFilter(column)` in your query the refresh mode of the template variable needs to be set to *On Time Range Change*. + +```sql +SELECT event_name FROM event_log WHERE $__timeFilter(time_column) +``` + Another option is a query that can create a key/value variable. The query should return two columns that are named `__text` and `__value`. The `__text` column value should be unique (if it is not unique then the first value is used). The options in the dropdown will have a text and value that allows you to have a friendly name as text and an id as the value. An example query with `hostname` as the text and `id` as the value: ```sql diff --git a/public/app/plugins/datasource/mysql/datasource.ts b/public/app/plugins/datasource/mysql/datasource.ts index ac5ccfeb5ca..5a8d3b236c0 100644 --- a/public/app/plugins/datasource/mysql/datasource.ts +++ b/public/app/plugins/datasource/mysql/datasource.ts @@ -103,12 +103,21 @@ export class MysqlDatasource { format: 'table', }; + var data = { + queries: [interpolatedQuery], + }; + + if (optionalOptions && optionalOptions.range && optionalOptions.range.from) { + data['from'] = optionalOptions.range.from.valueOf().toString(); + } + if (optionalOptions && optionalOptions.range && 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 548652aab1df7f7c92d15df2a01ab16c12d8b02a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 7 Dec 2017 13:33:46 +0100 Subject: [PATCH 75/96] ux: minor cleanup --- public/app/features/org/partials/profile.html | 5 ----- public/app/features/org/partials/select_org.html | 9 --------- 2 files changed, 14 deletions(-) diff --git a/public/app/features/org/partials/profile.html b/public/app/features/org/partials/profile.html index 02b0d27b84f..66e41fbb4b4 100644 --- a/public/app/features/org/partials/profile.html +++ b/public/app/features/org/partials/profile.html @@ -26,11 +26,6 @@ -

Password

- -

Organizations

diff --git a/public/app/features/org/partials/select_org.html b/public/app/features/org/partials/select_org.html index 2e7a992b4fa..116bca66286 100644 --- a/public/app/features/org/partials/select_org.html +++ b/public/app/features/org/partials/select_org.html @@ -35,15 +35,6 @@
- -
- - -
-
From 1542e8c1b26d8be430d12d78918e42a167986419 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Thu, 7 Dec 2017 14:07:16 +0100 Subject: [PATCH 76/96] removed row to center footer (#10115) * removed row to center footer * code formatting fix --- public/views/index.template.html | 183 ++++++++++++++++--------------- 1 file changed, 94 insertions(+), 89 deletions(-) diff --git a/public/views/index.template.html b/public/views/index.template.html index e022cc4873b..461bc0aa804 100644 --- a/public/views/index.template.html +++ b/public/views/index.template.html @@ -1,109 +1,114 @@ - - - - - - Grafana + + + + + - + Grafana - + - - + - + + - - + - + + -
-
-
-
-
{{alert.title}}
-
-
- -
-
+ -
-
-
- - -
+
+
+
+ +
+
+
{{alert.title}}
+
+
+
- +
- + [[if .GoogleTagManagerId]] + + + + + + [[end]] - [[if .GoogleTagManagerId]] - - - - - - [[end]] + - From 68dab71275cd611e4e584d7369a7ff03c7e9bfee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 7 Dec 2017 16:25:21 +0100 Subject: [PATCH 77/96] refactor: sidemenu toggle & hiding logic --- public/app/core/components/grafana_app.ts | 22 ++--- .../core/components/sidemenu/sidemenu.html | 5 +- .../app/core/components/sidemenu/sidemenu.ts | 18 +--- public/app/core/controllers/error_ctrl.ts | 12 ++- public/app/core/nav_model_srv.ts | 6 +- public/app/core/routes/routes.ts | 6 +- public/app/core/services/context_srv.ts | 4 - .../app/features/dashboard/dashnav/dashnav.ts | 7 -- public/app/partials/error.html | 97 ++++++++----------- public/app/partials/reset_password.html | 15 +-- public/sass/components/_sidemenu.scss | 18 +++- public/sass/pages/_login.scss | 6 -- 12 files changed, 99 insertions(+), 117 deletions(-) diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 77089b14ddc..39601f903ec 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -79,21 +79,16 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop sidemenuOpen = scope.contextSrv.sidemenu; body.toggleClass('sidemenu-open', sidemenuOpen); - scope.$watch('contextSrv.sidemenu', newVal => { - if (sidemenuOpen !== scope.contextSrv.sidemenu) { - sidemenuOpen = scope.contextSrv.sidemenu; - body.toggleClass('sidemenu-open', scope.contextSrv.sidemenu); - } + appEvents.on('toggle-sidemenu', () => { + body.toggleClass('sidemenu-open'); }); - let sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; - body.toggleClass('sidemenu-open--xs', sidemenuOpenSmallBreakpoint); + appEvents.on('toggle-sidemenu-mobile', () => { + body.toggleClass('sidemenu-open--xs'); + }); - scope.$watch('contextSrv.sidemenuSmallBreakpoint', newVal => { - if (sidemenuOpenSmallBreakpoint !== scope.contextSrv.sidemenuSmallBreakpoint) { - sidemenuOpenSmallBreakpoint = scope.contextSrv.sidemenuSmallBreakpoint; - body.toggleClass('sidemenu-open--xs', scope.contextSrv.sidemenuSmallBreakpoint); - } + appEvents.on('toggle-sidemenu-hidden', () => { + body.toggleClass('sidemenu-hidden'); }); // tooltip removal fix @@ -111,6 +106,9 @@ export function grafanaAppDirective(playlistSrv, contextSrv, $timeout, $rootScop } } + // clear body class sidemenu states + body.removeClass('sidemenu-open--xs'); + $("#tooltip, .tooltip").remove(); // check for kiosk url param diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index b47b7fc8265..7ed2ab1f2c7 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -3,11 +3,10 @@ - -

 Close

+ +  Close
-
diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 605b15dd5bc..05882d75fc9 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -16,6 +16,14 @@ .sidemenu__close { display: none; } + +} + +// body class that hides sidemenu +.sidemenu-hidden { + .sidemenu { + display: none; + } } @include media-breakpoint-up(sm) { @@ -164,7 +172,7 @@ li.sidemenu-org-switcher { } } -.sidemenu__logo, .sidemenu__logo_small_breakpoint { +.sidemenu__logo { display: block; padding: 0.4rem 1.0rem 0.4rem 0.65rem; min-height: $navbarHeight; @@ -203,6 +211,8 @@ li.sidemenu-org-switcher { .sidemenu__close { display: block; font-size: $font-size-md; + position: relative; + top: -3px; } .sidemenu__top, @@ -215,14 +225,16 @@ li.sidemenu-org-switcher { .sidemenu__logo { display: none; } + .sidemenu__logo_small_breakpoint { + padding: 12px 10px 26px; display: flex; flex-direction: row; justify-content: space-between; align-items: baseline; - &:hover { - background: transparent; + .fa-bars { + font-size: 25px; } } diff --git a/public/sass/pages/_login.scss b/public/sass/pages/_login.scss index 3c54224c4cc..2b3426b45b5 100644 --- a/public/sass/pages/_login.scss +++ b/public/sass/pages/_login.scss @@ -8,12 +8,6 @@ padding-top: $spacer * 5; /* BAD HACK - experiement to see how it looks */ } -.page-login { - .sidemenu { - display: none; - } -} - .login-form { display: inline-block; max-width: 24rem; From 87ceeb08988bdaa46e7fe57d0188ffa5b437017b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 7 Dec 2017 16:58:57 +0100 Subject: [PATCH 78/96] fix: v5 sidemenu & link to shortcuts now works, fixes #10087 --- public/app/core/components/sidemenu/sidemenu.html | 2 +- public/app/core/components/sidemenu/sidemenu.ts | 7 +++++++ public/app/core/services/keybindingSrv.ts | 2 -- public/sass/components/_shortcuts.scss | 1 - 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/public/app/core/components/sidemenu/sidemenu.html b/public/app/core/components/sidemenu/sidemenu.html index 7ed2ab1f2c7..e97d34739ba 100644 --- a/public/app/core/components/sidemenu/sidemenu.html +++ b/public/app/core/components/sidemenu/sidemenu.html @@ -59,7 +59,7 @@
  • - + {{::child.text}} diff --git a/public/app/core/components/sidemenu/sidemenu.ts b/public/app/core/components/sidemenu/sidemenu.ts index 489cad72957..79d1a0d7e99 100644 --- a/public/app/core/components/sidemenu/sidemenu.ts +++ b/public/app/core/components/sidemenu/sidemenu.ts @@ -50,6 +50,13 @@ export class SideMenuCtrl { templateHtml: '', }); } + + itemClicked(item, evt) { + if (item.url === '/shortcuts') { + appEvents.emit('show-modal', {templateHtml: ''}); + evt.preventDefault(); + } + } } export function sideMenuDirective() { diff --git a/public/app/core/services/keybindingSrv.ts b/public/app/core/services/keybindingSrv.ts index a8f36afc468..062eea3506a 100644 --- a/public/app/core/services/keybindingSrv.ts +++ b/public/app/core/services/keybindingSrv.ts @@ -1,5 +1,3 @@ -/// - import $ from 'jquery'; import _ from 'lodash'; diff --git a/public/sass/components/_shortcuts.scss b/public/sass/components/_shortcuts.scss index 1dedb062183..bfcfbf900fe 100644 --- a/public/sass/components/_shortcuts.scss +++ b/public/sass/components/_shortcuts.scss @@ -11,7 +11,6 @@ .shortcut-table-category-header { font-weight: normal; font-size: $font-size-h6; - font-style: italic; text-align: left; } From 10b0fc79f313b44009e0812ab4889f8f08489ddc Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 7 Dec 2017 13:16:35 +0100 Subject: [PATCH 79/96] fix: move components tests to specs folder --- public/app/core/{components/search => specs}/search.jest.ts | 4 ++-- .../core/{components/search => specs}/search_results.jest.ts | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) rename public/app/core/{components/search => specs}/search.jest.ts (98%) rename public/app/core/{components/search => specs}/search_results.jest.ts (97%) diff --git a/public/app/core/components/search/search.jest.ts b/public/app/core/specs/search.jest.ts similarity index 98% rename from public/app/core/components/search/search.jest.ts rename to public/app/core/specs/search.jest.ts index 68b1292f9d4..d3058e7550e 100644 --- a/public/app/core/components/search/search.jest.ts +++ b/public/app/core/specs/search.jest.ts @@ -1,5 +1,5 @@ -import { SearchCtrl } from './search'; -import { SearchSrv } from 'app/core/services/search_srv'; +import { SearchCtrl } from '../components/search/search'; +import { SearchSrv } from '../services/search_srv'; describe('SearchCtrl', () => { const searchSrvStub = { diff --git a/public/app/core/components/search/search_results.jest.ts b/public/app/core/specs/search_results.jest.ts similarity index 97% rename from public/app/core/components/search/search_results.jest.ts rename to public/app/core/specs/search_results.jest.ts index 514eff21dab..b8eed3cef9b 100644 --- a/public/app/core/components/search/search_results.jest.ts +++ b/public/app/core/specs/search_results.jest.ts @@ -1,4 +1,4 @@ -import { SearchResultsCtrl } from './search_results'; +import { SearchResultsCtrl } from '../components/search/search_results'; describe('SearchResultsCtrl', () => { let ctrl; From 2ea663df7887ff56f5f4bdc49fc7830eac36ffd7 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 7 Dec 2017 17:29:13 +0100 Subject: [PATCH 80/96] dashfolders: New Dashboard Folder page Fixes #10083. Permissions page is just a placeholder for now. --- pkg/api/index.go | 15 ++ .../core/components/PageHeader/PageHeader.tsx | 20 +- .../manage_dashboards/manage_dashboards.html | 98 ++++++++ .../manage_dashboards/manage_dashboards.ts | 221 ++++++++++++++++++ public/app/core/components/search/search.html | 12 +- .../components/search/search_results.html | 2 +- public/app/core/core.ts | 4 +- public/app/core/routes/routes.ts | 11 +- .../specs/manage_dashboards.jest.ts} | 6 +- public/app/features/dashboard/all.ts | 4 + .../features/dashboard/dashboard_list_ctrl.ts | 207 +--------------- .../dashboard/folder_dashboards_ctrl.ts | 16 ++ .../features/dashboard/folder_page_loader.ts | 21 ++ .../dashboard/folder_permissions_ctrl.ts | 16 ++ .../dashboard/partials/dashboardList.html | 121 ---------- .../dashboard/partials/dashboard_list.html | 5 + .../dashboard/partials/folder_dashboards.html | 5 + .../partials/folder_permissions.html | 5 + 18 files changed, 447 insertions(+), 342 deletions(-) create mode 100644 public/app/core/components/manage_dashboards/manage_dashboards.html create mode 100644 public/app/core/components/manage_dashboards/manage_dashboards.ts rename public/app/{features/dashboard/specs/dashboard_list_ctrl.jest.ts => core/specs/manage_dashboards.jest.ts} (98%) create mode 100644 public/app/features/dashboard/folder_dashboards_ctrl.ts create mode 100644 public/app/features/dashboard/folder_page_loader.ts create mode 100644 public/app/features/dashboard/folder_permissions_ctrl.ts delete mode 100644 public/app/features/dashboard/partials/dashboardList.html create mode 100644 public/app/features/dashboard/partials/dashboard_list.html create mode 100644 public/app/features/dashboard/partials/folder_dashboards.html create mode 100644 public/app/features/dashboard/partials/folder_permissions.html diff --git a/pkg/api/index.go b/pkg/api/index.go index f3e7af0e4b9..1e44ef33050 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -118,6 +118,21 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Children: dashboardChildNavs, }) + dashboardFolderChildNavs := []*dtos.NavLink{ + {Text: "Dashboards", Id: "manage-folder-dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-th-large"}, + {Text: "Permissions", Id: "manage-folder-permissions", Url: setting.AppSubUrl + "/dashboards?1", Icon: "fa fa-fw fa-lock"}, + } + + data.NavTree = append(data.NavTree, &dtos.NavLink{ + Text: "Dashboards", + Id: "manage-folder", + SubTitle: "Manage folder dashboards & permissions", + Icon: "fa fa-folder-open", + Url: setting.AppSubUrl + "/", + HideFromMenu: true, + Children: dashboardFolderChildNavs, + }) + if c.IsSignedIn { profileNode := &dtos.NavLink{ Text: c.SignedInUser.Name, diff --git a/public/app/core/components/PageHeader/PageHeader.tsx b/public/app/core/components/PageHeader/PageHeader.tsx index 5371b464ec3..3ab6cac8143 100644 --- a/public/app/core/components/PageHeader/PageHeader.tsx +++ b/public/app/core/components/PageHeader/PageHeader.tsx @@ -72,6 +72,19 @@ export default class PageHeader extends React.Component { super(props); } + renderBreadcrumb(breadcrumbs) { + const breadcrumbsResult = []; + for (let i = 0; i < breadcrumbs.length; i++) { + const bc = breadcrumbs[i]; + if (bc.uri) { + breadcrumbsResult.push({bc.title}); + } else { + breadcrumbsResult.push( / {bc.title}); + } + } + return breadcrumbsResult; + } + renderHeaderTitle(main) { return (
    @@ -81,7 +94,12 @@ export default class PageHeader extends React.Component {
    -

    {main.text}

    + {main.text &&

    {main.text}

    } + {main.breadcrumbs && main.breadcrumbs.length > 0 && ( +

    + {this.renderBreadcrumb(main.breadcrumbs)} +

    ) + } {main.subTitle &&
    {main.subTitle}
    } {main.subType && (
    diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html new file mode 100644 index 00000000000..8c5aeb0facd --- /dev/null +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -0,0 +1,98 @@ +
    +
    + + +
    +
    + + + Dashboard + + + + Folder + +
    + +
    + Filters: + + + + {{tagName}} + + +
    + +
    +
    + +
    +
    + +
    +
    + +
    + +
    + + +
    +
    +
    +
    + +
    +
    + +
    + +
    \ No newline at end of file diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.ts b/public/app/core/components/manage_dashboards/manage_dashboards.ts new file mode 100644 index 00000000000..5b74a0e50fc --- /dev/null +++ b/public/app/core/components/manage_dashboards/manage_dashboards.ts @@ -0,0 +1,221 @@ +import _ from 'lodash'; +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import { SearchSrv } from 'app/core/services/search_srv'; + +export class ManageDashboardsCtrl { + public sections: any[]; + tagFilterOptions: any[]; + selectedTagFilter: any; + query: any; + navModel: any; + canDelete = false; + canMove = false; + hasFilters = false; + selectAllChecked = false; + starredFilterOptions = [{ text: 'Filter by Starred', disabled: true }, { text: 'Yes' }, { text: 'No' }]; + selectedStarredFilter: any; + folderId?: number; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) { + this.query = { query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true }; + + if (this.folderId) { + this.query.folderIds = [this.folderId]; + } + + this.selectedStarredFilter = this.starredFilterOptions[0]; + + this.getDashboards().then(() => { + this.getTags(); + }); + } + + getDashboards() { + return this.searchSrv.search(this.query).then((result) => { + return this.initDashboardList(result); + }); + } + + initDashboardList(result: any) { + this.canMove = false; + this.canDelete = false; + this.selectAllChecked = false; + this.hasFilters = this.query.query.length > 0 || this.query.tag.length > 0 || this.query.starred; + + if (!result) { + this.sections = []; + return; + } + + this.sections = result; + + for (let section of this.sections) { + section.checked = false; + + for (let dashboard of section.items) { + dashboard.checked = false; + } + } + } + + selectionChanged() { + + let selectedDashboards = 0; + + for (let section of this.sections) { + selectedDashboards += _.filter(section.items, { checked: true }).length; + } + + const selectedFolders = _.filter(this.sections, { checked: true }).length; + this.canMove = selectedDashboards > 0 && selectedFolders === 0; + this.canDelete = selectedDashboards > 0 || selectedFolders > 0; + } + + getDashboardsToDelete() { + let selectedDashboards = []; + + for (const section of this.sections) { + if (section.checked) { + selectedDashboards.push(section.uri); + } else { + const selected = _.filter(section.items, { checked: true }); + selectedDashboards.push(..._.map(selected, 'uri')); + } + } + + return selectedDashboards; + } + + getFolderIds(sections) { + const ids = []; + for (let s of sections) { + if (s.checked) { + ids.push(s.id); + } + } + return ids; + } + + delete() { + const selectedDashboards = this.getDashboardsToDelete(); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete the ${selectedDashboards.length} selected dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + const promises = []; + for (let dash of selectedDashboards) { + promises.push(this.backendSrv.delete(`/api/dashboards/${dash}`)); + } + + this.$q.all(promises).then(() => { + this.getDashboards(); + }); + } + }); + } + + getDashboardsToMove() { + let selectedDashboards = []; + + for (const section of this.sections) { + const selected = _.filter(section.items, { checked: true }); + selectedDashboards.push(..._.map(selected, 'uri')); + } + + return selectedDashboards; + } + + moveTo() { + const selectedDashboards = this.getDashboardsToMove(); + + const template = '' + + '`'; + appEvents.emit('show-modal', { + templateHtml: template, + modalClass: 'modal--narrow', + model: { dashboards: selectedDashboards, afterSave: this.getDashboards.bind(this) } + }); + } + + getTags() { + return this.searchSrv.getDashboardTags().then((results) => { + this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); + this.selectedTagFilter = this.tagFilterOptions[0]; + }); + } + + filterByTag(tag) { + if (_.indexOf(this.query.tag, tag) === -1) { + this.query.tag.push(tag); + } + + return this.getDashboards(); + } + + onQueryChange() { + return this.getDashboards(); + } + + onTagFilterChange() { + var res = this.filterByTag(this.selectedTagFilter.term); + this.selectedTagFilter = this.tagFilterOptions[0]; + return res; + } + + removeTag(tag, evt) { + this.query.tag = _.without(this.query.tag, tag); + this.getDashboards(); + if (evt) { + evt.stopPropagation(); + evt.preventDefault(); + } + } + + onStarredFilterChange() { + this.query.starred = this.selectedStarredFilter.text === 'Yes'; + return this.getDashboards(); + } + + onSelectAllChanged() { + for (let section of this.sections) { + if (!section.hideHeader) { + section.checked = this.selectAllChecked; + } + + section.items = _.map(section.items, (item) => { + item.checked = this.selectAllChecked; + return item; + }); + } + + this.selectionChanged(); + } + + clearFilters() { + this.query.query = ''; + this.query.tag = []; + this.query.starred = false; + this.getDashboards(); + } +} + +export function manageDashboardsDirective() { + return { + restrict: 'E', + templateUrl: 'public/app/core/components/manage_dashboards/manage_dashboards.html', + controller: ManageDashboardsCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + folderId: '=' + } + }; +} + +coreModule.directive('manageDashboards', manageDashboardsDirective); diff --git a/public/app/core/components/search/search.html b/public/app/core/components/search/search.html index c458aa51906..f9b953efcee 100644 --- a/public/app/core/components/search/search.html +++ b/public/app/core/components/search/search.html @@ -21,12 +21,12 @@
    -
    No dashboards matching your query were found.
    - +
    No dashboards matching your query were found.
    +
    diff --git a/public/app/core/components/search/search_results.html b/public/app/core/components/search/search_results.html index 2008910f53a..ad0aa3e6b71 100644 --- a/public/app/core/components/search/search_results.html +++ b/public/app/core/components/search/search_results.html @@ -10,7 +10,7 @@
    {{::section.title}} -
    +
     
    diff --git a/public/app/core/core.ts b/public/app/core/core.ts index a1a93c3cdcc..b32651f418a 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -53,6 +53,7 @@ import {orgSwitcher} from './components/org_switcher'; import {profiler} from './profiler'; import {registerAngularDirectives} from './angular_wrappers'; import {searchResultsDirective} from './components/search/search_results'; +import {manageDashboardsDirective} from './components/manage_dashboards/manage_dashboards'; export { profiler, @@ -85,5 +86,6 @@ export { geminiScrollbar, gfPageDirective, orgSwitcher, - searchResultsDirective + searchResultsDirective, + manageDashboardsDirective }; diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 87437770e0e..8b42545d334 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -69,13 +69,18 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controllerAs: 'ctrl', }) .when('/dashboards', { - templateUrl: 'public/app/features/dashboard/partials/dashboardList.html', + templateUrl: 'public/app/features/dashboard/partials/dashboard_list.html', controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/folder/:folderId/:type/:slug/permissions', { + templateUrl: 'public/app/features/dashboard/partials/folder_permissions.html', + controller : 'FolderPermissionsCtrl', + controllerAs: 'ctrl', + }) .when('/dashboards/folder/:folderId/:type/:slug', { - templateUrl: 'public/app/features/dashboard/partials/dashboardList.html', - controller : 'DashboardListCtrl', + templateUrl: 'public/app/features/dashboard/partials/folder_dashboards.html', + controller : 'FolderDashboardsCtrl', controllerAs: 'ctrl', }) .when('/org', { diff --git a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts b/public/app/core/specs/manage_dashboards.jest.ts similarity index 98% rename from public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts rename to public/app/core/specs/manage_dashboards.jest.ts index 1c13d138b6d..770679f09bf 100644 --- a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts +++ b/public/app/core/specs/manage_dashboards.jest.ts @@ -1,8 +1,8 @@ -import { DashboardListCtrl } from '../dashboard_list_ctrl'; +import { ManageDashboardsCtrl } from 'app/core/components/manage_dashboards/manage_dashboards'; import { SearchSrv } from 'app/core/services/search_srv'; import q from 'q'; -describe('DashboardListCtrl', () => { +describe('ManageDashboards', () => { let ctrl; describe('when browsing dashboards', () => { @@ -542,5 +542,5 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) { } }; - return new DashboardListCtrl({}, { getNav: () => { } }, q, searchSrvStub, {}); + return new ManageDashboardsCtrl({}, { getNav: () => { } }, q, searchSrvStub); } diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index f2f2087a24e..a18af82215d 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -29,7 +29,11 @@ import './move_to_folder_modal/move_to_folder'; import coreModule from 'app/core/core_module'; import {DashboardListCtrl} from './dashboard_list_ctrl'; +import {FolderDashboardsCtrl} from './folder_dashboards_ctrl'; +import {FolderPermissionsCtrl} from './folder_permissions_ctrl'; import {DashboardImportCtrl} from './dashboard_import_ctrl'; coreModule.controller('DashboardListCtrl', DashboardListCtrl); +coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); +coreModule.controller('FolderPermissionsCtrl', FolderPermissionsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); diff --git a/public/app/features/dashboard/dashboard_list_ctrl.ts b/public/app/features/dashboard/dashboard_list_ctrl.ts index e2b3ee3227d..b5f5cb8e0dc 100644 --- a/public/app/features/dashboard/dashboard_list_ctrl.ts +++ b/public/app/features/dashboard/dashboard_list_ctrl.ts @@ -1,213 +1,8 @@ -import _ from 'lodash'; -import appEvents from 'app/core/app_events'; -import { SearchSrv } from 'app/core/services/search_srv'; - export class DashboardListCtrl { - public sections: any []; - tagFilterOptions: any []; - selectedTagFilter: any; - query: any; navModel: any; - canDelete = false; - canMove = false; - hasFilters = false; - selectAllChecked = false; - starredFilterOptions = [{text: 'Filter by Starred', disabled: true}, {text: 'Yes'}, {text: 'No'}]; - selectedStarredFilter: any; - folderTitle = null; /** @ngInject */ - constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv, private $routeParams) { + constructor(navModelSrv) { this.navModel = navModelSrv.getNav('dashboards', 'manage-dashboards', 0); - this.query = {query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true}; - - this.selectedStarredFilter = this.starredFilterOptions[0]; - - if (this.$routeParams.folderId && this.$routeParams.type && this.$routeParams.slug) { - backendSrv.getDashboard(this.$routeParams.type, this.$routeParams.slug).then(result => { - this.folderTitle = result.dashboard.title; - this.query.folderIds = [result.dashboard.id]; - - this.getDashboards().then(() => { - this.getTags(); - }); - }); - } else { - this.getDashboards().then(() => { - this.getTags(); - }); - } - } - - getDashboards() { - return this.searchSrv.search(this.query).then((result) => { - return this.initDashboardList(result); - }); - } - - initDashboardList(result: any) { - this.canMove = false; - this.canDelete = false; - this.selectAllChecked = false; - this.hasFilters = this.query.query.length > 0 || this.query.tag.length > 0 || this.query.starred; - - if (!result) { - this.sections = []; - return; - } - - this.sections = result; - - for (let section of this.sections) { - section.checked = false; - - for (let dashboard of section.items) { - dashboard.checked = false; - } - } - } - - selectionChanged() { - - let selectedDashboards = 0; - - for (let section of this.sections) { - selectedDashboards += _.filter(section.items, {checked: true}).length; - } - - const selectedFolders = _.filter(this.sections, {checked: true}).length; - this.canMove = selectedDashboards > 0 && selectedFolders === 0; - this.canDelete = selectedDashboards > 0 || selectedFolders > 0; - } - - getDashboardsToDelete() { - let selectedDashboards = []; - - for (const section of this.sections) { - if (section.checked) { - selectedDashboards.push(section.uri); - } else { - const selected = _.filter(section.items, {checked: true}); - selectedDashboards.push(... _.map(selected, 'uri')); - } - } - - return selectedDashboards; - } - - getFolderIds(sections) { - const ids = []; - for (let s of sections) { - if (s.checked) { - ids.push(s.id); - } - } - return ids; - } - - delete() { - const selectedDashboards = this.getDashboardsToDelete(); - - appEvents.emit('confirm-modal', { - title: 'Delete', - text: `Do you want to delete the ${selectedDashboards.length} selected dashboards?`, - icon: 'fa-trash', - yesText: 'Delete', - onConfirm: () => { - const promises = []; - for (let dash of selectedDashboards) { - promises.push(this.backendSrv.delete(`/api/dashboards/${dash}`)); - } - - this.$q.all(promises).then(() => { - this.getDashboards(); - }); - } - }); - } - - getDashboardsToMove() { - let selectedDashboards = []; - - for (const section of this.sections) { - const selected = _.filter(section.items, {checked: true}); - selectedDashboards.push(... _.map(selected, 'uri')); - } - - return selectedDashboards; - } - - moveTo() { - const selectedDashboards = this.getDashboardsToMove(); - - const template = '' + - '`'; - appEvents.emit('show-modal', { - templateHtml: template, - modalClass: 'modal--narrow', - model: {dashboards: selectedDashboards, afterSave: this.getDashboards.bind(this)} - }); - } - - getTags() { - return this.searchSrv.getDashboardTags().then((results) => { - this.tagFilterOptions = [{ term: 'Filter By Tag', disabled: true }].concat(results); - this.selectedTagFilter = this.tagFilterOptions[0]; - }); - } - - filterByTag(tag) { - if (_.indexOf(this.query.tag, tag) === -1) { - this.query.tag.push(tag); - } - - return this.getDashboards(); - } - - onQueryChange() { - return this.getDashboards(); - } - - onTagFilterChange() { - var res = this.filterByTag(this.selectedTagFilter.term); - this.selectedTagFilter = this.tagFilterOptions[0]; - return res; - } - - removeTag(tag, evt) { - this.query.tag = _.without(this.query.tag, tag); - this.getDashboards(); - if (evt) { - evt.stopPropagation(); - evt.preventDefault(); - } - } - - onStarredFilterChange() { - this.query.starred = this.selectedStarredFilter.text === 'Yes'; - return this.getDashboards(); - } - - onSelectAllChanged() { - for (let section of this.sections) { - if (!section.hideHeader) { - section.checked = this.selectAllChecked; - } - - section.items = _.map(section.items, (item) => { - item.checked = this.selectAllChecked; - return item; - }); - } - - this.selectionChanged(); - } - - clearFilters() { - this.query.query = ''; - this.query.tag = []; - this.query.starred = false; - this.getDashboards(); } } diff --git a/public/app/features/dashboard/folder_dashboards_ctrl.ts b/public/app/features/dashboard/folder_dashboards_ctrl.ts new file mode 100644 index 00000000000..0eca6449584 --- /dev/null +++ b/public/app/features/dashboard/folder_dashboards_ctrl.ts @@ -0,0 +1,16 @@ +import {FolderPageLoader} from './folder_page_loader'; + +export class FolderDashboardsCtrl { + navModel: any; + folderId: number; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $routeParams) { + if (this.$routeParams.folderId && this.$routeParams.type && this.$routeParams.slug) { + this.folderId = $routeParams.folderId; + this.navModel = navModelSrv.getNav('manage-folder', 'manage-folder-dashboards', 0); + + new FolderPageLoader(this.backendSrv, this.$routeParams).load(this.navModel, this.folderId); + } + } +} diff --git a/public/app/features/dashboard/folder_page_loader.ts b/public/app/features/dashboard/folder_page_loader.ts new file mode 100644 index 00000000000..cc2d8010270 --- /dev/null +++ b/public/app/features/dashboard/folder_page_loader.ts @@ -0,0 +1,21 @@ +import _ from "lodash"; + +export class FolderPageLoader { + constructor(private backendSrv, private $routeParams) { } + + load(navModel, folderId) { + this.backendSrv.getDashboard(this.$routeParams.type, this.$routeParams.slug).then(result => { + const folderTitle = result.dashboard.title; + navModel.main.text = ''; + navModel.main.breadcrumbs = [ + { title: 'Dashboards', uri: '/dashboards' }, + { title: folderTitle } + ]; + const folderUrl = `/dashboards/folder/${folderId}/${result.meta.type}/${result.meta.slug}`; + const dashTab = _.find(navModel.main.children, { id: 'manage-folder-dashboards' }); + dashTab.url = folderUrl; + const permTab = _.find(navModel.main.children, { id: 'manage-folder-permissions' }); + permTab.url = folderUrl + '/permissions'; + }); + } +} diff --git a/public/app/features/dashboard/folder_permissions_ctrl.ts b/public/app/features/dashboard/folder_permissions_ctrl.ts new file mode 100644 index 00000000000..a7d46088ac5 --- /dev/null +++ b/public/app/features/dashboard/folder_permissions_ctrl.ts @@ -0,0 +1,16 @@ +import {FolderPageLoader} from './folder_page_loader'; + +export class FolderPermissionsCtrl { + navModel: any; + folderId: number; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $routeParams) { + if (this.$routeParams.folderId && this.$routeParams.type && this.$routeParams.slug) { + this.folderId = $routeParams.folderId; + this.navModel = navModelSrv.getNav('manage-folder', 'manage-folder-permissions', 0); + + new FolderPageLoader(this.backendSrv, this.$routeParams).load(this.navModel, this.folderId); + } + } +} diff --git a/public/app/features/dashboard/partials/dashboardList.html b/public/app/features/dashboard/partials/dashboardList.html deleted file mode 100644 index cecd52aa891..00000000000 --- a/public/app/features/dashboard/partials/dashboardList.html +++ /dev/null @@ -1,121 +0,0 @@ - - -
    -
    -
    -

    -  {{ctrl.folderTitle}} -

    -
    -
    - - - - Dashboard - - - - Folder - -
    - -
    -
    - - -
    -
    - - - Dashboard - - - - Folder - -
    - -
    - Filters: - - - - {{tagName}} - - -
    - -
    -
    - -
    -
    - -
    - -
    - -
    -
    - -
    - -
    - - -
    -
    -
    -
    - -
    -
    -
    diff --git a/public/app/features/dashboard/partials/dashboard_list.html b/public/app/features/dashboard/partials/dashboard_list.html new file mode 100644 index 00000000000..241d464434a --- /dev/null +++ b/public/app/features/dashboard/partials/dashboard_list.html @@ -0,0 +1,5 @@ + + +
    + +
    diff --git a/public/app/features/dashboard/partials/folder_dashboards.html b/public/app/features/dashboard/partials/folder_dashboards.html new file mode 100644 index 00000000000..eeda28ad4a4 --- /dev/null +++ b/public/app/features/dashboard/partials/folder_dashboards.html @@ -0,0 +1,5 @@ + + +
    + +
    \ No newline at end of file diff --git a/public/app/features/dashboard/partials/folder_permissions.html b/public/app/features/dashboard/partials/folder_permissions.html new file mode 100644 index 00000000000..ad34618457d --- /dev/null +++ b/public/app/features/dashboard/partials/folder_permissions.html @@ -0,0 +1,5 @@ + + +
    +

    Coming soon! Permissions will be added in Grafana 5.0 beta.

    +
    \ No newline at end of file From ff535a5e9a3b75cfb2093fbf94bdffe36444319f Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 7 Dec 2017 18:05:47 +0100 Subject: [PATCH 81/96] dashfolders: css fix --- public/sass/components/_search.scss | 1 + 1 file changed, 1 insertion(+) diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index caf6084fb9e..25cb9a735a6 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -132,6 +132,7 @@ .search-section__header__icon { padding: 2px 10px; + width: 35px; } .search-section__header__toggle { From c629a58b6d21d811ade290640198f5d96e3713e9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 7 Dec 2017 18:13:56 +0100 Subject: [PATCH 82/96] refactor: removed graph height from legend decimal calc --- public/app/core/time_series2.ts | 5 ++--- public/app/core/utils/ticks.ts | 20 ++------------------ public/app/plugins/panel/graph/legend.ts | 14 +++++++++----- 3 files changed, 13 insertions(+), 26 deletions(-) diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index bf8a38761d3..c1caf82b348 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -21,14 +21,13 @@ function translateFillOption(fill) { * Calculate decimals for legend and update values for each series. * @param data series data * @param panel - * @param height Graph height */ -export function updateLegendValues(data: TimeSeries[], panel, height) { +export function updateLegendValues(data: TimeSeries[], panel) { for (let i = 0; i < data.length; i++) { let series = data[i]; let yaxes = panel.yaxes; let axis = yaxes[series.yaxis - 1]; - let {tickDecimals, scaledDecimals} = getFlotTickDecimals(data, axis, height); + let {tickDecimals, scaledDecimals} = getFlotTickDecimals(data, axis); let formater = kbn.valueFormats[panel.yaxes[series.yaxis - 1].format]; // decimal override diff --git a/public/app/core/utils/ticks.ts b/public/app/core/utils/ticks.ts index 334090c056f..8b16b528bdd 100644 --- a/public/app/core/utils/ticks.ts +++ b/public/app/core/utils/ticks.ts @@ -117,30 +117,14 @@ export function getFlotRange(panelMin, panelMax, datamin, datamax) { return {min, max}; } -/** - * Estimate number of ticks for Y axis. - * Implementation from Flot. - */ -export function getFlotNumberOfTicks(height, ticks?) { - let noTicks; - if (typeof ticks === "number" && ticks > 0) { - noTicks = ticks; - } else { - // heuristic based on the model a*sqrt(x) fitted to - // some data points that seemed reasonable - noTicks = 0.3 * Math.sqrt(height); - } - return noTicks; -} - /** * Calculate tick decimals. * Implementation from Flot. */ -export function getFlotTickDecimals(data, axis, height) { +export function getFlotTickDecimals(data, axis) { let {datamin, datamax} = getDataMinMax(data); let {min, max} = getFlotRange(axis.min, axis.max, datamin, datamax); - let noTicks = getFlotNumberOfTicks(height); + let noTicks = 3; let tickDecimals, maxDec; let delta = (max - min) / noTicks; let dec = -Math.floor(Math.log(delta) / Math.LN10); diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 7983910ffb3..854adef48fb 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -19,6 +19,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var i; var legendScrollbar; + scope.$on("$destroy", function() { + legendScrollbar.destroy(); + }); + ctrl.events.on('render-legend', () => { data = ctrl.seriesList; if (data) { @@ -27,8 +31,8 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { ctrl.events.emit('legend-rendering-complete'); }); - function updateLegendDecimals(graphHeight) { - updateLegendValues(data, panel, graphHeight); + function updateLegendDecimals() { + updateLegendValues(data, panel); } function getSeriesIndexForElement(el) { @@ -157,11 +161,10 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { // render first time for getting proper legend height if (!panel.legend.rightSide) { renderLegendElement(tableHeaderElem); - let graphHeight = ctrl.height - $container.height() - 23; - updateLegendDecimals(graphHeight); + updateLegendDecimals(); $container.empty(); } else { - updateLegendDecimals(ctrl.height); + updateLegendDecimals(); } renderLegendElement(tableHeaderElem); @@ -227,6 +230,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var maxLegendHeight = ctrl.height / 2; $container.css("max-height", maxLegendHeight - 6); $container.append(seriesElements); + if (!legendScrollbar) { legendScrollbar = new PerfectScrollbar($container[0]); } From 017b337f006c4b7e498e37404112143d048aec79 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Thu, 7 Dec 2017 19:32:38 +0100 Subject: [PATCH 83/96] dashfolders: create folder page Fixes #10085 --- pkg/api/index.go | 2 +- .../manage_dashboards/manage_dashboards.html | 2 +- public/app/core/routes/routes.ts | 5 ++ public/app/features/dashboard/all.ts | 2 + .../features/dashboard/create_folder_ctrl.ts | 41 +++++++++++++++++ .../dashboard/partials/create_folder.html | 46 +++++++++++++++++++ 6 files changed, 96 insertions(+), 2 deletions(-) create mode 100644 public/app/features/dashboard/create_folder_ctrl.ts create mode 100644 public/app/features/dashboard/partials/create_folder.html diff --git a/pkg/api/index.go b/pkg/api/index.go index 1e44ef33050..058f5541eb5 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -95,7 +95,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { Url: "#", Children: []*dtos.NavLink{ {Text: "Dashboard", Icon: "gicon gicon-dashboard-new", Url: setting.AppSubUrl + "/dashboard/new"}, - {Text: "Folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboard/new/?editview=new-folder"}, + {Text: "Folder", SubTitle: "Create a new folder to organize your dashboards", Id: "folder", Icon: "gicon gicon-folder-new", Url: setting.AppSubUrl + "/dashboards/folder/new"}, {Text: "Import", SubTitle: "Import dashboard from file or Grafana.com", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, }, }) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 8c5aeb0facd..5daa8ddf55a 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -8,7 +8,7 @@ Dashboard - + Folder diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 8b42545d334..a068cd4a789 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -73,6 +73,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/folder/new', { + templateUrl: 'public/app/features/dashboard/partials/create_folder.html', + controller : 'CreateFolderCtrl', + controllerAs: 'ctrl', + }) .when('/dashboards/folder/:folderId/:type/:slug/permissions', { templateUrl: 'public/app/features/dashboard/partials/folder_permissions.html', controller : 'FolderPermissionsCtrl', diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index a18af82215d..f21338f5d6a 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -32,8 +32,10 @@ import {DashboardListCtrl} from './dashboard_list_ctrl'; import {FolderDashboardsCtrl} from './folder_dashboards_ctrl'; import {FolderPermissionsCtrl} from './folder_permissions_ctrl'; import {DashboardImportCtrl} from './dashboard_import_ctrl'; +import {CreateFolderCtrl} from './create_folder_ctrl'; coreModule.controller('DashboardListCtrl', DashboardListCtrl); coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl); coreModule.controller('FolderPermissionsCtrl', FolderPermissionsCtrl); coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); +coreModule.controller('CreateFolderCtrl', CreateFolderCtrl); diff --git a/public/app/features/dashboard/create_folder_ctrl.ts b/public/app/features/dashboard/create_folder_ctrl.ts new file mode 100644 index 00000000000..f194dabbf1e --- /dev/null +++ b/public/app/features/dashboard/create_folder_ctrl.ts @@ -0,0 +1,41 @@ +import appEvents from 'app/core/app_events'; + +export class CreateFolderCtrl { + title = ''; + navModel: any; + nameExists = false; + titleTouched = false; + + constructor(private backendSrv, private $location, navModelSrv) { + this.navModel = navModelSrv.getNav('create', 'folder'); + } + + create() { + if (!this.title || this.title.trim().length === 0) { + return; + } + + const title = this.title.trim(); + + return this.backendSrv.createDashboardFolder(title).then(result => { + appEvents.emit('alert-success', ['Folder Created', 'OK']); + + var folderUrl = `/dashboards/folder/${result.id}/db/${result.slug}`; + this.$location.url(folderUrl); + }); + } + + titleChanged() { + this.titleTouched = true; + + this.backendSrv.search({query: this.title}).then(res => { + this.nameExists = false; + for (let hit of res) { + if (this.title === hit.title) { + this.nameExists = true; + break; + } + } + }); + } +} diff --git a/public/app/features/dashboard/partials/create_folder.html b/public/app/features/dashboard/partials/create_folder.html new file mode 100644 index 00000000000..6548057a796 --- /dev/null +++ b/public/app/features/dashboard/partials/create_folder.html @@ -0,0 +1,46 @@ + + +
    +

    + Create New Folder +

    + + + +
    From 8415ea6c714b2dfd94ada10547284af5b7bf6345 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 7 Dec 2017 19:53:32 +0100 Subject: [PATCH 84/96] refactoring: changing how graph height and legend height is calculated, using flex box seems to actually work, #10079 --- public/app/features/panel/panel_directive.ts | 2 +- public/app/plugins/panel/graph/graph.ts | 41 ++--------- public/app/plugins/panel/graph/legend.ts | 45 +++++------- public/app/plugins/panel/graph/template.ts | 20 ++---- public/sass/components/_panel_graph.scss | 72 ++++++++++---------- 5 files changed, 62 insertions(+), 118 deletions(-) diff --git a/public/app/features/panel/panel_directive.ts b/public/app/features/panel/panel_directive.ts index 0577c6fbfcc..323cd4e6f6b 100644 --- a/public/app/features/panel/panel_directive.ts +++ b/public/app/features/panel/panel_directive.ts @@ -20,7 +20,7 @@ var panelTemplate = `
    - +
    diff --git a/public/app/plugins/panel/graph/graph.ts b/public/app/plugins/panel/graph/graph.ts index 30761238d82..9a67f24890e 100755 --- a/public/app/plugins/panel/graph/graph.ts +++ b/public/app/plugins/panel/graph/graph.ts @@ -92,48 +92,11 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { } }, scope); - function getLegendHeight(panelHeight) { - const LEGEND_PADDING = 23; - - if (!panel.legend.show || panel.legend.rightSide) { - return 0; - } - - let legendHeight = getLegendContainerHeight() + LEGEND_PADDING; - return Math.min(legendHeight, Math.floor(panelHeight/2)); - } - - function getLegendContainerHeight() { - try { - let graphWrapperElem = elem.parent().parent(); - let legendElem = graphWrapperElem.find('.graph-legend-wrapper'); - let legendHeight = legendElem.height(); - return legendHeight; - } catch (e) { - console.log(e); - return 0; - } - } - - function setElementHeight() { - try { - var height = ctrl.height - getLegendHeight(ctrl.height); - elem.css('height', height + 'px'); - - return true; - } catch (e) { // IE throws errors sometimes - console.log(e); - return false; - } - } - function shouldAbortRender() { if (!data) { return true; } - if (!setElementHeight()) { return true; } - if (panelWidth === 0) { return true; } @@ -152,6 +115,10 @@ function graphDirective(timeSrv, popoverSrv, contextSrv) { $("
    ").text(panel.yaxes[1].label).appendTo(elem); } + if (ctrl.dataWarning) { + $(`
    ${ctrl.dataWarning.title}
    `).appendTo(elem); + } + thresholdManager.draw(plot); } diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 854adef48fb..4444c8b35ce 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -10,7 +10,6 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { return { link: function(scope, elem) { - var $container = $('
    '); var firstRender = true; var ctrl = scope.ctrl; var panel = ctrl.panel; @@ -20,7 +19,9 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var legendScrollbar; scope.$on("$destroy", function() { - legendScrollbar.destroy(); + if (!legendScrollbar) { + legendScrollbar.destroy(); + } }); ctrl.events.on('render-legend', () => { @@ -73,9 +74,9 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var el = $(e.currentTarget); var index = getSeriesIndexForElement(el); var seriesInfo = seriesList[index]; - var scrollPosition = $($container.children('tbody')).scrollTop(); + var scrollPosition = $(elem.children('tbody')).scrollTop(); ctrl.toggleSeries(seriesInfo, e); - $($container.children('tbody')).scrollTop(scrollPosition); + $(elem.children('tbody')).scrollTop(scrollPosition); } function sortLegend(e) { @@ -117,22 +118,21 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { } if (firstRender) { - elem.append($container); - $container.on('click', '.graph-legend-icon', openColorSelector); - $container.on('click', '.graph-legend-alias', toggleSeries); - $container.on('click', 'th', sortLegend); + elem.on('click', '.graph-legend-icon', openColorSelector); + elem.on('click', '.graph-legend-alias', toggleSeries); + elem.on('click', 'th', sortLegend); firstRender = false; } seriesList = data; - $container.empty(); + elem.empty(); // Set min-width if side style and there is a value, otherwise remove the CSS propery var width = panel.legend.rightSide && panel.legend.sideWidth ? panel.legend.sideWidth + "px" : ""; - $container.css("min-width", width); + elem.css("min-width", width); - $container.toggleClass('graph-legend-table', panel.legend.alignAsTable === true); + elem.toggleClass('graph-legend-table', panel.legend.alignAsTable === true); var tableHeaderElem; if (panel.legend.alignAsTable) { @@ -162,7 +162,7 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { if (!panel.legend.rightSide) { renderLegendElement(tableHeaderElem); updateLegendDecimals(); - $container.empty(); + elem.empty(); } else { updateLegendDecimals(); } @@ -214,26 +214,17 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { var seriesElements = renderSeriesLegendElements(); if (panel.legend.alignAsTable) { - var maxHeight = ctrl.height; - - if (!panel.legend.rightSide) { - maxHeight = maxHeight/2; - } - - var topPadding = 6; var tbodyElem = $(''); - tbodyElem.css("max-height", maxHeight - topPadding); tbodyElem.append(tableHeaderElem); tbodyElem.append(seriesElements); - $container.append(tbodyElem); + elem.append(tbodyElem); } else { - var maxLegendHeight = ctrl.height / 2; - $container.css("max-height", maxLegendHeight - 6); - $container.append(seriesElements); + elem.append(seriesElements); + } - if (!legendScrollbar) { - legendScrollbar = new PerfectScrollbar($container[0]); - } + if (!legendScrollbar) { + legendScrollbar = new PerfectScrollbar(elem[0]); + } else { legendScrollbar.update(); } } diff --git a/public/app/plugins/panel/graph/template.ts b/public/app/plugins/panel/graph/template.ts index 6ded97ce33a..0b9eb8227df 100644 --- a/public/app/plugins/panel/graph/template.ts +++ b/public/app/plugins/panel/graph/template.ts @@ -1,22 +1,10 @@ var template = ` -
    -
    - -
    - {{ctrl.dataWarning.title}} -
    - -
    -
    - +
    +
    -
    -
    - -
    +
    +
    `; export default template; - - diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index fcf95d6d702..119f1c905cc 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -1,10 +1,39 @@ -.graph-canvas-wrapper { - position: relative; - cursor: crosshair; +.graph-panel { + display: flex; + flex-direction: column; + height: 100%; + + &--legend-right { + flex-direction: row; + + // .graph-panel__chart { + // display: table-cell; + // width: 100%; + // position: relative; + // } + + .graph-legend { + max-height: 100%; + position: relative; + left: 4px; + margin: 0 0 0 1rem; + } + + .graph-legend-series { + display: block; + padding-left: 0px; + } + + .graph-legend-table .graph-legend-series { + display: table-row; + } + } } -.histogram-chart { +.graph-panel__chart { position: relative; + cursor: crosshair; + flex-grow: 1; } .datapoints-warning { @@ -23,6 +52,8 @@ .graph-legend { @include clearfix(); + flex: 0 1 auto; + max-height: 30%; margin: 0 $spacer; text-align: center; width: calc(100% - $spacer); @@ -161,39 +192,6 @@ } } -.graph-legend-rightside { - - &.graph-wrapper { - display: table; - width: 100%; - } - - .graph-canvas-wrapper { - display: table-cell; - width: 100%; - position: relative; - } - - .graph-legend-wrapper { - display: table-cell; - vertical-align: top; - position: relative; - left: 4px; - } - - .graph-legend { - margin: 0 0 0 1rem; - } - - .graph-legend-series { - display: block; - padding-left: 0px; - } - - .graph-legend-table .graph-legend-series { - display: table-row; - } -} .graph-legend-series-hidden { .graph-legend-value, From 3de17ecda7285c20cd826ebd33b69ea254906f18 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 8 Dec 2017 10:21:00 +0300 Subject: [PATCH 85/96] dashboard grid: enable CSS transforms (#10125) --- .../dashboard/dashgrid/DashboardGrid.tsx | 2 +- public/app/features/panel/panel_header.ts | 26 +++++++++++++++++++ public/sass/components/_dashboard_grid.scss | 7 +++++ 3 files changed, 34 insertions(+), 1 deletion(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 283bf0c74d3..bb06df15183 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -30,7 +30,7 @@ function GridWrapper({size, layout, onLayoutChange, children, onResize, onResize isResizable={true} measureBeforeMount={false} containerPadding={[0, 0]} - useCSSTransforms={false} + useCSSTransforms={true} margin={[GRID_CELL_VMARGIN, GRID_CELL_VMARGIN]} cols={GRID_COLUMN_COUNT} rowHeight={GRID_CELL_HEIGHT} diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index 846202bc61d..eba8d24bdfd 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -1,5 +1,6 @@ /// +import $ from 'jquery'; import {coreModule} from 'app/core/core'; var template = ` @@ -106,13 +107,38 @@ function panelHeader($compile) { } }); + elem.find('.panel-menu-toggle').click((evt) => { + console.log(evt); + togglePanelState(); + }); + function togglePanelMenu(e) { if (!isDragged) { e.stopPropagation(); + togglePanelState(); elem.find('[data-toggle=dropdown]').dropdown('toggle'); } } + /** + * Hack for adding special class 'dropdown-menu-open' to the panel. + * This class sets z-index for panel and prevents menu overlapping. + */ + function togglePanelState() { + const menuOpenClass = 'dropdown-menu-open'; + const panelGridClass = '.react-grid-item.panel'; + + let panelElem = elem.find('[data-toggle=dropdown]').parentsUntil('.panel').parent(); + let menuElem = elem.find('[data-toggle=dropdown]').parent(); + panelElem = panelElem && panelElem.length ? panelElem[0] : undefined; + if (panelElem) { + panelElem = $(panelElem); + $(panelGridClass).removeClass(menuOpenClass); + let state = !menuElem.hasClass('open'); + panelElem.toggleClass(menuOpenClass, state); + } + } + let mouseX, mouseY; elem.mousedown((e) => { mouseX = e.pageX; diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index b7886a3a2dc..a2ec6f5ff4c 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -47,3 +47,10 @@ } } +// Hack for preventing panel menu overlapping. +.react-grid-item.resizing.panel, +.react-grid-item.panel.dropdown-menu-open, +.react-grid-item.react-draggable-dragging.panel { + z-index: $zindex-dropdown; +} + From cd3cf5d19b9ffb413bd7000f74294073e6ee1900 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 8 Dec 2017 10:43:47 +0300 Subject: [PATCH 86/96] grid css transforms: minor refactor (#10128) --- public/app/features/panel/panel_header.ts | 9 ++++----- public/sass/components/_dashboard_grid.scss | 1 - 2 files changed, 4 insertions(+), 6 deletions(-) diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index eba8d24bdfd..c83aea05527 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -107,15 +107,14 @@ function panelHeader($compile) { } }); - elem.find('.panel-menu-toggle').click((evt) => { - console.log(evt); - togglePanelState(); + elem.find('.panel-menu-toggle').click(() => { + togglePanelStackPosition(); }); function togglePanelMenu(e) { if (!isDragged) { e.stopPropagation(); - togglePanelState(); + togglePanelStackPosition(); elem.find('[data-toggle=dropdown]').dropdown('toggle'); } } @@ -124,7 +123,7 @@ function panelHeader($compile) { * Hack for adding special class 'dropdown-menu-open' to the panel. * This class sets z-index for panel and prevents menu overlapping. */ - function togglePanelState() { + function togglePanelStackPosition() { const menuOpenClass = 'dropdown-menu-open'; const panelGridClass = '.react-grid-item.panel'; diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index a2ec6f5ff4c..076e8cad0b7 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -53,4 +53,3 @@ .react-grid-item.react-draggable-dragging.panel { z-index: $zindex-dropdown; } - From 4aad4a069c843eaba6cebf16ba52673d4351b6ba Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 8 Dec 2017 10:54:53 +0100 Subject: [PATCH 87/96] ux: graph legend refactoring --- public/app/plugins/panel/graph/legend.ts | 10 +++++----- public/sass/components/_panel_graph.scss | 14 +++----------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/public/app/plugins/panel/graph/legend.ts b/public/app/plugins/panel/graph/legend.ts index 4444c8b35ce..e7589170301 100644 --- a/public/app/plugins/panel/graph/legend.ts +++ b/public/app/plugins/panel/graph/legend.ts @@ -220,12 +220,12 @@ module.directive('graphLegend', function(popoverSrv, $timeout) { elem.append(tbodyElem); } else { elem.append(seriesElements); - } - if (!legendScrollbar) { - legendScrollbar = new PerfectScrollbar(elem[0]); - } else { - legendScrollbar.update(); + if (!legendScrollbar) { + legendScrollbar = new PerfectScrollbar(elem[0]); + } else { + legendScrollbar.update(); + } } } } diff --git a/public/sass/components/_panel_graph.scss b/public/sass/components/_panel_graph.scss index 119f1c905cc..c28a608052d 100644 --- a/public/sass/components/_panel_graph.scss +++ b/public/sass/components/_panel_graph.scss @@ -6,17 +6,9 @@ &--legend-right { flex-direction: row; - // .graph-panel__chart { - // display: table-cell; - // width: 100%; - // position: relative; - // } - .graph-legend { + flex: 0 1 10px; max-height: 100%; - position: relative; - left: 4px; - margin: 0 0 0 1rem; } .graph-legend-series { @@ -51,12 +43,10 @@ } .graph-legend { - @include clearfix(); flex: 0 1 auto; max-height: 30%; margin: 0 $spacer; text-align: center; - width: calc(100% - $spacer); padding-top: 6px; position: relative; @@ -121,7 +111,9 @@ display: block; overflow-y: auto; overflow-x: hidden; + height: 100%; padding-bottom: 1px; + padding-right: 5px; } .graph-legend-series { From f78f86d0ab19f631f6f7b39e88b4f0fe48a585aa Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Fri, 8 Dec 2017 11:23:57 +0100 Subject: [PATCH 88/96] dashfolders: Hide search input area when showing CTA. #10083 --- .../core/components/manage_dashboards/manage_dashboards.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/app/core/components/manage_dashboards/manage_dashboards.html b/public/app/core/components/manage_dashboards/manage_dashboards.html index 5daa8ddf55a..957bca0262c 100644 --- a/public/app/core/components/manage_dashboards/manage_dashboards.html +++ b/public/app/core/components/manage_dashboards/manage_dashboards.html @@ -1,4 +1,4 @@ -
    +
    From 35a7109a6af58ae69ef7f2566ded2d11b9a0f743 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 8 Dec 2017 11:45:44 +0100 Subject: [PATCH 89/96] refactoring: fixing bug when all values are null --- public/app/core/time_series2.ts | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/public/app/core/time_series2.ts b/public/app/core/time_series2.ts index c1caf82b348..5a242a5abc1 100644 --- a/public/app/core/time_series2.ts +++ b/public/app/core/time_series2.ts @@ -44,12 +44,18 @@ export function updateLegendValues(data: TimeSeries[], panel) { } export function getDataMinMax(data: TimeSeries[]) { - const datamin = _.minBy(data, (series) => { - return series.stats.min; - }).stats.min; - const datamax = _.maxBy(data, (series: TimeSeries) => { - return series.stats.max; - }).stats.max; + let datamin = null; + let datamax = null; + + for (let series of data) { + if (datamax === null || datamax < series.stats.max) { + datamax = series.stats.max; + } + if (datamin === null || datamin > series.stats.min) { + datamin = series.stats.min; + } + } + return {datamin, datamax}; } From e0c173c5afd3a353fda6fc2fdca396438a5e71b6 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Fri, 8 Dec 2017 13:49:14 +0300 Subject: [PATCH 90/96] Don't animate panels on initial render (#10130) * grid: dont animate panels on initial render * grid: dont animate panels on initial render --- .../dashboard/dashgrid/DashboardGrid.tsx | 76 ++++++++++++------- public/sass/components/_dashboard_grid.scss | 9 +++ 2 files changed, 58 insertions(+), 27 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index bb06df15183..392d8f4e805 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -11,37 +11,59 @@ import sizeMe from 'react-sizeme'; let lastGridWidth = 1200; -function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange}) { - if (size.width === 0) { - console.log('size is zero!'); +export interface GridWrapperProps { + size: any; + layout: any; + children: any; + onResize: any; + onResizeStop: any; + onWidthChange: any; + onLayoutChange: any; +} + +class GridWrapper extends React.Component { + animated: boolean; + + constructor(props) { + super(props); + if (this.props.size.width === 0) { + console.log('size is zero!'); + } + + const width = this.props.size.width > 0 ? this.props.size.width : lastGridWidth; + if (width !== lastGridWidth) { + this.props.onWidthChange(); + lastGridWidth = width; + } } - const width = size.width > 0 ? size.width : lastGridWidth; - if (width !== lastGridWidth) { - onWidthChange(); - lastGridWidth = width; + componentDidMount() { + // Disable animation on initial rendering and enable it when component has been mounted. + this.animated = true; } - return ( - - {children} - - ); + render() { + return ( + + {this.props.children} + + ); + } } const SizedReactLayoutGrid = sizeMe({monitorWidth: true})(GridWrapper); diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index 076e8cad0b7..656efe4c880 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -53,3 +53,12 @@ .react-grid-item.react-draggable-dragging.panel { z-index: $zindex-dropdown; } + +// Disable animation on initial rendering and enable it when component has been mounted. +.react-grid-item.cssTransforms.panel { + transition-property: none; +} + +.animated .react-grid-item.cssTransforms.panel { + transition-property: transform; +} From 974d4d9739e43e537f679e9a11f6a8ef5c70fe16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 8 Dec 2017 11:53:51 +0100 Subject: [PATCH 91/96] Revert "Don't animate panels on initial render (#10130)" This reverts commit e0c173c5afd3a353fda6fc2fdca396438a5e71b6. --- .../dashboard/dashgrid/DashboardGrid.tsx | 76 +++++++------------ public/sass/components/_dashboard_grid.scss | 9 --- 2 files changed, 27 insertions(+), 58 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 392d8f4e805..bb06df15183 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -11,59 +11,37 @@ import sizeMe from 'react-sizeme'; let lastGridWidth = 1200; -export interface GridWrapperProps { - size: any; - layout: any; - children: any; - onResize: any; - onResizeStop: any; - onWidthChange: any; - onLayoutChange: any; -} - -class GridWrapper extends React.Component { - animated: boolean; - - constructor(props) { - super(props); - if (this.props.size.width === 0) { - console.log('size is zero!'); - } - - const width = this.props.size.width > 0 ? this.props.size.width : lastGridWidth; - if (width !== lastGridWidth) { - this.props.onWidthChange(); - lastGridWidth = width; - } +function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange}) { + if (size.width === 0) { + console.log('size is zero!'); } - componentDidMount() { - // Disable animation on initial rendering and enable it when component has been mounted. - this.animated = true; + const width = size.width > 0 ? size.width : lastGridWidth; + if (width !== lastGridWidth) { + onWidthChange(); + lastGridWidth = width; } - render() { - return ( - - {this.props.children} - - ); - } + return ( + + {children} + + ); } const SizedReactLayoutGrid = sizeMe({monitorWidth: true})(GridWrapper); diff --git a/public/sass/components/_dashboard_grid.scss b/public/sass/components/_dashboard_grid.scss index 656efe4c880..076e8cad0b7 100644 --- a/public/sass/components/_dashboard_grid.scss +++ b/public/sass/components/_dashboard_grid.scss @@ -53,12 +53,3 @@ .react-grid-item.react-draggable-dragging.panel { z-index: $zindex-dropdown; } - -// Disable animation on initial rendering and enable it when component has been mounted. -.react-grid-item.cssTransforms.panel { - transition-property: none; -} - -.animated .react-grid-item.cssTransforms.panel { - transition-property: transform; -} From e03abde778dcff9302e0a5b3b25affde461434cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 8 Dec 2017 12:17:09 +0100 Subject: [PATCH 92/96] refactoring: #10130 --- .../dashboard/dashgrid/DashboardGrid.tsx | 16 ++++++++++++++-- public/app/plugins/panel/graph/legend.ts | 2 +- public/sass/components/_dashboard_grid.scss | 9 +++++++++ 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index bb06df15183..467a65597a5 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -11,7 +11,7 @@ import sizeMe from 'react-sizeme'; let lastGridWidth = 1200; -function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange}) { +function GridWrapper({size, layout, onLayoutChange, children, onResize, onResizeStop, onWidthChange, className}) { if (size.width === 0) { console.log('size is zero!'); } @@ -25,7 +25,7 @@ function GridWrapper({size, layout, onLayoutChange, children, onResize, onResize return ( { this.onResizeStop = this.onResizeStop.bind(this); this.onWidthChange = this.onWidthChange.bind(this); + this.state = {animated: false}; + // subscribe to dashboard events this.dashboard = this.panelContainer.getDashboard(); this.dashboard.on('panel-added', this.triggerForceUpdate.bind(this)); @@ -134,6 +136,14 @@ export class DashboardGrid extends React.Component { this.panelMap[newItem.i].resizeDone(); } + componentDidMount() { + setTimeout(() => { + this.setState(() => { + return {animated: true}; + }); + }); + } + renderPanels() { const panelElements = []; @@ -150,8 +160,10 @@ export class DashboardGrid extends React.Component { } render() { + console.log('animated', this.state.animated); return ( Date: Fri, 8 Dec 2017 12:26:19 +0100 Subject: [PATCH 93/96] tech: updated ngreact and with custom PR applied --- package.json | 1 - public/app/app.ts | 1 - public/app/core/components/grafana_app.ts | 2 - public/app/core/core.ts | 1 + public/app/core/services/ng_react.ts | 301 ++++++++++++++++++ .../dashboard/dashgrid/DashboardGrid.tsx | 1 - 6 files changed, 302 insertions(+), 5 deletions(-) create mode 100644 public/app/core/services/ng_react.ts diff --git a/package.json b/package.json index 492bbf5359e..59608e80403 100644 --- a/package.json +++ b/package.json @@ -125,7 +125,6 @@ "lodash": "^4.17.4", "moment": "^2.18.1", "mousetrap": "^1.6.0", - "ngreact": "^0.4.1", "perfect-scrollbar": "^1.2.0", "prop-types": "^15.6.0", "react": "^16.1.1", diff --git a/public/app/app.ts b/public/app/app.ts index 8e345c6abed..b76dceb4943 100644 --- a/public/app/app.ts +++ b/public/app/app.ts @@ -9,7 +9,6 @@ import 'angular-native-dragdrop'; import 'angular-bindonce'; import 'react'; import 'react-dom'; -import 'ngreact'; import 'vendor/bootstrap/bootstrap'; import 'vendor/angular-ui/ui-bootstrap-tpls'; diff --git a/public/app/core/components/grafana_app.ts b/public/app/core/components/grafana_app.ts index 39601f903ec..eff55405d6f 100644 --- a/public/app/core/components/grafana_app.ts +++ b/public/app/core/components/grafana_app.ts @@ -1,5 +1,3 @@ -/// - import config from 'app/core/config'; import _ from 'lodash'; import $ from 'jquery'; diff --git a/public/app/core/core.ts b/public/app/core/core.ts index 2c5c5653fb2..5a9b4265f8c 100644 --- a/public/app/core/core.ts +++ b/public/app/core/core.ts @@ -18,6 +18,7 @@ import './components/colorpicker/ColorPicker'; import './components/colorpicker/SeriesColorPicker'; import './components/colorpicker/spectrum_picker'; import './services/search_srv'; +import './services/ng_react'; import {grafanaAppDirective} from './components/grafana_app'; import {sideMenuDirective} from './components/sidemenu/sidemenu'; diff --git a/public/app/core/services/ng_react.ts b/public/app/core/services/ng_react.ts new file mode 100644 index 00000000000..9922772c730 --- /dev/null +++ b/public/app/core/services/ng_react.ts @@ -0,0 +1,301 @@ +// +// This is using ng-react with this PR applied https://github.com/ngReact/ngReact/pull/199 +// + + +// # ngReact +// ### Use React Components inside of your Angular applications +// +// Composed of +// - reactComponent (generic directive for delegating off to React Components) +// - reactDirective (factory for creating specific directives that correspond to reactComponent directives) + +import React from 'react'; +import ReactDOM from 'react-dom'; +import angular from 'angular'; + +// get a react component from name (components can be an angular injectable e.g. value, factory or +// available on window +function getReactComponent(name, $injector) { + // if name is a function assume it is component and return it + if (angular.isFunction(name)) { + return name; + } + + // a React component name must be specified + if (!name) { + throw new Error('ReactComponent name attribute must be specified'); + } + + // ensure the specified React component is accessible, and fail fast if it's not + var reactComponent; + try { + reactComponent = $injector.get(name); + } catch (e) {} + + if (!reactComponent) { + try { + reactComponent = name.split('.').reduce(function(current, namePart) { + return current[namePart]; + }, window); + } catch (e) {} + } + + if (!reactComponent) { + throw Error('Cannot find react component ' + name); + } + + return reactComponent; +} + +// wraps a function with scope.$apply, if already applied just return +function applied(fn, scope) { + if (fn.wrappedInApply) { + return fn; + } + var wrapped: any = function() { + var args = arguments; + var phase = scope.$root.$$phase; + if (phase === '$apply' || phase === '$digest') { + return fn.apply(null, args); + } else { + return scope.$apply(function() { + return fn.apply(null, args); + }); + } + }; + wrapped.wrappedInApply = true; + return wrapped; +} + +/** + * wraps functions on obj in scope.$apply + * + * keeps backwards compatibility, as if propsConfig is not passed, it will + * work as before, wrapping all functions and won't wrap only when specified. + * + * @version 0.4.1 + * @param obj react component props + * @param scope current scope + * @param propsConfig configuration object for all properties + * @returns {Object} props with the functions wrapped in scope.$apply + */ +function applyFunctions(obj, scope, propsConfig?) { + return Object.keys(obj || {}).reduce(function(prev, key) { + var value = obj[key]; + var config = (propsConfig || {})[key] || {}; + /** + * wrap functions in a function that ensures they are scope.$applied + * ensures that when function is called from a React component + * the Angular digest cycle is run + */ + prev[key] = angular.isFunction(value) && config.wrapApply !== false ? applied(value, scope) : value; + + return prev; + }, {}); +} + +/** + * + * @param watchDepth (value of HTML watch-depth attribute) + * @param scope (angular scope) + * + * Uses the watchDepth attribute to determine how to watch props on scope. + * If watchDepth attribute is NOT reference or collection, watchDepth defaults to deep watching by value + */ +function watchProps(watchDepth, scope, watchExpressions, listener) { + var supportsWatchCollection = angular.isFunction(scope.$watchCollection); + var supportsWatchGroup = angular.isFunction(scope.$watchGroup); + + var watchGroupExpressions = []; + + watchExpressions.forEach(function(expr) { + var actualExpr = getPropExpression(expr); + var exprWatchDepth = getPropWatchDepth(watchDepth, expr); + + if (exprWatchDepth === 'collection' && supportsWatchCollection) { + scope.$watchCollection(actualExpr, listener); + } else if (exprWatchDepth === 'reference' && supportsWatchGroup) { + watchGroupExpressions.push(actualExpr); + } else if (exprWatchDepth === 'one-time') { + //do nothing because we handle our one time bindings after this + } else { + scope.$watch(actualExpr, listener, exprWatchDepth !== 'reference'); + } + }); + + if (watchDepth === 'one-time') { + listener(); + } + + if (watchGroupExpressions.length) { + scope.$watchGroup(watchGroupExpressions, listener); + } +} + +// render React component, with scope[attrs.props] being passed in as the component props +function renderComponent(component, props, scope, elem) { + scope.$evalAsync(function() { + ReactDOM.render(React.createElement(component, props), elem[0]); + }); +} + +// get prop name from prop (string or array) +function getPropName(prop) { + return Array.isArray(prop) ? prop[0] : prop; +} + +// get prop name from prop (string or array) +function getPropConfig(prop) { + return Array.isArray(prop) ? prop[1] : {}; +} + +// get prop expression from prop (string or array) +function getPropExpression(prop) { + return Array.isArray(prop) ? prop[0] : prop; +} + +// find the normalized attribute knowing that React props accept any type of capitalization +function findAttribute(attrs, propName) { + var index = Object.keys(attrs).filter(function(attr) { + return attr.toLowerCase() === propName.toLowerCase(); + })[0]; + return attrs[index]; +} + +// get watch depth of prop (string or array) +function getPropWatchDepth(defaultWatch, prop) { + var customWatchDepth = Array.isArray(prop) && angular.isObject(prop[1]) && prop[1].watchDepth; + return customWatchDepth || defaultWatch; +} + +// # reactComponent +// Directive that allows React components to be used in Angular templates. +// +// Usage: +// +// +// This requires that there exists an injectable or globally available 'Hello' React component. +// The 'props' attribute is optional and is passed to the component. +// +// The following would would create and register the component: +// +// var module = angular.module('ace.react.components'); +// module.value('Hello', React.createClass({ +// render: function() { +// return
    Hello {this.props.name}
    ; +// } +// })); +// +var reactComponent = function($injector) { + return { + restrict: 'E', + replace: true, + link: function(scope, elem, attrs) { + var reactComponent = getReactComponent(attrs.name, $injector); + + var renderMyComponent = function() { + var scopeProps = scope.$eval(attrs.props); + var props = applyFunctions(scopeProps, scope); + + renderComponent(reactComponent, props, scope, elem); + }; + + // If there are props, re-render when they change + attrs.props ? watchProps(attrs.watchDepth, scope, [attrs.props], renderMyComponent) : renderMyComponent(); + + // cleanup when scope is destroyed + scope.$on('$destroy', function() { + if (!attrs.onScopeDestroy) { + ReactDOM.unmountComponentAtNode(elem[0]); + } else { + scope.$eval(attrs.onScopeDestroy, { + unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]), + }); + } + }); + }, + }; +}; + +// # reactDirective +// Factory function to create directives for React components. +// +// With a component like this: +// +// var module = angular.module('ace.react.components'); +// module.value('Hello', React.createClass({ +// render: function() { +// return
    Hello {this.props.name}
    ; +// } +// })); +// +// A directive can be created and registered with: +// +// module.directive('hello', function(reactDirective) { +// return reactDirective('Hello', ['name']); +// }); +// +// Where the first argument is the injectable or globally accessible name of the React component +// and the second argument is an array of property names to be watched and passed to the React component +// as props. +// +// This directive can then be used like this: +// +// +// +var reactDirective = function($injector) { + return function(reactComponentName, props, conf, injectableProps) { + var directive = { + restrict: 'E', + replace: true, + link: function(scope, elem, attrs) { + var reactComponent = getReactComponent(reactComponentName, $injector); + + // if props is not defined, fall back to use the React component's propTypes if present + props = props || Object.keys(reactComponent.propTypes || {}); + + // for each of the properties, get their scope value and set it to scope.props + var renderMyComponent = function() { + var scopeProps = {}, + config = {}; + + props.forEach(function(prop) { + var propName = getPropName(prop); + scopeProps[propName] = scope.$eval(findAttribute(attrs, propName)); + config[propName] = getPropConfig(prop); + }); + + scopeProps = applyFunctions(scopeProps, scope, config); + scopeProps = angular.extend({}, scopeProps, injectableProps); + renderComponent(reactComponent, scopeProps, scope, elem); + }; + + // watch each property name and trigger an update whenever something changes, + // to update scope.props with new values + var propExpressions = props.map(function(prop) { + return Array.isArray(prop) ? [attrs[getPropName(prop)], getPropConfig(prop)] : attrs[prop]; + }); + + // If we don't have any props, then our watch statement won't fire. + props.length ? watchProps(attrs.watchDepth, scope, propExpressions, renderMyComponent) : renderMyComponent(); + + // cleanup when scope is destroyed + scope.$on('$destroy', function() { + if (!attrs.onScopeDestroy) { + ReactDOM.unmountComponentAtNode(elem[0]); + } else { + scope.$eval(attrs.onScopeDestroy, { + unmountComponent: ReactDOM.unmountComponentAtNode.bind(this, elem[0]), + }); + } + }); + }, + }; + return angular.extend(directive, conf); + }; +}; + +let ngModule = angular.module('react', []); +ngModule.directive('reactComponent', ['$injector', reactComponent]); +ngModule.factory('reactDirective', ['$injector', reactDirective]); diff --git a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx index 467a65597a5..7b0f040e199 100644 --- a/public/app/features/dashboard/dashgrid/DashboardGrid.tsx +++ b/public/app/features/dashboard/dashgrid/DashboardGrid.tsx @@ -160,7 +160,6 @@ export class DashboardGrid extends React.Component { } render() { - console.log('animated', this.state.animated); return ( Date: Fri, 8 Dec 2017 13:30:16 +0100 Subject: [PATCH 94/96] ux: added react scrollbar component and added it to add panel panel --- .../core/components/ScrollBar/ScrollBar.tsx | 62 +++++++++++++++++++ .../app/features/dashboard/dashboard_ctrl.ts | 1 - .../dashboard/dashgrid/AddPanelPanel.tsx | 5 +- public/app/features/panel/panel_header.ts | 1 + public/sass/components/_panel_add_panel.scss | 1 + 5 files changed, 67 insertions(+), 3 deletions(-) create mode 100644 public/app/core/components/ScrollBar/ScrollBar.tsx diff --git a/public/app/core/components/ScrollBar/ScrollBar.tsx b/public/app/core/components/ScrollBar/ScrollBar.tsx new file mode 100644 index 00000000000..49a200b0f3b --- /dev/null +++ b/public/app/core/components/ScrollBar/ScrollBar.tsx @@ -0,0 +1,62 @@ +import React from 'react'; +import PerfectScrollbar from 'perfect-scrollbar'; + +export interface Props { + children: any; + className: string; +} + +export default class ScrollBar extends React.Component { + + private container: any; + private ps: PerfectScrollbar; + + constructor(props) { + super(props); + } + + componentDidMount() { + this.ps = new PerfectScrollbar(this.container); + } + + componentDidUpdate() { + this.ps.update(); + } + + componentWillUnmount() { + this.ps.destroy(); + } + + // methods can be invoked by outside + setScrollTop(top) { + if (this.container) { + this.container.scrollTop = top; + this.ps.update(); + + return true; + } + return false; + } + + setScrollLeft(left) { + if (this.container) { + this.container.scrollLeft = left; + this.ps.update(); + + return true; + } + return false; + } + + handleRef = ref => { + this.container = ref; + }; + + render() { + return ( +
    + {this.props.children} +
    + ); + } +} diff --git a/public/app/features/dashboard/dashboard_ctrl.ts b/public/app/features/dashboard/dashboard_ctrl.ts index 750826d386d..2c9d028ddf8 100644 --- a/public/app/features/dashboard/dashboard_ctrl.ts +++ b/public/app/features/dashboard/dashboard_ctrl.ts @@ -129,7 +129,6 @@ export class DashboardCtrl implements PanelContainer { } getPanelContainer() { - console.log('DashboardCtrl:getPanelContainer()'); return this; } diff --git a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx index 0dd4b9fcad6..75c8cbf36bc 100644 --- a/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx +++ b/public/app/features/dashboard/dashgrid/AddPanelPanel.tsx @@ -4,6 +4,7 @@ import _ from 'lodash'; import config from 'app/core/config'; import {PanelModel} from '../panel_model'; import {PanelContainer} from './PanelContainer'; +import ScrollBar from 'app/core/components/ScrollBar/ScrollBar'; export interface AddPanelPanelProps { panel: PanelModel; @@ -78,9 +79,9 @@ export class AddPanelPanel extends React.ComponentNew Panel Select a visualization
    -
    + {this.state.panelPlugins.map(this.renderPanelItem.bind(this))} -
    +
    ); diff --git a/public/app/features/panel/panel_header.ts b/public/app/features/panel/panel_header.ts index c83aea05527..5429a992a71 100644 --- a/public/app/features/panel/panel_header.ts +++ b/public/app/features/panel/panel_header.ts @@ -84,6 +84,7 @@ function panelHeader($compile) { restrict: 'E', template: template, link: function(scope, elem, attrs) { + console.log(elem.html()); let menuElem = elem.find('.panel-menu'); let menuScope; diff --git a/public/sass/components/_panel_add_panel.scss b/public/sass/components/_panel_add_panel.scss index be645574342..19eb90b2ea6 100644 --- a/public/sass/components/_panel_add_panel.scss +++ b/public/sass/components/_panel_add_panel.scss @@ -35,6 +35,7 @@ height: calc(100% - 43px); align-content: flex-start; justify-content: space-around; + position: relative; } .add-panel__item { From 9cae579c756b235d0ddc41dd300c59533cb78cc1 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Fri, 8 Dec 2017 13:37:37 +0100 Subject: [PATCH 95/96] fix for search dropdown on small screen + icon overlapping fix (#10091) * fix for search dropdown on small screen + icon overlapping fix * fixing search filter for small screen * hid tags and filter box * iphone input zoom fix * moved input styling to old-responsive --- public/sass/_old_responsive.scss | 8 ++++++++ public/sass/components/_search.scss | 15 +++++++++++++++ public/sass/components/_view_states.scss | 1 + 3 files changed, 24 insertions(+) diff --git a/public/sass/_old_responsive.scss b/public/sass/_old_responsive.scss index 72f4332a51e..fa119047f44 100644 --- a/public/sass/_old_responsive.scss +++ b/public/sass/_old_responsive.scss @@ -18,6 +18,14 @@ // Media queries // --------------------- +@include media-breakpoint-down(xs) { + input[type='text'], + input[type='number'], + textarea { + font-size: 16px; + } +} + @include media-breakpoint-up(sm) { .navbar-page-btn { max-width: 250px; diff --git a/public/sass/components/_search.scss b/public/sass/components/_search.scss index 25cb9a735a6..6c23f51aaa0 100644 --- a/public/sass/components/_search.scss +++ b/public/sass/components/_search.scss @@ -217,3 +217,18 @@ padding: $spacer*2 $spacer; background: $panel-bg; } + +@include media-breakpoint-down(xs) { + + .search-container { + left: 0; + } + + .search-dropdown__col_2 { + display: none; + } + + .search-item__tags { + display: none; + } +} diff --git a/public/sass/components/_view_states.scss b/public/sass/components/_view_states.scss index a698a81f1dc..b3c9a44e658 100644 --- a/public/sass/components/_view_states.scss +++ b/public/sass/components/_view_states.scss @@ -37,6 +37,7 @@ .navbar { box-shadow: none; background: transparent; + padding-left: $side-menu-width + 20px; } .navbar-page-btn { From 63b717f607d8e0a24ae39ec02b928ed58f195568 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Fri, 8 Dec 2017 13:54:11 +0100 Subject: [PATCH 96/96] ux: minor change to new folder page --- public/app/features/dashboard/partials/create_folder.html | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/public/app/features/dashboard/partials/create_folder.html b/public/app/features/dashboard/partials/create_folder.html index 6548057a796..ae5fcc126e8 100644 --- a/public/app/features/dashboard/partials/create_folder.html +++ b/public/app/features/dashboard/partials/create_folder.html @@ -1,15 +1,12 @@
    -

    - Create New Folder -