mirror of https://github.com/grafana/grafana
parent
7559982b05
commit
7f3293ce80
@ -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 () {}); |
|
||||||
@ -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); |
||||||
@ -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 = '<move-to-folder-modal dismiss="dismiss()" ' + |
||||||
|
'dashboards="model.dashboards" after-save="model.afterSave()">' + |
||||||
|
'</move-to-folder-modal>`'; |
||||||
|
appEvents.emit('show-modal', { |
||||||
|
templateHtml: template, |
||||||
|
modalClass: 'modal--narrow', |
||||||
|
model: {dashboards: selectedDashboards, afterSave: this.getDashboards.bind(this)} |
||||||
|
}); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,29 @@ |
|||||||
|
<div class="modal-body"> |
||||||
|
<div class="modal-header"> |
||||||
|
<h2 class="modal-header-title"> |
||||||
|
<i class="gicon gicon-folder-new"></i> |
||||||
|
<span class="p-l-1">Choose Dashboard Folder</span> |
||||||
|
</h2> |
||||||
|
|
||||||
|
<a class="modal-header-close" ng-click="ctrl.dismiss();"> |
||||||
|
<i class="fa fa-remove"></i> |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
|
||||||
|
<form name="ctrl.saveForm" ng-submit="ctrl.save()" class="modal-content folder-modal" novalidate> |
||||||
|
<p>Move the {{ctrl.dashboards.length}} selected dashboards to the following folder:</p> |
||||||
|
|
||||||
|
<div class="p-t-2"> |
||||||
|
<div class="gf-form"> |
||||||
|
<folder-picker initial-title="Choose" |
||||||
|
on-change="ctrl.onFolderChange($folder)" |
||||||
|
label-class="width-7"> |
||||||
|
</folder-picker> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
<div class="gf-form-button-row text-center"> |
||||||
|
<button type="submit" class="btn btn-success" ng-disabled="ctrl.saveForm.$invalid">Move</button> |
||||||
|
<a class="btn-text" ng-click="ctrl.dismiss();">Cancel</a> |
||||||
|
</div> |
||||||
|
</form> |
||||||
|
</div> |
||||||
@ -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); |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
<div class="scroll-canvas"> |
||||||
|
<div gemini-scrollbar> |
||||||
|
<navbar model="ctrl.navModel"></navbar> |
||||||
|
<div class="page-container" style="height: 95%"> |
||||||
|
<div class="page-header"> |
||||||
|
<h1>Dashboards</h1> |
||||||
|
|
||||||
|
<a class="btn btn-success" href="/dashboard/new"> |
||||||
|
<i class="fa fa-plus"></i> |
||||||
|
Create Dashboard |
||||||
|
</a> |
||||||
|
<a class="btn btn-success" href="/dashboard/new/?editview=new-folder"> |
||||||
|
<i class="fa fa-plus"></i> |
||||||
|
Create Folder |
||||||
|
</a> |
||||||
|
</div> |
||||||
|
<div class="gf-form width-15 gf-form-group"> |
||||||
|
<span style="position: relative;"> |
||||||
|
<input type="text" class="gf-form-input" placeholder="Find Dashboard by name" tabindex="1" give-focus="true" |
||||||
|
ng-model="ctrl.query" ng-model-options="{ debounce: 500 }" spellcheck='false' ng-change="ctrl.getDashboards()" /> |
||||||
|
</span> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="gf-form-group" ng-if="ctrl.dashboards.length > 1"> |
||||||
|
<div class="gf-form-button-row"> |
||||||
|
<button type="button" |
||||||
|
class="btn gf-form-button btn-secondary" |
||||||
|
ng-disabled="!ctrl.canMove" |
||||||
|
ng-click="ctrl.moveTo()" |
||||||
|
bs-tooltip="ctrl.canMove ? '' : 'Select a dashboard to move (cannot move folders)'" data-placement="bottom"> |
||||||
|
<i class="fa fa-exchange"></i> Move to... |
||||||
|
</button> |
||||||
|
<button type="button" |
||||||
|
class="btn gf-form-button btn-inverse" |
||||||
|
ng-click="ctrl.delete()" |
||||||
|
ng-disabled="!ctrl.canDelete"> |
||||||
|
<i class="fa fa-trash"></i> Delete |
||||||
|
</button> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<div class="admin-list-table" style="height: 80%"> |
||||||
|
<div gemini-scrollbar> |
||||||
|
<table class="filter-table form-inline" ng-show="ctrl.dashboards.length > 0"> |
||||||
|
<thead> |
||||||
|
<tr> |
||||||
|
<th class="width-4"></th> |
||||||
|
<th></th> |
||||||
|
</tr> |
||||||
|
</thead> |
||||||
|
<tbody> |
||||||
|
<tr bindonce ng-repeat="dashboard in ctrl.dashboards"> |
||||||
|
<td class="filter-table__switch-cell" bs-tooltip="" data-placement="right"> |
||||||
|
<gf-form-switch |
||||||
|
switch-class="gf-form-switch--table-cell" |
||||||
|
on-change="ctrl.selectionChanged()" |
||||||
|
checked="dashboard.checked"> |
||||||
|
</gf-form-switch> |
||||||
|
</td> |
||||||
|
<td> |
||||||
|
<a class="search-item pointer search-item--{{dashboard.type}}" |
||||||
|
bo-href-i="{{dashboard.url}}"> |
||||||
|
<span class="search-result-tags"> |
||||||
|
<span ng-click="ctrl.filterByTag(tag, $event)" bindonce ng-repeat="tag in dashboard.tags" tag-color-from-name="tag" class="label label-tag"> |
||||||
|
{{tag}} |
||||||
|
</span> |
||||||
|
<i class="fa" bo-class="{'fa-star': dashboard.isStarred, 'fa-star-o': !dashboard.isStarred}"></i> |
||||||
|
</span> |
||||||
|
<span class="search-result-link"> |
||||||
|
<i class="fa search-result-icon"></i> |
||||||
|
<span bo-text="dashboard.title"></span> |
||||||
|
</span> |
||||||
|
</a> |
||||||
|
</td> |
||||||
|
</tr> |
||||||
|
</tbody> |
||||||
|
</table> |
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
|
<em class="muted" ng-hide="ctrl.dashboards.length > 0"> |
||||||
|
No Dashboards or Folders found. |
||||||
|
</em> |
||||||
|
</div> |
||||||
|
|
||||||
|
</div> |
||||||
|
</div> |
||||||
|
|
||||||
@ -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(); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
|
}); |
||||||
Loading…
Reference in new issue