diff --git a/pkg/services/search/models.go b/pkg/services/search/models.go index b0955593573..dbd4f21a71e 100644 --- a/pkg/services/search/models.go +++ b/pkg/services/search/models.go @@ -63,6 +63,7 @@ type FindPersistedDashboardsQuery struct { FolderIds []int64 Tags []string Limit int + IsBrowse bool Result HitList } diff --git a/public/app/core/components/search/search.ts b/public/app/core/components/search/search.ts index 80ad0a70b12..c7ac381c7ce 100644 --- a/public/app/core/components/search/search.ts +++ b/public/app/core/components/search/search.ts @@ -151,7 +151,7 @@ export class SearchCtrl { } toggleFolder(section) { - this.searchSrv.toggleFolder(section); + this.searchSrv.toggleSection(section); } } diff --git a/public/app/features/dashboard/impression_store.ts b/public/app/core/services/impression_srv.ts similarity index 90% rename from public/app/features/dashboard/impression_store.ts rename to public/app/core/services/impression_srv.ts index 68478aef09a..9d2613d7372 100644 --- a/public/app/features/dashboard/impression_store.ts +++ b/public/app/core/services/impression_srv.ts @@ -2,7 +2,7 @@ import store from 'app/core/store'; import _ from 'lodash'; import config from 'app/core/config'; -export class ImpressionsStore { +export class ImpressionSrv { constructor() {} addDashboardImpression(dashboardId) { @@ -44,8 +44,5 @@ export class ImpressionsStore { } } -var impressions = new ImpressionsStore(); - -export { - impressions -}; +const impressionSrv = new ImpressionSrv(); +export default impressionSrv; diff --git a/public/app/core/services/search_srv.ts b/public/app/core/services/search_srv.ts index 64843c5a6c0..71ca931b423 100644 --- a/public/app/core/services/search_srv.ts +++ b/public/app/core/services/search_srv.ts @@ -1,23 +1,87 @@ import _ from 'lodash'; import coreModule from 'app/core/core_module'; +import impressionSrv from 'app/core/services/impression_srv'; +import store from 'app/core/store'; export class SearchSrv { + recentIsOpen: boolean; + starredIsOpen: boolean; /** @ngInject */ - constructor(private backendSrv) { + constructor(private backendSrv, private $q) { + this.recentIsOpen = store.getBool('search.sections.recent', true); + this.starredIsOpen = store.getBool('search.sections.starred', true); } - browse() { + private getRecentDashboards(sections) { + return this.queryForRecentDashboards().then(result => { + if (result.length > 0) { + sections['recent'] = { + title: 'Recent Boards', + icon: 'fa fa-clock-o', + score: -1, + expanded: this.recentIsOpen, + toggle: this.toggleRecent.bind(this), + items: result, + }; + } + }); + } + + private queryForRecentDashboards() { + var dashIds = _.take(impressionSrv.getDashboardOpened(), 5); + if (dashIds.length === 0) { + return Promise.resolve([]); + } + + return this.backendSrv.search({ dashboardIds: dashIds }).then(result => { + return dashIds.map(orderId => { + return this.transformToViewModel(_.find(result, { id: orderId })); + }).filter(item => !item.isStarred); + }); + } + + private toggleRecent(section) { + this.recentIsOpen = section.expanded = !section.expanded; + store.set('search.sections.recent', this.recentIsOpen); + + if (!section.expanded || section.items.length) { + return; + } + + return this.queryForRecentDashboards().then(result => { + section.items = result; + }); + } + + private toggleStarred(section) { + this.starredIsOpen = section.expanded = !section.expanded; + store.set('search.sections.starred', this.starredIsOpen); + } + + private getStarred(sections) { + return this.backendSrv.search({starred: true, limit: 5}).then(result => { + if (result.length > 0) { + sections['starred'] = { + title: 'Starred Boards', + icon: 'fa fa-star-o', + score: -2, + expanded: this.starredIsOpen, + toggle: this.toggleStarred.bind(this), + items: this.transformToViewModel(result), + }; + } + }); + } + + private getDashboardsAndFolders(sections) { const rootFolderId = 0; let query = { - folderIds: [rootFolderId] + folderIds: [rootFolderId], }; return this.backendSrv.search(query).then(results => { - - let sections: any = {}; - for (let hit of results) { if (hit.type === 'dash-folder') { sections[hit.id] = { @@ -26,7 +90,8 @@ export class SearchSrv { items: [], icon: 'fa fa-folder', score: _.keys(sections).length, - uri: hit.uri + uri: hit.uri, + toggle: this.toggleFolder.bind(this), }; } } @@ -37,7 +102,7 @@ export class SearchSrv { items: [], icon: 'fa fa-folder-open', score: _.keys(sections).length, - expanded: true + expanded: true, }; for (let hit of results) { @@ -45,18 +110,36 @@ export class SearchSrv { continue; } let section = sections[hit.folderId || 0]; - hit.url = 'dashboard/' + hit.uri; - section.items.push(hit); + if (section) { + section.items.push(this.transformToViewModel(hit)); + } else { + console.log('Error: dashboard returned from browse search but not folder', hit.id, hit.folderId); + } } + }); + } + private browse() { + let sections: any = {}; + + let promises = [ + this.getRecentDashboards(sections), + this.getStarred(sections), + this.getDashboardsAndFolders(sections), + ]; + + return this.$q.all(promises).then(() => { return _.sortBy(_.values(sections), 'score'); }); } + private transformToViewModel(hit) { + hit.url = 'dashboard/' + hit.uri; + return hit; + } + 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(); } @@ -65,7 +148,6 @@ export class SearchSrv { query.type = 'dash-db'; return this.backendSrv.search(query).then(results => { - let section = { hideHeader: true, items: [], @@ -76,15 +158,14 @@ export class SearchSrv { if (hit.type === 'dash-folder') { continue; } - hit.url = 'dashboard/' + hit.uri; - section.items.push(hit); + section.items.push(this.transformToViewModel(hit)); } return [section]; }); } - toggleFolder(section) { + private toggleFolder(section) { section.expanded = !section.expanded; section.icon = section.expanded ? 'fa fa-folder-open' : 'fa fa-folder'; @@ -93,17 +174,18 @@ export class SearchSrv { } let query = { - folderIds: [section.id] + folderIds: [section.id], }; return this.backendSrv.search(query).then(results => { - for (let hit of results) { - hit.url = 'dashboard/' + hit.uri; - section.items.push(hit); - } + section.items = _.map(results, this.transformToViewModel); }); } + toggleSection(section) { + section.toggle(section); + } + getDashboardTags() { return this.backendSrv.get('/api/dashboards/tags'); } diff --git a/public/app/core/specs/search_srv.jest.ts b/public/app/core/specs/search_srv.jest.ts index f27a7fd8c21..b6425557eb3 100644 --- a/public/app/core/specs/search_srv.jest.ts +++ b/public/app/core/specs/search_srv.jest.ts @@ -1,85 +1,188 @@ import { SearchSrv } from 'app/core/services/search_srv'; import { BackendSrvMock } from 'test/mocks/backend_srv'; +import impressionSrv from 'app/core/services/impression_srv'; + +jest.mock('app/core/store', () => { + return { + getBool: jest.fn(), + set: jest.fn(), + }; +}); + +jest.mock('app/core/services/impression_srv', () => { + return { + getDashboardOpened: jest.fn, + }; +}); describe('SearchSrv', () => { let searchSrv, backendSrvMock; beforeEach(() => { backendSrvMock = new BackendSrvMock(); - searchSrv = new SearchSrv(backendSrvMock); + searchSrv = new SearchSrv(backendSrvMock, Promise); + + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([]); }); - describe("with no query string and dashboards with folders returned", () => { + describe('With recent dashboards', () => { let results; beforeEach(() => { - backendSrvMock.search = jest.fn().mockReturnValue(Promise.resolve([ - { - title: 'folder1', - type: 'dash-folder', - id: 1, - }, - { - title: 'dash with no folder', - type: 'dash-db', - id: 2, - }, - { - title: 'dash in folder1 1', - type: 'dash-db', - id: 3, - folderId: 1 - }, - { - title: 'dash in folder1 2', - type: 'dash-db', - id: 4, - folderId: 1 - }, - ])); + backendSrvMock.search = jest + .fn() + .mockReturnValueOnce( + Promise.resolve([{ id: 2, title: 'second but first' }, { id: 1, title: 'first but second' }]), + ) + .mockReturnValue(Promise.resolve([])); - return searchSrv.search({query: ''}).then(res => { + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1, 2]); + + return searchSrv.search({ query: '' }).then(res => { results = res; }); }); - it("should create sections for each folder and root", () => { + it('should include recent dashboards section', () => { + expect(results[0].title).toBe('Recent Boards'); + }); + + it('should return order decided by impressions store not api', () => { + expect(results[0].items[0].title).toBe('first but second'); + expect(results[0].items[1].title).toBe('second but first'); + }); + }); + + describe('With starred dashboards', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest + .fn() + .mockReturnValue(Promise.resolve([ + {id: 1, title: 'starred'} + ])); + + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should include starred dashboards section', () => { + expect(results[0].title).toBe('Starred Boards'); + expect(results[0].items.length).toBe(1); + }); + }); + + describe('With starred dashboards and recent', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest + .fn() + .mockReturnValueOnce(Promise.resolve([ + {id: 1, title: 'starred and recent', isStarred: true}, + {id: 2, title: 'recent'} + ])) + .mockReturnValue(Promise.resolve([ + {id: 1, title: 'starred and recent'} + ])); + + impressionSrv.getDashboardOpened = jest.fn().mockReturnValue([1,2]); + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should not show starred in recent', () => { + expect(results[1].title).toBe('Recent Boards'); + expect(results[1].items[0].title).toBe('recent'); + }); + + it('should show starred', () => { + expect(results[0].title).toBe('Starred Boards'); + expect(results[0].items[0].title).toBe('starred and recent'); + }); + + }); + + describe('with no query string and dashboards with folders returned', () => { + let results; + + beforeEach(() => { + backendSrvMock.search = jest + .fn() + .mockReturnValueOnce(Promise.resolve([])) + .mockReturnValue( + Promise.resolve([ + { + title: 'folder1', + type: 'dash-folder', + id: 1, + }, + { + title: 'dash with no folder', + type: 'dash-db', + id: 2, + }, + { + title: 'dash in folder1 1', + type: 'dash-db', + id: 3, + folderId: 1, + }, + { + title: 'dash in folder1 2', + type: 'dash-db', + id: 4, + folderId: 1, + }, + ]), + ); + + return searchSrv.search({ query: '' }).then(res => { + results = res; + }); + }); + + it('should create sections for each folder and root', () => { expect(results).toHaveLength(2); }); it('should place folders first', () => { expect(results[0].title).toBe('folder1'); }); - }); - describe("with query string and dashboards with folders returned", () => { + describe('with query string and dashboards with folders returned', () => { let results; beforeEach(() => { backendSrvMock.search = jest.fn(); - backendSrvMock.search.mockReturnValue(Promise.resolve([ - { - id: 2, - title: 'dash with no folder', - type: 'dash-db', - }, - { - id: 3, - title: 'dash in folder1 1', - type: 'dash-db', - folderId: 1, - folderTitle: 'folder1', - }, - ])); + backendSrvMock.search.mockReturnValue( + Promise.resolve([ + { + id: 2, + title: 'dash with no folder', + type: 'dash-db', + }, + { + id: 3, + title: 'dash in folder1 1', + type: 'dash-db', + folderId: 1, + folderTitle: 'folder1', + }, + ]), + ); - return searchSrv.search({query: 'search'}).then(res => { + return searchSrv.search({ query: 'search' }).then(res => { results = res; }); }); - it("should not specify folder ids", () => { + it('should not specify folder ids', () => { expect(backendSrvMock.search.mock.calls[0][0].folderIds).toHaveLength(0); }); @@ -87,33 +190,31 @@ describe('SearchSrv', () => { expect(results).toHaveLength(1); expect(results[0].hideHeader).toBe(true); }); - }); - describe("with tags", () => { + describe('with tags', () => { beforeEach(() => { backendSrvMock.search = jest.fn(); backendSrvMock.search.mockReturnValue(Promise.resolve([])); - return searchSrv.search({tag: ['atag']}).then(() => {}); + return searchSrv.search({ tag: ['atag'] }).then(() => {}); }); - it("should send tags query to backend search", () => { + it('should send tags query to backend search', () => { expect(backendSrvMock.search.mock.calls[0][0].tag).toHaveLength(1); }); }); - describe("with starred", () => { + describe('with starred', () => { beforeEach(() => { backendSrvMock.search = jest.fn(); backendSrvMock.search.mockReturnValue(Promise.resolve([])); - return searchSrv.search({starred: true}).then(() => {}); + return searchSrv.search({ starred: true }).then(() => {}); }); - it("should send starred query to backend search", () => { + it('should send starred query to backend search', () => { expect(backendSrvMock.search.mock.calls[0][0].starred).toEqual(true); }); }); - }); diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts index 220f2b29081..fd36b52b4d4 100644 --- a/public/app/features/dashboard/all.ts +++ b/public/app/features/dashboard/all.ts @@ -1,4 +1,3 @@ - import './dashboard_ctrl'; import './alerting_srv'; import './history/history'; @@ -15,7 +14,6 @@ import './time_srv'; import './unsavedChangesSrv'; import './unsaved_changes_modal'; import './timepicker/timepicker'; -import './impression_store'; import './upload'; import './import/dash_import'; import './export/export_modal'; diff --git a/public/app/features/dashboard/dashboardLoaderSrv.js b/public/app/features/dashboard/dashboardLoaderSrv.js index 64ef3d4add4..588df632e9b 100644 --- a/public/app/features/dashboard/dashboardLoaderSrv.js +++ b/public/app/features/dashboard/dashboardLoaderSrv.js @@ -5,12 +5,13 @@ define([ 'jquery', 'app/core/utils/kbn', 'app/core/utils/datemath', - './impression_store' + 'app/core/services/impression_srv' ], -function (angular, moment, _, $, kbn, dateMath, impressionStore) { +function (angular, moment, _, $, kbn, dateMath, impressionSrv) { 'use strict'; kbn = kbn.default; + impressionSrv = impressionSrv.default; var module = angular.module('grafana.services'); @@ -50,7 +51,7 @@ function (angular, moment, _, $, kbn, dateMath, impressionStore) { promise.then(function(result) { if (result.meta.dashboardNotFound !== true) { - impressionStore.impressions.addDashboardImpression(result.dashboard.id); + impressionSrv.addDashboardImpression(result.dashboard.id); } return result; diff --git a/public/app/features/dashboard/dashboard_list_ctrl.ts b/public/app/features/dashboard/dashboard_list_ctrl.ts index d29202f16fb..ab62e3dfff6 100644 --- a/public/app/features/dashboard/dashboard_list_ctrl.ts +++ b/public/app/features/dashboard/dashboard_list_ctrl.ts @@ -26,14 +26,6 @@ export class DashboardListCtrl { } getDashboards() { - if (this.query.query.length === 0 && - this.query.tag.length === 0 && - !this.query.starred) { - return this.searchSrv.browse().then((result) => { - return this.initDashboardList(result); - }); - } - return this.searchSrv.search(this.query).then((result) => { return this.initDashboardList(result); }); @@ -144,7 +136,7 @@ export class DashboardListCtrl { } toggleFolder(section) { - return this.searchSrv.toggleFolder(section); + return this.searchSrv.toggleSection(section); } getTags() { diff --git a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts index 56c2bffdbd9..fda720ce87f 100644 --- a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts +++ b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts @@ -93,7 +93,7 @@ describe('DashboardListCtrl', () => { } ]; - ctrl = createCtrlWithStubs([], response); + ctrl = createCtrlWithStubs(response); }); describe('with query filter', () => { @@ -490,15 +490,12 @@ describe('DashboardListCtrl', () => { }); }); -function createCtrlWithStubs(browseResponse: any, searchResponse?: any, tags?: any) { +function createCtrlWithStubs(searchResponse: any, tags?: any) { const searchSrvStub = { - browse: () => { - return q.resolve(browseResponse); - }, search: (options: any) => { return q.resolve(searchResponse); }, - toggleFolder: (section) => { + toggleSection: (section) => { return; }, getDashboardTags: () => { diff --git a/public/app/features/plugins/plugin_loader.ts b/public/app/features/plugins/plugin_loader.ts index c1dd3246fa1..2aecd42bc09 100644 --- a/public/app/features/plugins/plugin_loader.ts +++ b/public/app/features/plugins/plugin_loader.ts @@ -13,7 +13,7 @@ import * as datemath from 'app/core/utils/datemath'; import * as fileExport from 'app/core/utils/file_export'; import * as flatten from 'app/core/utils/flatten'; import * as ticks from 'app/core/utils/ticks'; -import {impressions} from 'app/features/dashboard/impression_store'; +import impressionSrv from 'app/core/services/impression_srv'; import builtInPlugins from './built_in_plugins'; import * as d3 from 'd3'; @@ -78,7 +78,7 @@ exposeToPlugin('vendor/npm/rxjs/Rx', { }); exposeToPlugin('app/features/dashboard/impression_store', { - impressions: impressions, + impressions: impressionSrv, __esModule: true }); diff --git a/public/app/plugins/panel/dashlist/module.ts b/public/app/plugins/panel/dashlist/module.ts index 2b1630760ed..6e33c2684af 100644 --- a/public/app/plugins/panel/dashlist/module.ts +++ b/public/app/plugins/panel/dashlist/module.ts @@ -1,6 +1,6 @@ import _ from 'lodash'; import {PanelCtrl} from 'app/plugins/sdk'; -import {impressions} from 'app/features/dashboard/impression_store'; +import impressionSrv from 'app/core/services/impression_srv'; class DashListCtrl extends PanelCtrl { static templateUrl = 'module.html'; @@ -123,7 +123,7 @@ class DashListCtrl extends PanelCtrl { return Promise.resolve(); } - var dashIds = _.take(impressions.getDashboardOpened(), this.panel.limit); + var dashIds = _.take(impressionSrv.getDashboardOpened(), this.panel.limit); return this.backendSrv.search({dashboardIds: dashIds, limit: this.panel.limit}).then(result => { this.groups[1].list = dashIds.map(orderId => { return _.find(result, dashboard => { diff --git a/public/app/plugins/panel/singlestat/module.ts b/public/app/plugins/panel/singlestat/module.ts index 5f2b0a7cec6..00abb809fed 100644 --- a/public/app/plugins/panel/singlestat/module.ts +++ b/public/app/plugins/panel/singlestat/module.ts @@ -592,8 +592,6 @@ class SingleStatCtrl extends MetricsPanelCtrl { if (!ctrl.data) { return; } data = ctrl.data; - console.log('singlestat', elem.html()); - // get thresholds data.thresholds = panel.thresholds.split(',').map(function(strVale) { return Number(strVale.trim()); diff --git a/public/sass/layout/_page.scss b/public/sass/layout/_page.scss index 55c6cac4254..310ecd51532 100644 --- a/public/sass/layout/_page.scss +++ b/public/sass/layout/_page.scss @@ -123,7 +123,7 @@ .page-breadcrumbs { display: flex; - padding: 3px 1.5rem 1.5rem 1.5rem; + padding: 10px 25px; line-height: 0.5; }