Commit 781349d3 by Daniel Lee Committed by Marcus Efraimsson

dashboard: dashboard search results component. closes #10080

parent d29c695d
......@@ -20,37 +20,12 @@
<div class="search-dropdown">
<div class="search-dropdown__col_1">
<div class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && results.length">No dashboards matching your query were found.</h6>
<div ng-repeat="section in ctrl.results" class="search-section">
<a class="search-section__header pointer" ng-hide="section.hideHeader" ng-click="ctrl.toggleFolder(section)">
<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 class="search-results-container" grafana-scrollbar>
<h6 ng-show="!ctrl.isLoading && ctrl.results.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
results="ctrl.results"
on-tag-selected="ctrl.filterByTag($tag)" />
</div>
</div>
</div>
<div class="search-dropdown__col_2">
......
......@@ -94,13 +94,11 @@ export class SearchCtrl {
return query.query === '' && query.starred === false && query.tag.length === 0;
}
filterByTag(tag, evt) {
this.query.tag.push(tag);
this.search();
this.giveSearchFocus = this.giveSearchFocus + 1;
if (evt) {
evt.stopPropagation();
evt.preventDefault();
filterByTag(tag) {
if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag);
this.search();
this.giveSearchFocus = this.giveSearchFocus + 1;
}
}
......
<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';
import {orgSwitcher} from './components/org_switcher';
import {profiler} from './profiler';
import {registerAngularDirectives} from './angular_wrappers';
import {searchResultsDirective} from './components/search/search_results';
export {
profiler,
......@@ -83,5 +84,6 @@ export {
userGroupPicker,
geminiScrollbar,
gfPageDirective,
orgSwitcher
orgSwitcher,
searchResultsDirective
};
......@@ -128,14 +128,20 @@ export class SearchSrv {
});
}
private browse() {
private browse(options) {
let sections: any = {};
let promises = [
this.getRecentDashboards(sections),
this.getStarred(sections),
this.getDashboardsAndFolders(sections),
];
let promises = [];
if (!options.skipRecent) {
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 _.sortBy(_.values(sections), 'score');
......@@ -149,7 +155,7 @@ export class SearchSrv {
search(options) {
if (!options.query && (!options.tag || options.tag.length === 0) && !options.starred) {
return this.browse();
return this.browse(options);
}
let query = _.clone(options);
......@@ -157,6 +163,10 @@ export class SearchSrv {
query.type = 'dash-db';
return this.backendSrv.search(query).then(results => {
if (results.length === 0) {
return results;
}
let section = {
hideHeader: true,
items: [],
......
......@@ -2,6 +2,7 @@ import { SearchSrv } from 'app/core/services/search_srv';
import { BackendSrvMock } from 'test/mocks/backend_srv';
import impressionSrv from 'app/core/services/impression_srv';
import { contextSrv } from 'app/core/services/context_srv';
import { beforeEach } from 'test/lib/common';
jest.mock('app/core/store', () => {
return {
......@@ -244,4 +245,43 @@ describe('SearchSrv', () => {
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 {
/** @ngInject */
constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) {
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.getDashboards().then(() => {
......@@ -148,11 +148,9 @@ export class DashboardListCtrl {
});
}
filterByTag(tag, evt) {
this.query.tag.push(tag);
if (evt) {
evt.stopPropagation();
evt.preventDefault();
filterByTag(tag) {
if (_.indexOf(this.query.tag, tag) === -1) {
this.query.tag.push(tag);
}
return this.getDashboards();
......@@ -163,9 +161,9 @@ export class DashboardListCtrl {
}
onTagFilterChange() {
this.query.tag.push(this.selectedTagFilter.term);
var res = this.filterByTag(this.selectedTagFilter.term);
this.selectedTagFilter = this.tagFilterOptions[0];
return this.getDashboards();
return res;
}
removeTag(tag, evt) {
......
......@@ -78,54 +78,13 @@
/>
</div>
</div>
<div class="search-results-container" ng-show="ctrl.sections.length > 0" grafana-scrollbar>
<div ng-repeat="section in ctrl.sections" class="search-section">
<div class="search-section__header__with-checkbox" ng-hide="section.hideHeader">
<gf-form-switch
on-change="ctrl.selectionChanged()"
checked="section.checked">
</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 class="search-results-container">
<h6 ng-show="ctrl.sections.length === 0">No dashboards matching your query were found.</h6>
<dashboard-search-results
results="ctrl.sections"
editable="true"
on-selection-changed="ctrl.selectionChanged()"
on-tag-selected="ctrl.filterByTag($tag)" />
</div>
</div>
</div>
<em class="muted" ng-hide="ctrl.sections.length > 0">
No Dashboards or Folders found.
</em>
......@@ -3,6 +3,7 @@
.search-results-container {
padding-left: 0;
padding-right: 0;
}
}
......
......@@ -129,12 +129,8 @@
}
}
.search-section__header__with-checkbox {
display: flex;
}
.search-section__header__icon {
padding: 5px 10px;
padding: 2px 10px;
}
.search-section__header__toggle {
......@@ -145,14 +141,6 @@
flex-grow: 1;
}
.search-item__with-checkbox {
display: flex;
.search-item {
margin: 1px 3px;
}
}
.search-item {
@include list-item();
@include left-brand-border();
......
......@@ -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-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