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.
This commit is contained in:
Marcus Efraimsson 2017-12-06 20:34:41 +01:00
parent fe177f198b
commit f87b9aaa8a
7 changed files with 418 additions and 28 deletions

View File

@ -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();
});
});
});
});

View File

@ -64,18 +64,70 @@ export class SearchCtrl {
this.moveSelection(-1);
}
if (evt.keyCode === 13) {
var selectedDash = this.results[this.selectedIndex];
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;
});
}
}

View File

@ -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"

View File

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

View File

@ -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);

View File

@ -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 || []);
}

View File

@ -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;
}
}