Просмотр исходного кода

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.
Marcus Efraimsson 8 лет назад
Родитель
Сommit
f87b9aaa8a

+ 330 - 0
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();
+            });
+        });
+    });
+});

+ 83 - 10
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;
+    });
   }
 }
 

+ 1 - 1
public/app/core/components/search/search_results.html

@@ -1,5 +1,5 @@
 <div ng-repeat="section in ctrl.results" class="search-section">
-  <a class="search-section__header pointer" ng-hide="section.hideHeader" ng-click="ctrl.toggleFolderExpand(section)">
+  <a class="search-section__header pointer" ng-hide="section.hideHeader" ng-class="{'selected': section.selected}" ng-click="ctrl.toggleFolderExpand(section)">
     <div ng-click="ctrl.toggleSelection(section, $event)">
       <gf-form-switch
         ng-show="ctrl.editable"

+ 0 - 4
public/app/core/services/search_srv.ts

@@ -201,10 +201,6 @@ export class SearchSrv {
     });
   }
 
-  toggleSection(section) {
-    section.toggle(section);
-  }
-
   getDashboardTags() {
     return this.backendSrv.get('/api/dashboards/tags');
   }

+ 0 - 4
public/app/features/dashboard/dashboard_list_ctrl.ts

@@ -150,10 +150,6 @@ export class DashboardListCtrl {
     });
   }
 
-  toggleFolder(section) {
-    return this.searchSrv.toggleSection(section);
-  }
-
   getTags() {
     return this.searchSrv.getDashboardTags().then((results) => {
       this.tagFilterOptions =  [{ term: 'Filter By Tag', disabled: true }].concat(results);

+ 0 - 3
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 || []);
     }

+ 4 - 6
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;
   }
 }