mirror of
https://github.com/grafana/grafana.git
synced 2025-01-08 15:13:30 -06:00
dashboard: dashboard search results component. closes #10080
This commit is contained in:
parent
d29c695d44
commit
781349d360
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
43
public/app/core/components/search/search_results.html
Normal file
43
public/app/core/components/search/search_results.html
Normal file
@ -0,0 +1,43 @@
|
||||
<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>
|
75
public/app/core/components/search/search_results.jest.ts
Normal file
75
public/app/core/components/search/search_results.jest.ts
Normal file
@ -0,0 +1,75 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
});
|
56
public/app/core/components/search/search_results.ts
Normal file
56
public/app/core/components/search/search_results.ts
Normal file
@ -0,0 +1,56 @@
|
||||
// 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 {
|
||||
|
Loading…
Reference in New Issue
Block a user