Просмотр исходного кода

dashboard: dashboard search results component. closes #10080

Daniel Lee 8 лет назад
Родитель
Сommit
781349d360

+ 5 - 30
public/app/core/components/search/search.html

@@ -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>
-        <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>
     </div>
 
 
     <div class="search-dropdown__col_2">
     <div class="search-dropdown__col_2">

+ 5 - 7
public/app/core/components/search/search.ts

@@ -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) {
-    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 - 0
public/app/core/components/search/search_results.html

@@ -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 - 0
public/app/core/components/search/search_results.jest.ts

@@ -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 - 0
public/app/core/components/search/search_results.ts

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

+ 3 - 1
public/app/core/core.ts

@@ -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 {
   userGroupPicker,
   userGroupPicker,
   geminiScrollbar,
   geminiScrollbar,
   gfPageDirective,
   gfPageDirective,
-  orgSwitcher
+  orgSwitcher,
+  searchResultsDirective
 };
 };

+ 17 - 7
public/app/core/services/search_srv.ts

@@ -128,14 +128,20 @@ export class SearchSrv {
     });
     });
   }
   }
 
 
-  private browse() {
+  private browse(options) {
     let sections: any = {};
     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 this.$q.all(promises).then(() => {
       return _.sortBy(_.values(sections), 'score');
       return _.sortBy(_.values(sections), 'score');
@@ -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 {
     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: [],

+ 40 - 0
public/app/core/specs/search_srv.jest.ts

@@ -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', () => {
       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();
+    });
+  });
 });
 });

+ 6 - 8
public/app/features/dashboard/dashboard_list_ctrl.ts

@@ -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 {
     });
     });
   }
   }
 
 
-  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();
     return this.getDashboards();
@@ -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) {

+ 7 - 48
public/app/features/dashboard/partials/dashboardList.html

@@ -78,54 +78,13 @@
         />
         />
       </div>
       </div>
     </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>
   </div>
 </div>
 </div>
-
-<em class="muted" ng-hide="ctrl.sections.length > 0">
-  No Dashboards or Folders found.
-</em>

+ 1 - 0
public/sass/components/_dashboard_list.scss

@@ -3,6 +3,7 @@
 
 
     .search-results-container {
     .search-results-container {
         padding-left: 0;
         padding-left: 0;
+        padding-right: 0;
     }
     }
 }
 }
 
 

+ 1 - 13
public/sass/components/_search.scss

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

+ 67 - 0
public/sass/components/_switch.scss

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