From b19b68df08fd27878dbf7f813532bd0aedf2fc02 Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Tue, 28 Nov 2017 11:27:29 +0100 Subject: [PATCH 01/24] 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 5fb3ce1be76f4623e995627ab4d510e5a16ffd4f Mon Sep 17 00:00:00 2001 From: Patrick O'Carroll Date: Mon, 4 Dec 2017 13:12:32 +0100 Subject: [PATCH 02/24] new grays for light theme --- public/sass/_variables.dark.scss | 1 + public/sass/_variables.light.scss | 57 ++++++++++++++++++--------- public/sass/components/_sidemenu.scss | 3 +- public/sass/layout/_page.scss | 5 +-- 4 files changed, 43 insertions(+), 23 deletions(-) diff --git a/public/sass/_variables.dark.scss b/public/sass/_variables.dark.scss index 6172fc77515..6a44493a001 100644 --- a/public/sass/_variables.dark.scss +++ b/public/sass/_variables.dark.scss @@ -238,6 +238,7 @@ $navbarButtonBackgroundHighlight: $body-bg; $side-menu-bg: $black; $side-menu-item-hover-bg: $dark-2; $side-menu-shadow: 0 0 20px black; +$side-menu-link-color: $link-color; $breadcrumb-hover-hl: #111; // Menu dropdowns diff --git a/public/sass/_variables.light.scss b/public/sass/_variables.light.scss index 621ec2a51a1..2360f6ff766 100644 --- a/public/sass/_variables.light.scss +++ b/public/sass/_variables.light.scss @@ -27,7 +27,7 @@ $gray-5: #ECECEC; $gray-6: #f4f5f8; $gray-7: #fbfbfb;*/ -$black: #000; +/*$black: #000; $dark-1: #121314; $dark-2: #1d1d1f; $dark-3: #272729; @@ -39,7 +39,22 @@ $gray-3: #b2b3b8; $gray-4: #d5d6db; $gray-5: #e9eaf0; $gray-6: #f4f5f8; -$gray-7: #fafbfc; +$gray-7: #fafbfc;*/ + +$black: #000; +$dark-1: #13161d; +$dark-2: #1e2028; +$dark-3: #303133; +$dark-4: #35373f; +$dark-5: #41444b; +$gray-1: #52545c; +$gray-2: #767980; +$gray-3: #acaeb6; +$gray-4: #ced0d8; +$gray-5: #dfe3eb; +//$gray-5: #dfe7eb; +$gray-6: #ebedf2; +$gray-7: #f7f8fa; $white: #fff; @@ -72,18 +87,21 @@ $critical: #EC2128; $body-bg: $white; $page-bg: $white; $body-color: $gray-1; -$text-color: $gray-1; +$text-color: $dark-4; $text-color-strong: $white; -$text-color-weak: $gray-3; +$text-color-weak: $gray-2; $text-color-faint: $gray-4; $text-color-emphasis: $dark-5; $text-shadow-strong: none; $text-shadow-faint: none; +$textShadow: none; // gradients $brand-gradient: linear-gradient(to right, rgba(255,213,0,1.0) 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-gradient: linear-gradient(120deg, transparent 40%, darken($gray-6, 4%) 98%); +$page-gradient: linear-gradient(120deg, $gray-7 40%, $gray-6 98%); +//$page-gradient: $gray-7; // Links // ------------------------- @@ -110,7 +128,7 @@ $component-active-bg: $brand-primary !default; // Panel // ------------------------- -$panel-bg: $gray-7; +$panel-bg: $white;//$gray-7; $panel-border-color: $gray-5; $panel-border: solid 1px $panel-border-color; $panel-drop-zone-bg: repeating-linear-gradient(-128deg, $body-bg, $body-bg 10px, $gray-6 10px, $gray-6 20px); @@ -130,12 +148,12 @@ $code-tag-bg: $gray-6; $code-tag-border: darken($code-tag-bg, 3%); // cards -$card-background: linear-gradient(135deg, $gray-5, $gray-6); +$card-background: linear-gradient(135deg, #ccd2d9, #dde4ed);//linear-gradient(135deg, $gray-4, $gray-5); $card-background-hover: linear-gradient(135deg, $gray-6, $gray-7); $card-shadow: -1px -1px 0 0 hsla(0, 0%, 100%, .1), 1px 1px 0 0 rgba(0, 0, 0, .1); // Lists -$list-item-bg: $card-background; +$list-item-bg: linear-gradient(135deg, #dde4ed, $gray-6);//$card-background; $list-item-hover-bg: darken($gray-5, 5%); $list-item-link-color: $text-color; $list-item-shadow: $card-shadow; @@ -170,9 +188,9 @@ $btn-warning-bg-hl: darken($orange, 3%); $btn-danger-bg: lighten($red, 3%); $btn-danger-bg-hl: darken($red, 3%); -$btn-inverse-bg: $gray-5; -$btn-inverse-bg-hl: darken($gray-5, 5%); -$btn-inverse-text-color: $dark-4; +$btn-inverse-bg: #acb6bf;//#dae5f0;//$gray-5; +$btn-inverse-bg-hl: darken(#b8c2cc, 5%); +$btn-inverse-text-color: $gray-7;//$dark-4; $btn-link-color: $gray-1; @@ -184,24 +202,25 @@ $iconContainerBackground: $white; // Forms // ------------------------- -$input-bg: $gray-7; +$input-bg: $white; $input-bg-disabled: $gray-5; $input-color: $dark-3; -$input-border-color: $gray-5; +$input-border-color: #dde4ed;//$gray-4; $input-box-shadow: none; $input-border-focus: $blue !default; $input-box-shadow-focus: $blue !default; $input-color-placeholder: $gray-4 !default; -$input-label-bg: #eaebee; -$input-label-border-color: #e3e4e7; +$input-label-bg: #dde4ed;//#dfe7f0;//#dae5f0;//#d8dfed;//$gray-5;#dde6f0; +$input-label-border-color: #dde4ed;//#dfe7f0;//#dae5f0;//#d8dfed;//$gray-5;#dde6f0; $input-invalid-border-color: lighten($red, 5%); // Sidemenu // ------------------------- -$side-menu-bg: $gray-7; -$side-menu-item-hover-bg: $gray-5; -$side-menu-shadow: 0 0 5px #c2c2c2; +$side-menu-bg: $dark-3; +$side-menu-item-hover-bg: $gray-1; +$side-menu-shadow: 0 0 5px $dark-5;//0 0 5px #c2c2c2; +$side-menu-link-color: $gray-6; // Menu dropdowns // ------------------------- @@ -264,7 +283,7 @@ $wellBackground: $gray-3; // ------------------------- $navbarHeight: 52px; -$navbarBackgroundHighlight: white; +$navbarBackgroundHighlight: $white; $navbarBackground: $white; $navbarBorder: 1px solid $gray-4; $navbarShadow: 0 0 3px #c1c1c1; diff --git a/public/sass/components/_sidemenu.scss b/public/sass/components/_sidemenu.scss index 2facca8dadc..fc9acc375d8 100644 --- a/public/sass/components/_sidemenu.scss +++ b/public/sass/components/_sidemenu.scss @@ -100,7 +100,7 @@ display: inline-block; .fa, .icon-gf, .gicon { - color: $link-color; + color: $side-menu-link-color; position: relative; opacity: .7; font-size: 130%; @@ -125,6 +125,7 @@ white-space: nowrap; background-color: $side-menu-item-hover-bg; font-size: 17px; + color: #ebedf2; } li.sidemenu-org-switcher { diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index a13e703717d..5c9e9622e7f 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -144,7 +144,6 @@ 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; @@ -166,13 +165,13 @@ &.active, &:hover { background: #fff;//#333; - background: linear-gradient($gray-4, darken($gray-4, 5%)); //linear-gradient(#333, #000); + background: linear-gradient(#dde4ed, darken(#dde4ed, 5%)); //linear-gradient(#333, #000); } &.active:after, &:hover:after { background: #fff;//#333; - background: linear-gradient(135deg, $gray-4,darken($gray-4, 5%));////linear-gradient(135deg, #333, #000); + background: linear-gradient(135deg, #dde4ed,darken(#dde4ed, 5%));////linear-gradient(135deg, #333, #000); } &:after { From 44ea0ff71d83ec16b543865fad1ecf4dea1a2b99 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 15:55:37 +0300 Subject: [PATCH 03/24] Move import dashboard from modal to the page --- pkg/api/index.go | 2 +- public/app/core/routes/routes.ts | 5 + public/app/features/dashboard/all.ts | 3 +- .../dashboard/dashboard_import_ctrl.ts | 163 ++++++++++++++++++ .../dashboard/partials/dashboardImport.html | 125 ++++++++++++++ ...ctrl_specs.ts => dashboard_import_ctrl.ts} | 6 +- 6 files changed, 299 insertions(+), 5 deletions(-) create mode 100644 public/app/features/dashboard/dashboard_import_ctrl.ts create mode 100644 public/app/features/dashboard/partials/dashboardImport.html rename public/app/features/dashboard/specs/{dash_import_ctrl_specs.ts => dashboard_import_ctrl.ts} (93%) diff --git a/pkg/api/index.go b/pkg/api/index.go index eb9e70bb404..d2069441b81 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -95,7 +95,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { 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: "Import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/new/?editview=import"}, }, }) } @@ -104,6 +103,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true}, {Divider: true, HideFromTabs: true}, {Text: "Manage", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"}, + {Text: "Import", Id: "import", Url: setting.AppSubUrl + "/dashboards/import", Icon: "gicon gicon-dashboard-import"}, {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"}, {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"}, } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index 68595da4296..a965cd406d3 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -68,6 +68,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) + .when('/dashboards/import', { + templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', + controller : 'DashboardImportCtrl', + controllerAs: 'ctrl', + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index b4f1f4e77f0..f2f2087a24e 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -15,7 +15,6 @@ import './unsavedChangesSrv'; import './unsaved_changes_modal'; import './timepicker/timepicker'; import './upload'; -import './import/dash_import'; import './export/export_modal'; import './export_data/export_data_modal'; import './ad_hoc_filters'; @@ -30,5 +29,7 @@ import './move_to_folder_modal/move_to_folder'; import coreModule from 'app/core/core_module'; import {DashboardListCtrl} from './dashboard_list_ctrl'; +import {DashboardImportCtrl} from './dashboard_import_ctrl'; coreModule.controller('DashboardListCtrl', DashboardListCtrl); +coreModule.controller('DashboardImportCtrl', DashboardImportCtrl); diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts new file mode 100644 index 00000000000..f2b3fb90fc9 --- /dev/null +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -0,0 +1,163 @@ +import _ from 'lodash'; +import config from 'app/core/config'; + +export class DashboardImportCtrl { + navModel: any; + step: number; + jsonText: string; + parseError: string; + nameExists: boolean; + dash: any; + inputs: any[]; + inputsValid: boolean; + gnetUrl: string; + gnetError: string; + gnetInfo: any; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) { + this.navModel = navModelSrv.getNav('dashboards', 'import', 0); + + 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 = ''; + } +} diff --git a/public/app/features/dashboard/partials/dashboardImport.html b/public/app/features/dashboard/partials/dashboardImport.html new file mode 100644 index 00000000000..be43078d32a --- /dev/null +++ b/public/app/features/dashboard/partials/dashboardImport.html @@ -0,0 +1,125 @@ + + +
+
+ +
+ +
+ +
Grafana.com Dashboard
+ +
+
+ +
+
+ +
+
+ +
Or paste JSON
+ +
+
+ +
+ + + + {{ctrl.parseError}} + +
+
+ +
+
+

