mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
dashfolders: bulk move/delete improvements
bulk delete dashboards synchronously moved bulk delete and move dashboards to backend_srv better error handling/messages when moving and deleting folders/dashboards fixes #10181
This commit is contained in:
@@ -18,7 +18,7 @@ export class ManageDashboardsCtrl {
|
|||||||
folderId?: number;
|
folderId?: number;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private backendSrv, navModelSrv, private $q, private searchSrv: SearchSrv) {
|
constructor(private backendSrv, navModelSrv, private searchSrv: SearchSrv) {
|
||||||
this.query = { query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true };
|
this.query = { query: '', mode: 'tree', tag: [], starred: false, skipRecent: true, skipStarred: true };
|
||||||
|
|
||||||
if (this.folderId) {
|
if (this.folderId) {
|
||||||
@@ -76,15 +76,18 @@ export class ManageDashboardsCtrl {
|
|||||||
this.canDelete = selectedDashboards > 0 || selectedFolders > 0;
|
this.canDelete = selectedDashboards > 0 || selectedFolders > 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
getDashboardsToDelete() {
|
getFoldersAndDashboardsToDelete() {
|
||||||
let selectedDashboards = [];
|
let selectedDashboards = {
|
||||||
|
folders: [],
|
||||||
|
dashboards: []
|
||||||
|
};
|
||||||
|
|
||||||
for (const section of this.sections) {
|
for (const section of this.sections) {
|
||||||
if (section.checked && section.id !== 0) {
|
if (section.checked && section.id !== 0) {
|
||||||
selectedDashboards.push(section.slug);
|
selectedDashboards.folders.push(section.slug);
|
||||||
} else {
|
} else {
|
||||||
const selected = _.filter(section.items, { checked: true });
|
const selected = _.filter(section.items, { checked: true });
|
||||||
selectedDashboards.push(..._.map(selected, 'slug'));
|
selectedDashboards.dashboards.push(..._.map(selected, 'slug'));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,23 +105,71 @@ export class ManageDashboardsCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
delete() {
|
delete() {
|
||||||
const selectedDashboards = this.getDashboardsToDelete();
|
const data = this.getFoldersAndDashboardsToDelete();
|
||||||
|
const folderCount = data.folders.length;
|
||||||
|
const dashCount = data.dashboards.length;
|
||||||
|
let text = 'Do you want to delete the ';
|
||||||
|
let text2;
|
||||||
|
|
||||||
|
if (folderCount > 0 && dashCount > 0) {
|
||||||
|
text += `selected folder${folderCount === 1 ? '' : 's'} and dashboard${dashCount === 1 ? '' : 's'}?`;
|
||||||
|
text2 = `All dashboards of the selected folder${folderCount === 1 ? '' : 's'} will also be deleted`;
|
||||||
|
} else if (folderCount > 0) {
|
||||||
|
text += `selected folder${folderCount === 1 ? '' : 's'} and all its dashboards?`;
|
||||||
|
} else {
|
||||||
|
text += `selected dashboard${dashCount === 1 ? '' : 's'}?`;
|
||||||
|
}
|
||||||
|
|
||||||
appEvents.emit('confirm-modal', {
|
appEvents.emit('confirm-modal', {
|
||||||
title: 'Delete',
|
title: 'Delete',
|
||||||
text: `Do you want to delete the ${selectedDashboards.length} selected dashboards?`,
|
text: text,
|
||||||
|
text2: text2,
|
||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
yesText: 'Delete',
|
yesText: 'Delete',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
const promises = [];
|
const foldersAndDashboards = data.folders.concat(data.dashboards);
|
||||||
for (let dash of selectedDashboards) {
|
this.deleteFoldersAndDashboards(foldersAndDashboards);
|
||||||
promises.push(this.backendSrv.delete(`/api/dashboards/db/${dash}`));
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private deleteFoldersAndDashboards(slugs) {
|
||||||
|
this.backendSrv.deleteDashboards(slugs).then(result => {
|
||||||
|
const folders = _.filter(result, dash => dash.meta.isFolder);
|
||||||
|
const folderCount = folders.length;
|
||||||
|
const dashboards = _.filter(result, dash => !dash.meta.isFolder);
|
||||||
|
const dashCount = dashboards.length;
|
||||||
|
|
||||||
|
if (result.length > 0) {
|
||||||
|
let header;
|
||||||
|
let msg;
|
||||||
|
|
||||||
|
if (folderCount > 0 && dashCount > 0) {
|
||||||
|
header = `Folder${folderCount === 1 ? '' : 's'} And Dashboard${dashCount === 1 ? '' : 's'} Deleted`;
|
||||||
|
msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} `;
|
||||||
|
msg += `and ${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`;
|
||||||
|
} else if (folderCount > 0) {
|
||||||
|
header = `Folder${folderCount === 1 ? '' : 's'} Deleted`;
|
||||||
|
|
||||||
|
if (folderCount === 1) {
|
||||||
|
msg = `${folders[0].dashboard.title} has been deleted`;
|
||||||
|
} else {
|
||||||
|
msg = `${folderCount} folder${folderCount === 1 ? '' : 's'} has been deleted`;
|
||||||
|
}
|
||||||
|
} else if (dashCount > 0) {
|
||||||
|
header = `Dashboard${dashCount === 1 ? '' : 's'} Deleted`;
|
||||||
|
|
||||||
|
if (dashCount === 1) {
|
||||||
|
msg = `${dashboards[0].dashboard.title} has been deleted`;
|
||||||
|
} else {
|
||||||
|
msg = `${dashCount} dashboard${dashCount === 1 ? '' : 's'} has been deleted`;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
this.$q.all(promises).then(() => {
|
appEvents.emit('alert-success', [header, msg]);
|
||||||
this.getDashboards();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.getDashboards();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
import _ from 'lodash';
|
import _ from 'lodash';
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
|
import { DashboardModel } from 'app/features/dashboard/dashboard_model';
|
||||||
|
|
||||||
export class BackendSrv {
|
export class BackendSrv {
|
||||||
private inFlightRequests = {};
|
private inFlightRequests = {};
|
||||||
@@ -246,6 +247,96 @@ export class BackendSrv {
|
|||||||
return this.getDashboard('db', res.slug);
|
return this.getDashboard('db', res.slug);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
deleteDashboard(slug) {
|
||||||
|
let deferred = this.$q.defer();
|
||||||
|
|
||||||
|
this.getDashboard('db', slug)
|
||||||
|
.then(fullDash => {
|
||||||
|
this.delete(`/api/dashboards/db/${slug}`)
|
||||||
|
.then(() => {
|
||||||
|
deferred.resolve(fullDash);
|
||||||
|
}).catch(err => {
|
||||||
|
deferred.reject(err);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteDashboards(dashboardSlugs) {
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
for (let slug of dashboardSlugs) {
|
||||||
|
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, slug));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.executeInOrder(tasks, []);
|
||||||
|
}
|
||||||
|
|
||||||
|
moveDashboards(dashboardSlugs, toFolder) {
|
||||||
|
const tasks = [];
|
||||||
|
|
||||||
|
for (let slug of dashboardSlugs) {
|
||||||
|
tasks.push(this.createTask(this.moveDashboard.bind(this), true, slug, toFolder));
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.executeInOrder(tasks, [])
|
||||||
|
.then(result => {
|
||||||
|
return {
|
||||||
|
totalCount: result.length,
|
||||||
|
successCount: _.filter(result, { succeeded: true }).length,
|
||||||
|
alreadyInFolderCount: _.filter(result, { alreadyInFolder: true }).length
|
||||||
|
};
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private moveDashboard(slug, toFolder) {
|
||||||
|
let deferred = this.$q.defer();
|
||||||
|
|
||||||
|
this.getDashboard('db', slug).then(fullDash => {
|
||||||
|
const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
|
||||||
|
|
||||||
|
if ((!model.folderId && toFolder.id === 0) ||
|
||||||
|
model.folderId === toFolder.id) {
|
||||||
|
deferred.resolve({alreadyInFolder: true});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
model.folderId = toFolder.id;
|
||||||
|
model.meta.folderId = toFolder.id;
|
||||||
|
model.meta.folderTitle = toFolder.title;
|
||||||
|
const clone = model.getSaveModelClone();
|
||||||
|
|
||||||
|
this.saveDashboard(clone, {})
|
||||||
|
.then(() => {
|
||||||
|
deferred.resolve({succeeded: true});
|
||||||
|
}).catch(err => {
|
||||||
|
deferred.resolve({succeeded: false});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return deferred.promise;
|
||||||
|
}
|
||||||
|
|
||||||
|
private createTask(fn, ignoreRejections, ...args: any[]) {
|
||||||
|
return (result) => {
|
||||||
|
return fn.apply(null, args)
|
||||||
|
.then(res => {
|
||||||
|
return Array.prototype.concat(result, [res]);
|
||||||
|
}).catch(err => {
|
||||||
|
if (ignoreRejections) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
throw err;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private executeInOrder(tasks, initialValue) {
|
||||||
|
return tasks.reduce(this.$q.when, initialValue);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -501,7 +501,7 @@ describe('ManageDashboards', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
describe('when deleting dashboards', () => {
|
describe('when deleting dashboards', () => {
|
||||||
let toBeDeleted = [];
|
let toBeDeleted: any;
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
ctrl = createCtrlWithStubs([]);
|
ctrl = createCtrlWithStubs([]);
|
||||||
@@ -535,23 +535,27 @@ describe('ManageDashboards', () => {
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
toBeDeleted = ctrl.getDashboardsToDelete();
|
toBeDeleted = ctrl.getFoldersAndDashboardsToDelete();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return 3 items', () => {
|
it('should return 1 folder', () => {
|
||||||
expect(toBeDeleted.length).toEqual(3);
|
expect(toBeDeleted.folders.length).toEqual(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return 2 dashboards', () => {
|
||||||
|
expect(toBeDeleted.dashboards.length).toEqual(2);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should filter out children if parent is checked', () => {
|
it('should filter out children if parent is checked', () => {
|
||||||
expect(toBeDeleted[0]).toEqual('folder');
|
expect(toBeDeleted.folders[0]).toEqual('folder');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not filter out children if parent not is checked', () => {
|
it('should not filter out children if parent not is checked', () => {
|
||||||
expect(toBeDeleted[1]).toEqual('folder-2-dash');
|
expect(toBeDeleted.dashboards[0]).toEqual('folder-2-dash');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not filter out children if parent is checked and root', () => {
|
it('should not filter out children if parent is checked and root', () => {
|
||||||
expect(toBeDeleted[2]).toEqual('root-dash');
|
expect(toBeDeleted.dashboards[1]).toEqual('root-dash');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -599,5 +603,5 @@ function createCtrlWithStubs(searchResponse: any, tags?: any) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
return new ManageDashboardsCtrl({}, { getNav: () => { } }, q, <SearchSrv>searchSrvStub);
|
return new ManageDashboardsCtrl({}, { getNav: () => { } }, <SearchSrv>searchSrvStub);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ export class FolderSettingsCtrl {
|
|||||||
icon: 'fa-trash',
|
icon: 'fa-trash',
|
||||||
yesText: 'Delete',
|
yesText: 'Delete',
|
||||||
onConfirm: () => {
|
onConfirm: () => {
|
||||||
return this.backendSrv.delete(`/api/dashboards/${this.meta.type}/${this.meta.slug}`).then(() => {
|
return this.backendSrv.deleteDashboard(this.meta.slug).then(() => {
|
||||||
appEvents.emit('alert-success', ['Folder deleted']);
|
appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]);
|
||||||
this.$location.url('/dashboards');
|
this.$location.url('/dashboards');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
import _ from 'lodash';
|
|
||||||
import coreModule from 'app/core/core_module';
|
import coreModule from 'app/core/core_module';
|
||||||
import appEvents from 'app/core/app_events';
|
import appEvents from 'app/core/app_events';
|
||||||
import { DashboardModel } from '../dashboard_model';
|
|
||||||
|
|
||||||
export class MoveToFolderCtrl {
|
export class MoveToFolderCtrl {
|
||||||
dashboards: any;
|
dashboards: any;
|
||||||
@@ -10,75 +8,23 @@ export class MoveToFolderCtrl {
|
|||||||
afterSave: any;
|
afterSave: any;
|
||||||
|
|
||||||
/** @ngInject */
|
/** @ngInject */
|
||||||
constructor(private backendSrv, private $q) { }
|
constructor(private backendSrv) { }
|
||||||
|
|
||||||
onFolderChange(folder) {
|
onFolderChange(folder) {
|
||||||
this.folder = folder;
|
this.folder = folder;
|
||||||
}
|
}
|
||||||
|
|
||||||
private doNext(fn, ...args: any[]) {
|
|
||||||
return function (result) {
|
|
||||||
return fn.apply(null, args)
|
|
||||||
.then(res => {
|
|
||||||
return Array.prototype.concat(result, [res]);
|
|
||||||
});
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
private doInOrder(tasks, init) {
|
|
||||||
return tasks.reduce(this.$q.when, init);
|
|
||||||
}
|
|
||||||
|
|
||||||
private moveDashboard(dash) {
|
|
||||||
let deferred = this.$q.defer();
|
|
||||||
|
|
||||||
this.backendSrv.get('/api/dashboards/db/' + dash)
|
|
||||||
.then(fullDash => {
|
|
||||||
const model = new DashboardModel(fullDash.dashboard, fullDash.meta);
|
|
||||||
|
|
||||||
if ((!model.folderId && this.folder.id === 0) ||
|
|
||||||
model.folderId === this.folder.id) {
|
|
||||||
deferred.resolve({alreadyInFolder: true});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
model.folderId = this.folder.id;
|
|
||||||
model.meta.folderId = this.folder.id;
|
|
||||||
model.meta.folderTitle = this.folder.title;
|
|
||||||
const clone = model.getSaveModelClone();
|
|
||||||
|
|
||||||
this.backendSrv.saveDashboard(clone)
|
|
||||||
.then(() => {
|
|
||||||
deferred.resolve({succeeded: true});
|
|
||||||
})
|
|
||||||
.catch(err => {
|
|
||||||
deferred.resolve({succeeded: false});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return deferred.promise;
|
|
||||||
}
|
|
||||||
|
|
||||||
save() {
|
save() {
|
||||||
const tasks = [];
|
return this.backendSrv.moveDashboards(this.dashboards, this.folder)
|
||||||
|
|
||||||
for (let dash of this.dashboards) {
|
|
||||||
tasks.push(this.doNext(this.moveDashboard.bind(this), dash));
|
|
||||||
}
|
|
||||||
|
|
||||||
return this.doInOrder(tasks, [])
|
|
||||||
.then(result => {
|
.then(result => {
|
||||||
const totalCount = result.length;
|
if (result.successCount > 0) {
|
||||||
const successCount = _.filter(result, { succeeded: true }).length;
|
const header = `Dashboard${result.successCount === 1 ? '' : 's'} Moved`;
|
||||||
const alreadyInFolderCount = _.filter(result, { alreadyInFolder: true }).length;
|
const msg = `${result.successCount} dashboard${result.successCount === 1 ? '' : 's'} moved to ${this.folder.title}`;
|
||||||
|
appEvents.emit('alert-success', [header, msg]);
|
||||||
if (successCount > 0) {
|
|
||||||
const msg = successCount + ' dashboard' + (successCount === 1 ? '' : 's') + ' moved to ' + this.folder.title;
|
|
||||||
appEvents.emit('alert-success', [ 'Dashboard' + (successCount === 1 ? '' : 's') + ' Moved', msg]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (totalCount === alreadyInFolderCount) {
|
if (result.totalCount === result.alreadyInFolderCount) {
|
||||||
appEvents.emit('alert-error', ['Error', 'Dashboards already belongs to folder ' + this.folder.title]);
|
appEvents.emit('alert-error', ['Error', `Dashboards already belongs to folder ${this.folder.title}`]);
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dismiss();
|
this.dismiss();
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ export class SettingsCtrl {
|
|||||||
}
|
}
|
||||||
|
|
||||||
deleteDashboardConfirmed() {
|
deleteDashboardConfirmed() {
|
||||||
this.backendSrv.delete('/api/dashboards/db/' + this.dashboard.meta.slug).then(() => {
|
this.backendSrv.deleteDashboard(this.dashboard.meta.slug).then(() => {
|
||||||
appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
|
appEvents.emit('alert-success', ['Dashboard Deleted', this.dashboard.title + ' has been deleted']);
|
||||||
this.$location.url('/');
|
this.$location.url('/');
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user