mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
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:
parent
fe177f198b
commit
f87b9aaa8a
330
public/app/core/components/search/search.jest.ts
Normal file
330
public/app/core/components/search/search.jest.ts
Normal 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();
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
@ -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;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -201,10 +201,6 @@ export class SearchSrv {
|
||||
});
|
||||
}
|
||||
|
||||
toggleSection(section) {
|
||||
section.toggle(section);
|
||||
}
|
||||
|
||||
getDashboardTags() {
|
||||
return this.backendSrv.get('/api/dashboards/tags');
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -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 || []);
|
||||
}
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user