+ Importing Dashboard from + Grafana.com +

+ +
+ + +
+
+ + +
+
+ +

+ Options +

+ +
+
+
+ + + +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ + +
+ +
+ + + +
+
+
+ +
+ + + Cancel +
+ +
+
diff --git a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.ts similarity index 93% rename from public/app/features/dashboard/specs/dash_import_ctrl_specs.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.ts index c541aca34b2..e78109b01a0 100644 --- a/public/app/features/dashboard/specs/dash_import_ctrl_specs.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.ts @@ -1,9 +1,9 @@ import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; -import {DashImportCtrl} from 'app/features/dashboard/import/dash_import'; +import {DashboardImportCtrl} from '../dashboard_import_ctrl'; import config from 'app/core/config'; -describe('DashImportCtrl', function() { +describe('DashboardImportCtrl', function() { var ctx: any = {}; var backendSrv = { search: sinon.stub().returns(Promise.resolve([])), @@ -15,7 +15,7 @@ describe('DashImportCtrl', function() { beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { ctx.$q = $q; ctx.scope = $rootScope.$new(); - ctx.ctrl = $controller(DashImportCtrl, { + ctx.ctrl = $controller(DashboardImportCtrl, { $scope: ctx.scope, backendSrv: backendSrv, }); From 4403d919c10457fb5d85a52303341255297fe18f Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 16:51:37 +0300 Subject: [PATCH 04/24] move DashboardImportCtrl tests to jest --- ..._ctrl.ts => dashboard_import_ctrl.jest.ts} | 49 +++++++++---------- 1 file changed, 23 insertions(+), 26 deletions(-) rename public/app/features/dashboard/specs/{dashboard_import_ctrl.ts => dashboard_import_ctrl.jest.ts} (57%) diff --git a/public/app/features/dashboard/specs/dashboard_import_ctrl.ts b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts similarity index 57% rename from public/app/features/dashboard/specs/dashboard_import_ctrl.ts rename to public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts index e78109b01a0..3270bb5a732 100644 --- a/public/app/features/dashboard/specs/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/specs/dashboard_import_ctrl.jest.ts @@ -1,25 +1,24 @@ -import {describe, beforeEach, it, sinon, expect, angularMocks} from 'test/lib/common'; - import {DashboardImportCtrl} from '../dashboard_import_ctrl'; -import config from 'app/core/config'; +import config from '../../../core/config'; describe('DashboardImportCtrl', function() { var ctx: any = {}; - var backendSrv = { - search: sinon.stub().returns(Promise.resolve([])), - get: sinon.stub() - }; - beforeEach(angularMocks.module('grafana.core')); + let navModelSrv; + let backendSrv; - beforeEach(angularMocks.inject(($rootScope, $controller, $q) => { - ctx.$q = $q; - ctx.scope = $rootScope.$new(); - ctx.ctrl = $controller(DashboardImportCtrl, { - $scope: ctx.scope, - backendSrv: backendSrv, - }); - })); + beforeEach(() => { + navModelSrv = { + getNav: () => {} + }; + + backendSrv = { + search: jest.fn().mockReturnValue(Promise.resolve([])), + get: jest.fn() + }; + + ctx.ctrl = new DashboardImportCtrl(backendSrv, navModelSrv, {}, {}, {}); + }); describe('when uploading json', function() { beforeEach(function() { @@ -37,13 +36,13 @@ describe('DashboardImportCtrl', function() { }); it('should build input model', function() { - expect(ctx.ctrl.inputs.length).to.eql(1); - expect(ctx.ctrl.inputs[0].name).to.eql('ds'); - expect(ctx.ctrl.inputs[0].info).to.eql('Select a Test DB data source'); + expect(ctx.ctrl.inputs.length).toBe(1); + expect(ctx.ctrl.inputs[0].name).toBe('ds'); + expect(ctx.ctrl.inputs[0].info).toBe('Select a Test DB data source'); }); it('should set inputValid to false', function() { - expect(ctx.ctrl.inputsValid).to.eql(false); + expect(ctx.ctrl.inputsValid).toBe(false); }); }); @@ -51,7 +50,7 @@ describe('DashboardImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = 'http://grafana.com/dashboards/123'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -60,7 +59,7 @@ describe('DashboardImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/123'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/123'); }); }); @@ -68,7 +67,7 @@ describe('DashboardImportCtrl', function() { beforeEach(function() { ctx.ctrl.gnetUrl = '2342'; // setup api mock - backendSrv.get = sinon.spy(() => { + backendSrv.get = jest.fn(() => { return Promise.resolve({ json: {} }); @@ -77,10 +76,8 @@ describe('DashboardImportCtrl', function() { }); it('should call gnet api with correct dashboard id', function() { - expect(backendSrv.get.getCall(0).args[0]).to.eql('api/gnet/dashboards/2342'); + expect(backendSrv.get.mock.calls[0][0]).toBe('api/gnet/dashboards/2342'); }); }); }); - - From 4d7ff4de150a0cafe8349ff9d5ea72ef26c9c9d5 Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Mon, 4 Dec 2017 18:00:22 +0300 Subject: [PATCH 05/24] move import menu item to the original place --- pkg/api/index.go | 3 ++- public/app/core/routes/routes.ts | 10 +++++----- public/app/features/dashboard/dashboard_import_ctrl.ts | 2 +- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/pkg/api/index.go b/pkg/api/index.go index d2069441b81..c1945906624 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -90,11 +90,13 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { if c.OrgRole == m.ROLE_ADMIN || c.OrgRole == m.ROLE_EDITOR { data.NavTree = append(data.NavTree, &dtos.NavLink{ Text: "Create", + Id: "create", Icon: "fa fa-fw fa-plus", 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: "Import", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, }, }) } @@ -103,7 +105,6 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { {Text: "Home", Url: setting.AppSubUrl + "/", Icon: "fa fa-fw fa-home", HideFromTabs: true}, {Divider: true, HideFromTabs: true}, {Text: "Manage", Id: "dashboards", Url: setting.AppSubUrl + "/dashboards", Icon: "fa fa-fw fa-sitemap"}, - {Text: "Import", Id: "import", Url: setting.AppSubUrl + "/dashboards/import", Icon: "gicon gicon-dashboard-import"}, {Text: "Playlists", Id: "playlists", Url: setting.AppSubUrl + "/playlists", Icon: "fa fa-fw fa-film"}, {Text: "Snapshots", Id: "snapshots", Url: setting.AppSubUrl + "/dashboard/snapshots", Icon: "icon-gf icon-gf-fw icon-gf-snapshot"}, } diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index a965cd406d3..d3ef3d51477 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -48,6 +48,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { reloadOnSearch: false, pageClass: 'page-dashboard', }) + .when('/dashboard/import', { + templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', + controller : 'DashboardImportCtrl', + controllerAs: 'ctrl', + }) .when('/datasources', { templateUrl: 'public/app/features/plugins/partials/ds_list.html', controller : 'DataSourcesCtrl', @@ -68,11 +73,6 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DashboardListCtrl', controllerAs: 'ctrl', }) - .when('/dashboards/import', { - templateUrl: 'public/app/features/dashboard/partials/dashboardImport.html', - controller : 'DashboardImportCtrl', - controllerAs: 'ctrl', - }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/features/dashboard/dashboard_import_ctrl.ts b/public/app/features/dashboard/dashboard_import_ctrl.ts index f2b3fb90fc9..9a630681145 100644 --- a/public/app/features/dashboard/dashboard_import_ctrl.ts +++ b/public/app/features/dashboard/dashboard_import_ctrl.ts @@ -16,7 +16,7 @@ export class DashboardImportCtrl { /** @ngInject */ constructor(private backendSrv, navModelSrv, private $location, private $scope, $routeParams) { - this.navModel = navModelSrv.getNav('dashboards', 'import', 0); + this.navModel = navModelSrv.getNav('create', 'import'); this.step = 1; this.nameExists = false; From 9be6f9734d1efcc0880866fc4bc654416aea0cd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Mon, 4 Dec 2017 16:29:46 +0100 Subject: [PATCH 06/24] ux: updated padding --- public/sass/components/_tabbed_view.scss | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/public/sass/components/_tabbed_view.scss b/public/sass/components/_tabbed_view.scss index dac7408d70f..d2c9b24d2a0 100644 --- a/public/sass/components/_tabbed_view.scss +++ b/public/sass/components/_tabbed_view.scss @@ -26,7 +26,7 @@ .tabbed-view-panel-title { float: left; - padding-top: 1rem; + padding-top: 9px; margin: 0 2rem 0 0; } From afc036492c62518f883c0b424b696b8cec893fba Mon Sep 17 00:00:00 2001 From: Alexander Zobnin Date: Tue, 5 Dec 2017 13:46:58 +0300 Subject: [PATCH 07/24] dashboard: migrations for repeat rows (#10070) --- .../features/dashboard/dashboard_migration.ts | 5 ++-- .../specs/dashboard_migration.jest.ts | 26 +++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/public/app/features/dashboard/dashboard_migration.ts b/public/app/features/dashboard/dashboard_migration.ts index 6cc3e37f2f8..95a70cbfcab 100644 --- a/public/app/features/dashboard/dashboard_migration.ts +++ b/public/app/features/dashboard/dashboard_migration.ts @@ -383,8 +383,8 @@ export class DashboardMigrator { return; } - // Add special "row" panels if even one row is collapsed or has visible title - const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle); + // Add special "row" panels if even one row is collapsed, repeated or has visible title + const showRows = _.some(old.rows, (row) => row.collapse || row.showTitle || row.repeat); for (let row of old.rows) { let height: any = row.height || DEFAULT_ROW_HEIGHT; @@ -398,6 +398,7 @@ export class DashboardMigrator { rowPanel.type = 'row'; rowPanel.title = row.title; rowPanel.collapsed = row.collapse; + rowPanel.repeat = row.repeat; rowPanel.panels = []; rowPanel.gridPos = {x: 0, y: yPos, w: GRID_COLUMN_COUNT, h: rowGridHeight}; rowPanelModel = new PanelModel(rowPanel); diff --git a/public/app/features/dashboard/specs/dashboard_migration.jest.ts b/public/app/features/dashboard/specs/dashboard_migration.jest.ts index 2c8b5ebcd50..dfdcb7601a4 100644 --- a/public/app/features/dashboard/specs/dashboard_migration.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_migration.jest.ts @@ -2,6 +2,7 @@ import _ from 'lodash'; import { DashboardModel } from '../dashboard_model'; import { PanelModel } from '../panel_model'; import {GRID_CELL_HEIGHT, GRID_CELL_VMARGIN} from 'app/core/constants'; +import { expect } from 'test/lib/common'; jest.mock('app/core/services/context_srv', () => ({})); @@ -315,12 +316,33 @@ describe('DashboardModel', function() { expect(panelGridPos).toEqual(expectedGrid); }); + + it('should add repeated row if repeat set', function() { + model.rows = [ + createRow({showTitle: true, title: "Row", height: 8, repeat: "server"}, [[6]]), + createRow({height: 8}, [[12]]) + ]; + let dashboard = new DashboardModel(model); + let panelGridPos = getGridPositions(dashboard); + let expectedGrid = [ + {x: 0, y: 0, w: 24, h: 8}, + {x: 0, y: 1, w: 12, h: 8}, + {x: 0, y: 9, w: 24, h: 8}, + {x: 0, y: 10, w: 24, h: 8} + ]; + + expect(panelGridPos).toEqual(expectedGrid); + expect(dashboard.panels[0].repeat).toBe("server"); + expect(dashboard.panels[1].repeat).toBeUndefined(); + expect(dashboard.panels[2].repeat).toBeUndefined(); + expect(dashboard.panels[3].repeat).toBeUndefined(); + }); }); }); function createRow(options, panelDescriptions: any[]) { const PANEL_HEIGHT_STEP = GRID_CELL_HEIGHT + GRID_CELL_VMARGIN; - let {collapse, height, showTitle, title} = options; + let {collapse, height, showTitle, title, repeat} = options; height = height * PANEL_HEIGHT_STEP; let panels = []; _.each(panelDescriptions, panelDesc => { @@ -330,7 +352,7 @@ function createRow(options, panelDescriptions: any[]) { } panels.push(panel); }); - let row = {collapse, height, showTitle, title, panels}; + let row = {collapse, height, showTitle, title, panels, repeat}; return row; } From 90da964a144abc4a601fdca053d2f43bd146a57c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Tue, 5 Dec 2017 12:19:01 +0100 Subject: [PATCH 08/24] refactoring PR #10068 --- pkg/api/index.go | 2 +- .../dashboard/import/dash_import.html | 138 -------------- .../features/dashboard/import/dash_import.ts | 176 ------------------ .../dashboard/partials/dashboardImport.html | 7 +- public/app/features/dashboard/upload.ts | 2 +- 5 files changed, 6 insertions(+), 319 deletions(-) delete mode 100644 public/app/features/dashboard/import/dash_import.html delete mode 100644 public/app/features/dashboard/import/dash_import.ts diff --git a/pkg/api/index.go b/pkg/api/index.go index c1945906624..31e413a7ca3 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -96,7 +96,7 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { 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: "Import", Id: "import", Icon: "gicon gicon-dashboard-import", Url: setting.AppSubUrl + "/dashboard/import"}, + {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/features/dashboard/import/dash_import.html b/public/app/features/dashboard/import/dash_import.html deleted file mode 100644 index c617c752366..00000000000 --- a/public/app/features/dashboard/import/dash_import.html +++ /dev/null @@ -1,138 +0,0 @@ - - - - - - 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 18/24] 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 19/24] 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 20/24] 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 21/24] 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 22/24] 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 f87b9aaa8a3f5e518942eab1a9d8065747980ba1 Mon Sep 17 00:00:00 2001 From: Marcus Efraimsson Date: Wed, 6 Dec 2017 20:34:41 +0100 Subject: [PATCH 23/24] 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 24/24] 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(); + }); + }); + }); });