Merge pull request #10739 from grafana/10630_folder_api

New folder and permissions API
This commit is contained in:
Marcus Efraimsson
2018-02-22 15:51:54 +01:00
committed by GitHub
42 changed files with 1668 additions and 463 deletions

View File

@@ -26,7 +26,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
loadStore() {
const { nav, folder, view } = this.props;
return folder.load(view.routeParams.get('uid') as string).then(res => {
view.updatePathAndQuery(`${res.meta.url}/permissions`, {}, {});
view.updatePathAndQuery(`${res.url}/permissions`, {}, {});
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-permissions');
});
}

View File

@@ -9,17 +9,14 @@ describe('FolderSettings', () => {
let page;
beforeAll(() => {
backendSrv.getDashboardByUid.mockReturnValue(
backendSrv.getFolderByUid.mockReturnValue(
Promise.resolve({
dashboard: {
id: 1,
title: 'Folder Name',
uid: 'uid-str',
},
meta: {
url: '/dashboards/f/uid/folder-name',
canSave: true,
},
id: 1,
uid: 'uid',
title: 'Folder Name',
url: '/dashboards/f/uid/folder-name',
canSave: true,
version: 1,
})
);

View File

@@ -10,7 +10,6 @@ import appEvents from 'app/core/app_events';
@observer
export class FolderSettings extends React.Component<IContainerProps, any> {
formSnapshot: any;
dashboard: any;
constructor(props) {
super(props);
@@ -22,9 +21,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
return folder.load(view.routeParams.get('uid') as string).then(res => {
this.formSnapshot = getSnapshot(folder);
this.dashboard = res.dashboard;
view.updatePathAndQuery(`${res.meta.url}/settings`, {}, {});
view.updatePathAndQuery(`${res.url}/settings`, {}, {});
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
});
@@ -51,7 +48,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
const { nav, folder, view } = this.props;
folder
.saveFolder(this.dashboard, { overwrite: false })
.saveFolder({ overwrite: false })
.then(newUrl => {
view.updatePathAndQuery(newUrl, {}, {});
@@ -61,7 +58,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
.then(() => {
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
})
.catch(this.handleSaveFolderError);
.catch(this.handleSaveFolderError.bind(this));
}
delete(evt) {
@@ -79,7 +76,7 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
return this.props.folder.deleteFolder().then(() => {
return folder.deleteFolder().then(() => {
appEvents.emit('alert-success', ['Folder Deleted', `${title} has been deleted`]);
view.updatePathAndQuery('dashboards', '', '');
});
@@ -91,6 +88,8 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
if (err.data && err.data.status === 'version-mismatch') {
err.isHandled = true;
const { nav, folder, view } = this.props;
appEvents.emit('confirm-modal', {
title: 'Conflict',
text: 'Someone else has updated this folder.',
@@ -98,16 +97,20 @@ export class FolderSettings extends React.Component<IContainerProps, any> {
yesText: 'Save & Overwrite',
icon: 'fa-warning',
onConfirm: () => {
this.props.folder.saveFolder(this.dashboard, { overwrite: true });
folder
.saveFolder({ overwrite: true })
.then(newUrl => {
view.updatePathAndQuery(newUrl, {}, {});
appEvents.emit('dashboard-saved');
appEvents.emit('alert-success', ['Folder saved']);
})
.then(() => {
return nav.initFolderNav(toJS(folder.folder), 'manage-folder-settings');
});
},
});
}
if (err.data && err.data.status === 'name-exists') {
err.isHandled = true;
appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']);
}
}
render() {

View File

@@ -53,7 +53,7 @@ describe('AddPermissions', () => {
wrapper.find('form').simulate('submit', { preventDefault() {} });
expect(backendSrv.post.mock.calls.length).toBe(1);
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
});
});
@@ -80,7 +80,7 @@ describe('AddPermissions', () => {
wrapper.find('form').simulate('submit', { preventDefault() {} });
expect(backendSrv.post.mock.calls.length).toBe(1);
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
});
});

View File

@@ -78,8 +78,8 @@ export class ManageDashboardsCtrl {
return;
}
return this.backendSrv.getDashboardByUid(this.folderUid).then(dash => {
this.canSave = dash.meta.canSave;
return this.backendSrv.getFolderByUid(this.folderUid).then(folder => {
this.canSave = folder.canSave;
});
});
}
@@ -173,48 +173,13 @@ export class ManageDashboardsCtrl {
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
const foldersAndDashboards = data.folders.concat(data.dashboards);
this.deleteFoldersAndDashboards(foldersAndDashboards);
this.deleteFoldersAndDashboards(data.folders, data.dashboards);
},
});
}
private deleteFoldersAndDashboards(uids) {
this.backendSrv.deleteDashboards(uids).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`;
}
}
appEvents.emit('alert-success', [header, msg]);
}
private deleteFoldersAndDashboards(folderUids, dashboardUids) {
this.backendSrv.deleteFoldersAndDashboards(folderUids, dashboardUids).then(() => {
this.refreshList();
});
}

View File

@@ -221,14 +221,18 @@ export class BackendSrv {
return this.get('/api/search', query);
}
getDashboard(type, slug) {
return this.get('/api/dashboards/' + type + '/' + slug);
getDashboardBySlug(slug) {
return this.get(`/api/dashboards/db/${slug}`);
}
getDashboardByUid(uid: string) {
return this.get(`/api/dashboards/uid/${uid}`);
}
getFolderByUid(uid: string) {
return this.get(`/api/folders/${uid}`);
}
saveDashboard(dash, options) {
options = options || {};
@@ -240,55 +244,41 @@ export class BackendSrv {
});
}
createDashboardFolder(name) {
const dash = {
schemaVersion: 16,
title: name.trim(),
editable: true,
panels: [],
};
return this.post('/api/dashboards/db/', {
dashboard: dash,
isFolder: true,
overwrite: false,
}).then(res => {
return this.getDashboard('db', res.slug);
});
createFolder(payload: any) {
return this.post('/api/folders', payload);
}
saveFolder(dash, options) {
updateFolder(folder, options) {
options = options || {};
return this.post('/api/dashboards/db/', {
dashboard: dash,
isFolder: true,
return this.put(`/api/folders/${folder.uid}`, {
title: folder.title,
version: folder.version,
overwrite: options.overwrite === true,
message: options.message || '',
});
}
deleteDashboard(uid) {
let deferred = this.$q.defer();
this.getDashboardByUid(uid).then(fullDash => {
this.delete(`/api/dashboards/uid/${uid}`)
.then(() => {
deferred.resolve(fullDash);
})
.catch(err => {
deferred.reject(err);
});
});
return deferred.promise;
deleteFolder(uid: string, showSuccessAlert) {
return this.request({ method: 'DELETE', url: `/api/folders/${uid}`, showSuccessAlert: showSuccessAlert === true });
}
deleteDashboards(dashboardUids) {
deleteDashboard(uid, showSuccessAlert) {
return this.request({
method: 'DELETE',
url: `/api/dashboards/uid/${uid}`,
showSuccessAlert: showSuccessAlert === true,
});
}
deleteFoldersAndDashboards(folderUids, dashboardUids) {
const tasks = [];
for (let uid of dashboardUids) {
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, uid));
for (let folderUid of folderUids) {
tasks.push(this.createTask(this.deleteFolder.bind(this), true, folderUid, true));
}
for (let dashboardUid of dashboardUids) {
tasks.push(this.createTask(this.deleteDashboard.bind(this), true, dashboardUid, true));
}
return this.executeInOrder(tasks, []);

View File

@@ -18,9 +18,9 @@ export class CreateFolderCtrl {
return;
}
return this.backendSrv.createDashboardFolder(this.title).then(result => {
return this.backendSrv.createFolder({ title: this.title }).then(result => {
appEvents.emit('alert-success', ['Folder Created', 'OK']);
this.$location.url(locationUtil.stripBaseFromUrl(result.meta.url));
this.$location.url(locationUtil.stripBaseFromUrl(result.url));
});
}

View File

@@ -14,7 +14,7 @@ export class FolderDashboardsCtrl {
const loader = new FolderPageLoader(this.backendSrv);
loader.load(this, this.uid, 'manage-folder-dashboards').then(folder => {
const url = locationUtil.stripBaseFromUrl(folder.meta.url);
const url = locationUtil.stripBaseFromUrl(folder.url);
if (url !== $location.path()) {
$location.path(url).replace();

View File

@@ -36,16 +36,16 @@ export class FolderPageLoader {
},
};
return this.backendSrv.getDashboardByUid(uid).then(result => {
ctrl.folderId = result.dashboard.id;
const folderTitle = result.dashboard.title;
const folderUrl = result.meta.url;
return this.backendSrv.getFolderByUid(uid).then(folder => {
ctrl.folderId = folder.id;
const folderTitle = folder.title;
const folderUrl = folder.url;
ctrl.navModel.main.text = folderTitle;
const dashTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-dashboards');
dashTab.url = folderUrl;
if (result.meta.canAdmin) {
if (folder.canAdmin) {
const permTab = ctrl.navModel.main.children.find(child => child.id === 'manage-folder-permissions');
permTab.url = folderUrl + '/permissions';
@@ -55,7 +55,7 @@ export class FolderPageLoader {
ctrl.navModel.main.children = [dashTab];
}
return result;
return folder;
});
}
}

View File

@@ -89,13 +89,13 @@ export class FolderPickerCtrl {
evt.preventDefault();
}
return this.backendSrv.createDashboardFolder(this.newFolderName).then(result => {
return this.backendSrv.createFolder({ title: this.newFolderName }).then(result => {
appEvents.emit('alert-success', ['Folder Created', 'OK']);
this.closeCreateFolder();
this.folder = {
text: result.dashboard.title,
value: result.dashboard.id,
text: result.title,
value: result.id,
};
this.onFolderChange(this.folder);
});

View File

@@ -7,8 +7,7 @@ export class FolderSettingsCtrl {
folderId: number;
uid: string;
canSave = false;
dashboard: any;
meta: any;
folder: any;
title: string;
hasChanged: boolean;
@@ -23,10 +22,9 @@ export class FolderSettingsCtrl {
$location.path(`${folder.meta.url}/settings`).replace();
}
this.dashboard = folder.dashboard;
this.meta = folder.meta;
this.canSave = folder.meta.canSave;
this.title = this.dashboard.title;
this.folder = folder;
this.canSave = this.folder.canSave;
this.title = this.folder.title;
});
}
}
@@ -38,10 +36,10 @@ export class FolderSettingsCtrl {
return;
}
this.dashboard.title = this.title.trim();
this.folder.title = this.title.trim();
return this.backendSrv
.updateDashboardFolder(this.dashboard, { overwrite: false })
.updateFolder(this.folder)
.then(result => {
if (result.url !== this.$location.path()) {
this.$location.url(result.url + '/settings');
@@ -54,7 +52,7 @@ export class FolderSettingsCtrl {
}
titleChanged() {
this.hasChanged = this.dashboard.title.toLowerCase() !== this.title.trim().toLowerCase();
this.hasChanged = this.folder.title.toLowerCase() !== this.title.trim().toLowerCase();
}
delete(evt) {
@@ -69,8 +67,8 @@ export class FolderSettingsCtrl {
icon: 'fa-trash',
yesText: 'Delete',
onConfirm: () => {
return this.backendSrv.deleteDashboard(this.dashboard.uid).then(() => {
appEvents.emit('alert-success', ['Folder Deleted', `${this.dashboard.title} has been deleted`]);
return this.backendSrv.deleteFolder(this.uid).then(() => {
appEvents.emit('alert-success', ['Folder Deleted', `${this.folder.title} has been deleted`]);
this.$location.url('dashboards');
});
},
@@ -88,15 +86,9 @@ export class FolderSettingsCtrl {
yesText: 'Save & Overwrite',
icon: 'fa-warning',
onConfirm: () => {
this.backendSrv.updateDashboardFolder(this.dashboard, { overwrite: true });
this.backendSrv.updateFolder(this.folder, { overwrite: true });
},
});
}
if (err.data && err.data.status === 'name-exists') {
err.isHandled = true;
appEvents.emit('alert-error', ['A folder or dashboard with this name exists already.']);
}
}
}

View File

@@ -374,7 +374,7 @@ describe('DashboardModel', function() {
it('should assign id', function() {
model.rows = [createRow({ collapse: true, height: 8 }, [[6], [6]])];
model.rows[0].panels[0] = { };
model.rows[0].panels[0] = {};
let dashboard = new DashboardModel(model);
expect(dashboard.panels[0].id).toBe(1);

View File

@@ -18,7 +18,7 @@ export class SoloPanelCtrl {
// if no uid, redirect to new route based on slug
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
backendSrv.getDashboardBySlug($routeParams.slug).then(res => {
if (res) {
const url = locationUtil.stripBaseFromUrl(res.meta.url.replace('/d/', '/d-solo/'));
$location.path(url).replace();

View File

@@ -21,7 +21,7 @@ export class LoadDashboardCtrl {
// if no uid, redirect to new route based on slug
if (!($routeParams.type === 'script' || $routeParams.type === 'snapshot') && !$routeParams.uid) {
backendSrv.get(`/api/dashboards/db/${$routeParams.slug}`).then(res => {
backendSrv.getDashboardBySlug($routeParams.slug).then(res => {
if (res) {
$location.path(locationUtil.stripBaseFromUrl(res.meta.url)).replace();
}

View File

@@ -2,11 +2,12 @@ import { types, getEnv, flow } from 'mobx-state-tree';
export const Folder = types.model('Folder', {
id: types.identifier(types.number),
uid: types.string,
title: types.string,
url: types.string,
canSave: types.boolean,
uid: types.string,
hasChanged: types.boolean,
version: types.number,
});
export const FolderStore = types
@@ -21,15 +22,15 @@ export const FolderStore = types
}
const backendSrv = getEnv(self).backendSrv;
const res = yield backendSrv.getDashboardByUid(uid);
const res = yield backendSrv.getFolderByUid(uid);
self.folder = Folder.create({
id: res.dashboard.id,
title: res.dashboard.title,
url: res.meta.url,
uid: res.dashboard.uid,
canSave: res.meta.canSave,
id: res.id,
uid: res.uid,
title: res.title,
url: res.url,
canSave: res.canSave,
hasChanged: false,
version: res.version,
});
return res;
@@ -40,12 +41,13 @@ export const FolderStore = types
self.folder.hasChanged = originalTitle.toLowerCase() !== title.trim().toLowerCase() && title.trim().length > 0;
},
saveFolder: flow(function* saveFolder(dashboard: any, options: any) {
saveFolder: flow(function* saveFolder(options: any) {
const backendSrv = getEnv(self).backendSrv;
dashboard.title = self.folder.title.trim();
self.folder.title = self.folder.title.trim();
const res = yield backendSrv.saveFolder(dashboard, options);
const res = yield backendSrv.updateFolder(self.folder, options);
self.folder.url = res.url;
self.folder.version = res.version;
return `${self.folder.url}/settings`;
}),
@@ -53,6 +55,6 @@ export const FolderStore = types
deleteFolder: flow(function* deleteFolder() {
const backendSrv = getEnv(self).backendSrv;
return backendSrv.deleteDashboard(self.folder.uid);
return backendSrv.deleteFolder(self.folder.uid);
}),
}));

View File

@@ -44,7 +44,7 @@ describe('PermissionsStore', () => {
expect(store.items[0].permission).toBe(2);
expect(store.items[0].permissionName).toBe('Edit');
expect(backendSrv.post.mock.calls.length).toBe(1);
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
});
it('should save removed permissions automatically', () => {
@@ -54,7 +54,7 @@ describe('PermissionsStore', () => {
expect(store.items.length).toBe(2);
expect(backendSrv.post.mock.calls.length).toBe(1);
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/permissions');
});
describe('when duplicate team permissions are added', () => {

View File

@@ -110,7 +110,7 @@ export const PermissionsStore = types
self.dashboardId = dashboardId;
self.items.clear();
const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`);
const res = yield backendSrv.get(`/api/dashboards/id/${dashboardId}/permissions`);
const items = prepareServerResponse(res, dashboardId, isFolder, isInRoot);
self.items = items;
self.originalItems = items;
@@ -210,7 +210,7 @@ const updateItems = self => {
let res;
try {
res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/acl`, {
res = backendSrv.post(`/api/dashboards/id/${self.dashboardId}/permissions`, {
items: updated,
});
} catch (error) {