Commit 781349d3 by Daniel Lee Committed by Marcus Efraimsson

dashboard: dashboard search results component. closes #10080

parent d29c695d
...@@ -20,37 +20,12 @@ ...@@ -20,37 +20,12 @@
<div class="search-dropdown"> <div class="search-dropdown">
<div class="search-dropdown__col_1"> <div class="search-dropdown__col_1">
<div class="search-results-container" grafana-scrollbar> <div class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && results.length">No dashboards matching your query were found.</h6> <h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
<div ng-repeat="section in ctrl.results" class="search-section"> results="ctrl.results"
<a class="search-section__header pointer" ng-hide="section.hideHeader" ng-click="ctrl.toggleFolder(section)"> on-tag-selected="ctrl.filterByTag($tag)" />
<i class="search-section__header__icon" ng-class="section.icon"></i>
<span class="search-section__header__text">{{::section.title}}</span>
<i class="fa fa-minus search-section__header__toggle" ng-show="section.expanded"></i>
<i class="fa fa-plus search-section__header__toggle" ng-hide="section.expanded"></i>
</a>
<div ng-if="section.expanded">
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}">
<span class="search-item__icon">
<i class="fa fa-th-large"></i>
</span>
<span class="search-item__body">
<div class="search-item__body-title">{{::item.title}}</div>
<div class="search-item__body-sub-title" ng-show="item.folderTitle && section.hideHeader">
{{::item.folderTitle}}
</div>
</span>
<span class="search-item__tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in item.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
</span>
</a>
</div>
</div> </div>
</div>
</div> </div>
<div class="search-dropdown__col_2"> <div class="search-dropdown__col_2">
......
...@@ -94,13 +94,11 @@ export class SearchCtrl { ...@@ -94,13 +94,11 @@ export class SearchCtrl {
return query.query === '' && query.starred === false && query.tag.length === 0; return query.query === '' && query.starred === false && query.tag.length === 0;
} }
filterByTag(tag, evt) { filterByTag(tag) {
this.query.tag.push(tag); if (_.indexOf(this.query.tag, tag) === -1) {
this.search(); this.query.tag.push(tag);
this.giveSearchFocus = this.giveSearchFocus + 1; this.search();
if (evt) { this.giveSearchFocus = this.giveSearchFocus + 1;
evt.stopPropagation();
evt.preventDefault();
} }
} }
......
<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)">
<div ng-click="ctrl.toggleSelection(section, $event)">
<gf-form-switch
ng-show="ctrl.editable"
on-change="ctrl.selectionChanged($event)"
checked="section.checked"
switch-class="gf-form-switch--search-result__section">
</gf-form-switch>
</div>
<i class="search-section__header__icon" ng-class="section.icon"></i>
<span class="search-section__header__text">{{::section.title}}</span>
<i class="fa fa-minus search-section__header__toggle" ng-show="section.expanded"></i>
<i class="fa fa-plus search-section__header__toggle" ng-hide="section.expanded"></i>
</a>
<div ng-if="section.expanded">
<a ng-repeat="item in section.items" class="search-item" ng-class="{'selected': item.selected}" ng-href="{{::item.url}}">
<div ng-click="ctrl.toggleSelection(item, $event)">
<gf-form-switch
ng-show="ctrl.editable"
on-change="ctrl.selectionChanged()"
checked="item.checked"
switch-class="gf-form-switch--search-result__item">
</gf-form-switch>
</div>
<span class="search-item__icon">
<i class="fa fa-th-large"></i>
</span>
<span class="search-item__body">
<div class="search-item__body-title">{{::item.title}}</div>
<div class="search-item__body-sub-title" ng-show="item.folderTitle && section.hideHeader">
{{::item.folderTitle}}
</div>
</span>
<span class="search-item__tags">
<span ng-click="ctrl.selectTag(tag, $event)" ng-repeat="tag in item.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
</span>
</a>
</div>
</div>
\ No newline at end of file
import { SearchResultsCtrl } from './search_results';
describe('SearchResultsCtrl', () => {
let ctrl;
describe('when checking an item that is not checked', () => {
let item = {checked: false};
let selectionChanged = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl();
ctrl.onSelectionChanged = () => selectionChanged = true;
ctrl.toggleSelection(item);
});
it('should set checked to true', () => {
expect(item.checked).toBeTruthy();
});
it('should trigger selection changed callback', () => {
expect(selectionChanged).toBeTruthy();
});
});
describe('when checking an item that is checked', () => {
let item = {checked: true};
let selectionChanged = false;
beforeEach(() => {
ctrl = new SearchResultsCtrl();
ctrl.onSelectionChanged = () => selectionChanged = true;
ctrl.toggleSelection(item);
});
it('should set checked to false', () => {
expect(item.checked).toBeFalsy();
});
it('should trigger selection changed callback', () => {
expect(selectionChanged).toBeTruthy();
});
});
describe('when selecting a tag', () => {
let selectedTag = null;
beforeEach(() => {
ctrl = new SearchResultsCtrl();
ctrl.onTagSelected = (tag) => selectedTag = tag;
ctrl.selectTag('tag-test');
});
it('should trigger tag selected callback', () => {
expect(selectedTag["$tag"]).toBe('tag-test');
});
});
describe('when toggle a folder', () => {
let folderToggled = false;
let folder = {
toggle: () => {
folderToggled = true;
}
};
beforeEach(() => {
ctrl = new SearchResultsCtrl();
ctrl.toggleFolderExpand(folder);
});
it('should trigger folder toggle callback', () => {
expect(folderToggled).toBeTruthy();
});
});
});
// import _ from 'lodash';
import coreModule from '../../core_module';
export class SearchResultsCtrl {
results: any;
onSelectionChanged: any;
onTagSelected: any;
toggleFolderExpand(section) {
if (section.toggle) {
section.toggle(section);
}
}
toggleSelection(item, evt) {
item.checked = !item.checked;
if (this.onSelectionChanged) {
this.onSelectionChanged();
}
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
}
selectTag(tag, evt) {
if (this.onTagSelected) {
this.onTagSelected({$tag: tag});
}
if (evt) {
evt.stopPropagation();
evt.preventDefault();
}
}
}
export function searchResultsDirective() {
return {
restrict: 'E',
templateUrl: 'public/app/core/components/search/search_results.html',
controller: SearchResultsCtrl,
bindToController: true,
controllerAs: 'ctrl',
scope: {
editable: '@',
results: '=',
onSelectionChanged: '&',
onTagSelected: '&'
},
};
}
coreModule.directive('dashboardSearchResults', searchResultsDirective);
...@@ -52,6 +52,7 @@ import {gfPageDirective} from './components/gf_page'; ...@@ -52,6 +52,7 @@ import {gfPageDirective} from './components/gf_page';
import {orgSwitcher} from './components/org_switcher'; import {orgSwitcher} from './components/org_switcher';
import {profiler} from './profiler'; import {profiler} from './profiler';
import {registerAngularDirectives} from './angular_wrappers'; import {registerAngularDirectives} from './angular_wrappers';
import {searchResultsDirective} from './components/search/search_results';
export { export {
profiler, profiler,
...@@ -83,5 +84,6 @@ export { ...@@ -83,5 +84,6 @@ export {
userGroupPicker, userGroupPicker,
geminiScrollbar, geminiScrollbar,
gfPageDirective, gfPageDirective,
orgSwitcher orgSwitcher,
searchResultsDirective
}; };
...@@ -128,14 +128,20 @@ export class SearchSrv { ...@@ -128,14 +128,20 @@ export class SearchSrv {
}); });
} }
private browse() { private browse(options) {
let sections: any = {}; let sections: any = {};
let promises = [ let promises = [];
this.getRecentDashboards(sections),
this.getStarred(sections), if (!options.skipRecent) {
this.getDashboardsAndFolders(sections), promises.push(this.getRecentDashboards(sections));
]; }
if (!options.skipStarred) {
promises.push(this.getStarred(sections));
}
promises.push(this.getDashboardsAndFolders(sections));
return this.$q.all(promises).then(() => { return this.$q.all(promises).then(() => {
return _.sortBy(_.values(sections), 'score'); return _.sortBy(_.values(sections), 'score');
...@@ -149,7 +155,7 @@ export class SearchSrv { ...@@ -149,7 +155,7 @@ export class SearchSrv {
search(options) { search(options) {
if (!options.query && (!options.tag || options.tag.length === 0) && !options.starred) { if (!options.query && (!options.tag || options.tag.length === 0) && !options.starred) {
return this.browse(); return this.browse(options);
} }
let query = _.clone(options); let query = _.clone(options);
...@@ -157,6 +163,10 @@ export class SearchSrv { ...@@ -157,6 +163,10 @@ export class SearchSrv {
query.type = 'dash-db'; query.type = 'dash-db';
return this.backendSrv.search(query).then(results => { return this.backendSrv.search(query).then(results => {
if (results.length === 0) {
return results;
}
let section = { let section = {
hideHeader: true, hideHeader: true,
items: [], items: [],
......
...@@ -2,6 +2,7 @@ import { SearchSrv } from 'app/core/services/search_srv'; ...@@ -2,6 +2,7 @@ import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrvMock } from 'test/mocks/backend_srv'; import { BackendSrvMock } from 'test/mocks/backend_srv';
import impressionSrv from 'app/core/services/impression_srv'; import impressionSrv from 'app/core/services/impression_srv';
import { contextSrv } from 'app/core/services/context_srv'; import { contextSrv } from 'app/core/services/context_srv';
import { beforeEach } from 'test/lib/common';
jest.mock('app/core/store', () => { jest.mock('app/core/store', () => {
return { return {
...@@ -244,4 +245,43 @@ describe('SearchSrv', () => { ...@@ -244,4 +245,43 @@ describe('SearchSrv', () => {
expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true); expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true);
}); });
}); });
describe('when skipping recent dashboards', () => {
let getRecentDashboardsCalled = false;
beforeEach(() => {
backendSrvMock.search = jest.fn();
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
searchSrv.getRecentDashboards = () => {
getRecentDashboardsCalled = true;
};
return searchSrv.search({ skipRecent: true }).then(() => {});
});
it('should not fetch recent dashboards', () => {
expect(getRecentDashboardsCalled).toBeFalsy();
});
});
describe('when skipping starred dashboards', () => {
let getStarredCalled = false;
beforeEach(() => {
backendSrvMock.search = jest.fn();
backendSrvMock.search.mockReturnValue(Promise.resolve([]));
impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]);
searchSrv.getStarred = () => {
getStarredCalled = true;
};
return searchSrv.search({ skipStarred: true }).then(() => {});
});
it('should not fetch starred dashboards', () => {
expect(getStarredCalled).toBeFalsy();
});
});
}); });
...@@ -18,7 +18,7 @@ export class DashboardListCtrl { ...@@ -18,7 +18,7 @@ export class DashboardListCtrl {
/** @ngInject */ /** @ngInject */
constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) { constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) {
this.navModel = navModelSrv.getNav('dashboards', 'dashboards', 0); this.navModel = navModelSrv.getNav('dashboards', 'dashboards', 0);
this.query = {query: '', mode: 'tree', tag: [], starred: false}; this.query = {query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true};
this.selectedStarredFilter = this.starredFilterOptions[0]; this.selectedStarredFilter = this.starredFilterOptions[0];
this.getDashboards().then(() => { this.getDashboards().then(() => {
...@@ -148,11 +148,9 @@ export class DashboardListCtrl { ...@@ -148,11 +148,9 @@ export class DashboardListCtrl {
}); });
} }
filterByTag(tag, evt) { filterByTag(tag) {
this.query.tag.push(tag); if (_.indexOf(this.query.tag, tag) === -1) {
if (evt) { this.query.tag.push(tag);
evt.stopPropagation();
evt.preventDefault();
} }
return this.getDashboards(); return this.getDashboards();
...@@ -163,9 +161,9 @@ export class DashboardListCtrl { ...@@ -163,9 +161,9 @@ export class DashboardListCtrl {
} }
onTagFilterChange() { onTagFilterChange() {
this.query.tag.push(this.selectedTagFilter.term); var res = this.filterByTag(this.selectedTagFilter.term);
this.selectedTagFilter = this.tagFilterOptions[0]; this.selectedTagFilter = this.tagFilterOptions[0];
return this.getDashboards(); return res;
} }
removeTag(tag, evt) { removeTag(tag, evt) {
......
...@@ -78,54 +78,13 @@ ...@@ -78,54 +78,13 @@
/> />
</div> </div>
</div> </div>
<div class="search-results-container" ng-show="ctrl.sections.length > 0" grafana-scrollbar> <div class="search-results-container">
<div ng-repeat="section in ctrl.sections" class="search-section"> <h6 ng-show="ctrl.sections.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
<div class="search-section__header__with-checkbox" ng-hide="section.hideHeader"> results="ctrl.sections"
<gf-form-switch editable="true"
on-change="ctrl.selectionChanged()" on-selection-changed="ctrl.selectionChanged()"
checked="section.checked"> on-tag-selected="ctrl.filterByTag($tag)" />
</gf-form-switch>
<a class="search-section__header pointer" ng-click="ctrl.toggleFolder(section)" ng-hide="section.hideHeader">
<i class="search-section__header__icon" ng-class="section.icon"></i>
<span class="search-section__header__text">{{::section.title}}</span>
<i class="fa fa-minus search-section__header__toggle" ng-show="section.expanded"></i>
<i class="fa fa-plus search-section__header__toggle" ng-hide="section.expanded"></i>
</a>
</div>
<div ng-if="section.expanded">
<div ng-repeat="item in section.items" class="search-item__with-checkbox" ng-class="{'selected': item.selected}">
<gf-form-switch
on-change="ctrl.selectionChanged()"
checked="item.checked" />
<a ng-href="{{::item.url}}" class="search-item">
<span class="search-item__icon">
<i class="fa fa-th-large"></i>
</span>
<span class="search-item__body">
<div class="search-item__body-title">{{::item.title}}</div>
<div class="search-item__body-sub-title" ng-show="item.folderTitle && section.hideHeader">
<i class="fa fa-folder-o"></i>
{{::item.folderTitle}}
</div>
</span>
<span class="search-item__tags">
<span ng-click="ctrl.filterByTag(tag, $event)" ng-repeat="tag in item.tags" tag-color-from-name="tag" class="label label-tag">
{{tag}}
</span>
</span>
<span class="search-item__actions">
<i class="fa" ng-class="{'fa-star': item.isStarred, 'fa-star-o': !item.isStarred}"></i>
</span>
</a>
</div>
</div>
</div>
</div> </div>
</div> </div>
</div> </div>
<em class="muted" ng-hide="ctrl.sections.length > 0">
No Dashboards or Folders found.
</em>
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
.search-results-container { .search-results-container {
padding-left: 0; padding-left: 0;
padding-right: 0;
} }
} }
......
...@@ -129,12 +129,8 @@ ...@@ -129,12 +129,8 @@
} }
} }
.search-section__header__with-checkbox {
display: flex;
}
.search-section__header__icon { .search-section__header__icon {
padding: 5px 10px; padding: 2px 10px;
} }
.search-section__header__toggle { .search-section__header__toggle {
...@@ -145,14 +141,6 @@ ...@@ -145,14 +141,6 @@
flex-grow: 1; flex-grow: 1;
} }
.search-item__with-checkbox {
display: flex;
.search-item {
margin: 1px 3px;
}
}
.search-item { .search-item {
@include list-item(); @include list-item();
@include left-brand-border(); @include left-brand-border();
......
...@@ -102,6 +102,73 @@ $switch-height: 1.5rem; ...@@ -102,6 +102,73 @@ $switch-height: 1.5rem;
} }
} }
.gf-form-switch--search-result__section, .gf-form-switch--search-result__item {
min-width: 2.6rem;
input + label {
background-color: inherit;
height: 1.7rem;
}
}
.gf-form-switch--search-result__section {
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
}
}
.gf-form-switch--search-result__item {
input + label {
height: 2.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
}
}
gf-form-switch[disabled] { gf-form-switch[disabled] {
.gf-form-label, .gf-form-label,
.gf-form-switch input + label { .gf-form-switch input + label {
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment