diff --git a/pkg/api/index.go b/pkg/api/index.go index 0ca6801ac83..01e85f92654 100644 --- a/pkg/api/index.go +++ b/pkg/api/index.go @@ -217,6 +217,12 @@ func setIndexViewData(c *middleware.Context) (*dtos.IndexViewData, error) { {Text: "New", Url: setting.AppSubUrl + "/datasources", Icon: "fa fa-fw fa-plus"}, }, }, + { + Text: "Dashboard List", + Description: "Manage Dashboards And Folders", + Id: "dashboards", + Url: setting.AppSubUrl + "/dashboards", + }, { Text: "Preferences", Id: "org", diff --git a/public/app/core/routes/routes.ts b/public/app/core/routes/routes.ts index c33d368248f..e6861db4252 100644 --- a/public/app/core/routes/routes.ts +++ b/public/app/core/routes/routes.ts @@ -48,10 +48,6 @@ function setupAngularRoutes($routeProvider, $locationProvider) { reloadOnSearch: false, pageClass: 'page-dashboard', }) - .when('/dashboards/list', { - templateUrl: 'public/app/features/dashboard/partials/dash_list.html', - controller : 'DashListCtrl', - }) .when('/configuration', { templateUrl: 'public/app/features/admin/partials/configuration_home.html', controller : 'ConfigurationHomeCtrl', @@ -73,6 +69,11 @@ function setupAngularRoutes($routeProvider, $locationProvider) { controller : 'DataSourceEditCtrl', controllerAs: 'ctrl', }) + .when('/dashboards', { + templateUrl: 'public/app/features/dashboard/partials/dashboardList.html', + controller : 'DashboardListCtrl', + controllerAs: 'ctrl', + }) .when('/org', { templateUrl: 'public/app/features/org/partials/orgDetails.html', controller : 'OrgDetailsCtrl', diff --git a/public/app/features/admin/admin_list_users_ctrl.ts b/public/app/features/admin/admin_list_users_ctrl.ts index ce0a33e634a..b2fcf403702 100644 --- a/public/app/features/admin/admin_list_users_ctrl.ts +++ b/public/app/features/admin/admin_list_users_ctrl.ts @@ -1,5 +1,3 @@ -/// - export default class AdminListUsersCtrl { users: any; pages = []; diff --git a/public/app/features/dashboard/all.js b/public/app/features/dashboard/all.js deleted file mode 100644 index 620a66e09e8..00000000000 --- a/public/app/features/dashboard/all.js +++ /dev/null @@ -1,32 +0,0 @@ -define([ - './dashboard_ctrl', - './alerting_srv', - './history/history', - './dashboardLoaderSrv', - './dashnav/dashnav', - './submenu/submenu', - './save_as_modal', - './save_modal', - './shareModalCtrl', - './shareSnapshotCtrl', - './dashboard_srv', - './viewStateSrv', - './time_srv', - './unsavedChangesSrv', - './unsaved_changes_modal', - './timepicker/timepicker', - './impression_store', - './upload', - './import/dash_import', - './export/export_modal', - './export_data/export_data_modal', - './ad_hoc_filters', - './repeat_option/repeat_option', - './dashgrid/DashboardGrid', - './dashgrid/PanelLoader', - './dashgrid/RowOptions', - './acl/acl', - './acl/acl', - './folder_picker/picker', - './folder_modal/folder' -], function () {}); diff --git a/public/app/features/dashboard/all.ts b/public/app/features/dashboard/all.ts new file mode 100644 index 00000000000..220f2b29081 --- /dev/null +++ b/public/app/features/dashboard/all.ts @@ -0,0 +1,36 @@ + +import './dashboard_ctrl'; +import './alerting_srv'; +import './history/history'; +import './dashboardLoaderSrv'; +import './dashnav/dashnav'; +import './submenu/submenu'; +import './save_as_modal'; +import './save_modal'; +import './shareModalCtrl'; +import './shareSnapshotCtrl'; +import './dashboard_srv'; +import './viewStateSrv'; +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'; +import './export_data/export_data_modal'; +import './ad_hoc_filters'; +import './repeat_option/repeat_option'; +import './dashgrid/DashboardGrid'; +import './dashgrid/PanelLoader'; +import './dashgrid/RowOptions'; +import './acl/acl'; +import './folder_picker/picker'; +import './folder_modal/folder'; +import './move_to_folder_modal/move_to_folder'; +import coreModule from 'app/core/core_module'; + +import {DashboardListCtrl} from './dashboard_list_ctrl'; + +coreModule.controller('DashboardListCtrl', DashboardListCtrl); diff --git a/public/app/features/dashboard/dashboard_list_ctrl.ts b/public/app/features/dashboard/dashboard_list_ctrl.ts new file mode 100644 index 00000000000..232d8d7bb22 --- /dev/null +++ b/public/app/features/dashboard/dashboard_list_ctrl.ts @@ -0,0 +1,117 @@ +import _ from 'lodash'; +import appEvents from 'app/core/app_events'; + +export class DashboardListCtrl { + public dashboards: any []; + query: any; + navModel: any; + canDelete = false; + canMove = false; + + /** @ngInject */ + constructor(private backendSrv, navModelSrv, private $q) { + this.navModel = navModelSrv.getNav('cfg', 'dashboards'); + this.query = ''; + this.getDashboards(); + } + + getDashboards() { + return this.backendSrv.get(`/api/search?query=${this.query}&mode=tree`).then((result) => { + + this.dashboards = this.groupDashboardsInFolders(result); + + for (let dash of this.dashboards) { + dash.checked = false; + } + }); + } + + groupDashboardsInFolders(results) { + let byId = _.groupBy(results, 'id'); + let byFolderId = _.groupBy(results, 'folderId'); + let finalList = []; + + // add missing parent folders + _.each(results, (hit, index) => { + if (hit.folderId && !byId[hit.folderId]) { + const folder = { + id: hit.folderId, + uri: `db/${hit.folderSlug}`, + title: hit.folderTitle, + type: 'dash-folder' + }; + byId[hit.folderId] = folder; + results.splice(index, 0, folder); + } + }); + + // group by folder + for (let hit of results) { + if (hit.folderId) { + hit.type = "dash-child"; + } else { + finalList.push(hit); + } + + hit.url = 'dashboard/' + hit.uri; + + if (hit.type === 'dash-folder') { + if (!byFolderId[hit.id]) { + continue; + } + + for (let child of byFolderId[hit.id]) { + finalList.push(child); + } + } + } + + return finalList; + } + + selectionChanged() { + const selected = _.filter(this.dashboards, {checked: true}).length; + this.canDelete = selected > 0; + + const selectedDashboards = _.filter(this.dashboards, (o) => { + return o.checked && (o.type === 'dash-db' || o.type === 'dash-child'); + }).length; + + const selectedFolders = _.filter(this.dashboards, {checked: true, type: 'dash-folder'}).length; + this.canMove = selectedDashboards > 0 && selectedFolders === 0; + } + + delete() { + const selectedDashboards = _.filter(this.dashboards, {checked: true}); + + appEvents.emit('confirm-modal', { + title: 'Delete', + text: `Do you want to delete the ${selectedDashboards.length} selected dashboards?`, + icon: 'fa-trash', + yesText: 'Delete', + onConfirm: () => { + const promises = []; + for (let dash of selectedDashboards) { + promises.push(this.backendSrv.delete(`/api/dashboards/${dash.uri}`)); + } + + this.$q.all(promises).then(() => { + this.getDashboards(); + }); + } + }); + } + + moveTo() { + const selectedDashboards = _.filter(this.dashboards, {checked: true}); + + const template = '' + + '`'; + appEvents.emit('show-modal', { + templateHtml: template, + modalClass: 'modal--narrow', + model: {dashboards: selectedDashboards, afterSave: this.getDashboards.bind(this)} + }); + } +} diff --git a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html new file mode 100644 index 00000000000..429d6439e57 --- /dev/null +++ b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.html @@ -0,0 +1,29 @@ + diff --git a/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts new file mode 100644 index 00000000000..df7b49c2f00 --- /dev/null +++ b/public/app/features/dashboard/move_to_folder_modal/move_to_folder.ts @@ -0,0 +1,61 @@ +import coreModule from 'app/core/core_module'; +import appEvents from 'app/core/app_events'; +import {DashboardModel} from '../dashboard_model'; + +export class MoveToFolderCtrl { + dashboards: any; + folder: any; + dismiss: any; + afterSave: any; + + /** @ngInject */ + constructor(private backendSrv, private $q) {} + + onFolderChange(folder) { + this.folder = folder; + } + + save() { + const promises = []; + for (let dash of this.dashboards) { + const promise = this.backendSrv.get('/api/dashboards/' + dash.uri).then(fullDash => { + const model = new DashboardModel(fullDash.dashboard, fullDash.meta); + model.folderId = this.folder.id; + model.meta.folderId = this.folder.id; + model.meta.folderTitle = this.folder.title; + const clone = model.getSaveModelClone(); + return this.backendSrv.saveDashboard(clone); + }); + + promises.push(promise); + } + + return this.$q.all(promises).then(() => { + appEvents.emit('alert-success', ['Dashboards Moved', 'OK']); + this.dismiss(); + + return this.afterSave(); + }).then(() => { + console.log('afterSave'); + }).catch(err => { + appEvents.emit('alert-error', [err.message]); + }); + } +} + +export function moveToFolderModal() { + return { + restrict: 'E', + templateUrl: 'public/app/features/dashboard/move_to_folder_modal/move_to_folder.html', + controller: MoveToFolderCtrl, + bindToController: true, + controllerAs: 'ctrl', + scope: { + dismiss: "&", + dashboards: "=", + afterSave: "&" + } + }; +} + +coreModule.directive('moveToFolderModal', moveToFolderModal); diff --git a/public/app/features/dashboard/partials/dashboardList.html b/public/app/features/dashboard/partials/dashboardList.html new file mode 100644 index 00000000000..aca798ebf32 --- /dev/null +++ b/public/app/features/dashboard/partials/dashboardList.html @@ -0,0 +1,88 @@ +
+
+ +
+ +
+ + + +
+ +
+
+ + +
+
+ +
+
+ + + + + + + + + + + + + +
+ + + + + + + {{tag}} + + + + + + + + +
+
+
+ + + No Dashboards or Folders found. + +
+ +
+
+ diff --git a/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts new file mode 100644 index 00000000000..ad85c8d319c --- /dev/null +++ b/public/app/features/dashboard/specs/dashboard_list_ctrl.jest.ts @@ -0,0 +1,174 @@ +import {DashboardListCtrl} from '../dashboard_list_ctrl'; +import q from 'q'; + +describe('DashboardListCtrl', () => { + describe('when fetching dashboards', () => { + let ctrl; + + describe('and dashboard has parent that is not in search result', () => { + beforeEach(() => { + const response = [ + { + id: 399, + title: "Dashboard Test", + uri: "db/dashboard-test", + type: "dash-db", + tags: [], + isStarred: false, + folderId: 410, + folderTitle: "afolder", + folderSlug: "afolder" + } + ]; + + ctrl = new DashboardListCtrl({get: () => q.resolve(response)}, {getNav: () => {}}, q); + return ctrl.getDashboards(); + }); + + it('should add the missing parent folder to the result', () => { + expect(ctrl.dashboards.length).toEqual(2); + expect(ctrl.dashboards[0].id).toEqual(410); + expect(ctrl.dashboards[1].id).toEqual(399); + }); + }); + + beforeEach(() => { + const response = [ + { + id: 410, + title: "afolder", + uri: "db/afolder", + type: "dash-folder", + tags: [], + isStarred: false + }, + { + id: 3, + title: "something else", + uri: "db/something-else", + type: "dash-db", + tags: [], + isStarred: false, + }, + { + id: 399, + title: "Dashboard Test", + uri: "db/dashboard-test", + type: "dash-db", + tags: [], + isStarred: false, + folderId: 410, + folderTitle: "afolder", + folderSlug: "afolder" + } + ]; + ctrl = new DashboardListCtrl({get: () => q.resolve(response)}, {getNav: () => {}}, null); + return ctrl.getDashboards(); + }); + + it('should group them in folders', () => { + expect(ctrl.dashboards.length).toEqual(3); + expect(ctrl.dashboards[0].id).toEqual(410); + expect(ctrl.dashboards[1].id).toEqual(399); + expect(ctrl.dashboards[2].id).toEqual(3); + }); + }); + + describe('when selecting dashboards', () => { + let ctrl; + + beforeEach(() => { + ctrl = new DashboardListCtrl({get: () => q.resolve([])}, {getNav: () => {}}, null); + }); + + describe('and no dashboards are selected', () => { + beforeEach(() => { + ctrl.dashboards = [ + {id: 1, type: 'dash-folder'}, + {id: 2, type: 'dash-db'} + ]; + ctrl.selectionChanged(); + }); + + it('should disable Move To button', () => { + expect(ctrl.canMove).toBeFalsy(); + }); + + it('should disable delete button', () => { + expect(ctrl.canDelete).toBeFalsy(); + }); + }); + + describe('and one dashboard in root is selected', () => { + beforeEach(() => { + ctrl.dashboards = [ + {id: 1, type: 'dash-folder'}, + {id: 2, type: 'dash-db', checked: true} + ]; + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + + describe('and one child dashboard is selected', () => { + beforeEach(() => { + ctrl.dashboards = [ + {id: 1, type: 'dash-folder'}, + {id: 2, type: 'dash-child', checked: true} + ]; + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + + describe('and one child dashboard and one dashboard is selected', () => { + beforeEach(() => { + ctrl.dashboards = [ + {id: 1, type: 'dash-folder'}, + {id: 2, type: 'dash-child', checked: true} + ]; + ctrl.selectionChanged(); + }); + + it('should enable Move To button', () => { + expect(ctrl.canMove).toBeTruthy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + + describe('and one child dashboard and one folder is selected', () => { + beforeEach(() => { + ctrl.dashboards = [ + {id: 1, type: 'dash-folder', checked: true}, + {id: 2, type: 'dash-child', checked: true} + ]; + ctrl.selectionChanged(); + }); + + it('should disable Move To button', () => { + expect(ctrl.canMove).toBeFalsy(); + }); + + it('should enable delete button', () => { + expect(ctrl.canDelete).toBeTruthy(); + }); + }); + }); +}); diff --git a/public/sass/components/_dash_list.scss b/public/sass/components/_dash_list.scss new file mode 100644 index 00000000000..e69de29bb2d