mirror of
https://github.com/grafana/grafana.git
synced 2026-07-30 00:08:10 -05:00
dashfolders: permissions tab for dashboard folders
This commit is contained in:
@@ -1,75 +1,61 @@
|
||||
<div class="modal-body">
|
||||
<div class="modal-header">
|
||||
<h2 class="modal-header-title">
|
||||
<i class="fa fa-lock"></i>
|
||||
<span class="p-l-1">Permissions</span>
|
||||
</h2>
|
||||
<div>
|
||||
<table class="filter-table gf-form-group">
|
||||
<tr ng-repeat="acl in ctrl.items" ng-class="{'gf-form-disabled': acl.inherited}">
|
||||
<td style="width: 100%;">
|
||||
<i class="{{acl.icon}}"></i>
|
||||
<span ng-bind-html="acl.nameHtml"></span>
|
||||
</td>
|
||||
<td>
|
||||
<em class="muted no-wrap" ng-show="acl.inherited">Inherited from folder</em>
|
||||
</td>
|
||||
<td class="query-keyword">Can</td>
|
||||
<td>
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="acl.permission" ng-options="p.value as p.text for p in ctrl.permissionOptions" ng-change="ctrl.permissionChanged(acl)" ng-disabled="acl.inherited"></select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-inverse btn-small" ng-click="ctrl.removeItem($index)" ng-hide="acl.inherited">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="ctrl.aclItems.length === 0">
|
||||
<td colspan="4">
|
||||
<em>No permissions are set. Will only be accessible by admins.</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<a class="modal-header-close" ng-click="ctrl.dismiss();">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="modal-content">
|
||||
<table class="filter-table gf-form-group">
|
||||
<tr ng-repeat="acl in ctrl.items" ng-class="{'gf-form-disabled': acl.inherited}">
|
||||
<td style="width: 100%;">
|
||||
<i class="{{acl.icon}}"></i>
|
||||
<span ng-bind-html="acl.nameHtml"></span>
|
||||
</td>
|
||||
<td>
|
||||
<em class="muted no-wrap" ng-show="acl.inherited">Inherited from folder</em>
|
||||
</td>
|
||||
<td class="query-keyword">Can</td>
|
||||
<td>
|
||||
<div class="gf-form-inline">
|
||||
<form name="addPermission" class="gf-form-group">
|
||||
<h6 class="muted">Add Permission For</h6>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="acl.permission" ng-options="p.value as p.text for p in ctrl.permissionOptions" ng-change="ctrl.permissionChanged(acl)" ng-disabled="acl.inherited"></select>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<a class="btn btn-inverse btn-small" ng-click="ctrl.removeItem($index)" ng-hide="acl.inherited">
|
||||
<i class="fa fa-remove"></i>
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-show="ctrl.aclItems.length === 0">
|
||||
<td colspan="4">
|
||||
<em>No permissions. Will only be accessible by admins.</em>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<div class="gf-form-inline">
|
||||
<form name="addPermission" class="gf-form-group">
|
||||
<h6 class="muted">Add Permission For</h6>
|
||||
<div class="gf-form-inline">
|
||||
<div class="gf-form">
|
||||
<div class="gf-form-select-wrapper">
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.newType" ng-options="p.value as p.text for p in ctrl.aclTypes" ng-change="ctrl.typeChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.newType === 'User'">
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.newType === 'Group'">
|
||||
<team-picker team-picked="ctrl.groupPicked($group)"></team-picker>
|
||||
<select class="gf-form-input gf-size-auto" ng-model="ctrl.newType" ng-options="p.value as p.text for p in ctrl.aclTypes" ng-change="ctrl.typeChanged()"></select>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="gf-form width-17">
|
||||
<span ng-if="ctrl.error" class="text-error p-l-1">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ctrl.error}}
|
||||
</span>
|
||||
<div class="gf-form" ng-show="ctrl.newType === 'User'">
|
||||
<user-picker user-picked="ctrl.userPicked($user)"></user-picker>
|
||||
</div>
|
||||
<div class="gf-form" ng-show="ctrl.newType === 'Group'">
|
||||
<team-picker team-picked="ctrl.groupPicked($group)"></team-picker>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div class="gf-form width-17">
|
||||
<span ng-if="ctrl.error" class="text-error p-l-1">
|
||||
<i class="fa fa-warning"></i>
|
||||
{{ctrl.error}}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="gf-form-button-row text-center">
|
||||
<button type="button" class="btn btn-danger" ng-disabled="!ctrl.canUpdate" ng-click="ctrl.update()">
|
||||
Update Permissions
|
||||
</button>
|
||||
<a class="btn-text" ng-click="ctrl.dismiss();">Close</a>
|
||||
</div>
|
||||
<div class="gf-form-button-row">
|
||||
<button type="button" class="btn btn-danger" ng-disabled="!ctrl.canUpdate" ng-click="ctrl.update()">
|
||||
Update Permissions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,6 +3,8 @@ import _ from 'lodash';
|
||||
|
||||
export class AclCtrl {
|
||||
dashboard: any;
|
||||
meta: any;
|
||||
|
||||
items: DashboardAcl[];
|
||||
permissionOptions = [{ value: 1, text: 'View' }, { value: 2, text: 'Edit' }, { value: 4, text: 'Admin' }];
|
||||
aclTypes = [
|
||||
@@ -12,7 +14,6 @@ export class AclCtrl {
|
||||
{ value: 'Editor', text: 'Everyone With Editor Role' },
|
||||
];
|
||||
|
||||
dismiss: () => void;
|
||||
newType: string;
|
||||
canUpdate: boolean;
|
||||
error: string;
|
||||
@@ -20,18 +21,17 @@ export class AclCtrl {
|
||||
readonly duplicateError = 'This permission exists already.';
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, dashboardSrv, private $sce, private $scope) {
|
||||
constructor(private backendSrv, private $sce, private $scope) {
|
||||
this.items = [];
|
||||
this.resetNewType();
|
||||
this.dashboard = dashboardSrv.getCurrent();
|
||||
this.get(this.dashboard.id);
|
||||
this.getAcl(this.dashboard.id);
|
||||
}
|
||||
|
||||
resetNewType() {
|
||||
this.newType = 'Group';
|
||||
}
|
||||
|
||||
get(dashboardId: number) {
|
||||
getAcl(dashboardId: number) {
|
||||
return this.backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`).then(result => {
|
||||
this.items = _.map(result, this.prepareViewModel.bind(this));
|
||||
this.sortItems();
|
||||
@@ -43,7 +43,8 @@ export class AclCtrl {
|
||||
}
|
||||
|
||||
prepareViewModel(item: DashboardAcl): DashboardAcl {
|
||||
item.inherited = !this.dashboard.meta.isFolder && this.dashboard.id !== item.dashboardId;
|
||||
item.inherited =
|
||||
!this.meta.isFolder && this.dashboard.id !== item.dashboardId;
|
||||
item.sortRank = 0;
|
||||
|
||||
if (item.userId > 0) {
|
||||
@@ -88,8 +89,8 @@ export class AclCtrl {
|
||||
});
|
||||
}
|
||||
|
||||
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, { items: updated }).then(() => {
|
||||
return this.dismiss();
|
||||
return this.backendSrv.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
|
||||
items: updated
|
||||
});
|
||||
}
|
||||
|
||||
@@ -168,8 +169,9 @@ export function dashAclModal() {
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
dismiss: '&',
|
||||
},
|
||||
dashboard: "=",
|
||||
meta: "="
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
import { AclCtrl } from "../acl";
|
||||
|
||||
describe("AclCtrl", () => {
|
||||
const backendSrv = {
|
||||
getDashboard: jest.fn(() =>
|
||||
Promise.resolve({ id: 1, meta: { isFolder: false } })
|
||||
),
|
||||
get: jest.fn(() => Promise.resolve([])),
|
||||
post: jest.fn(() => Promise.resolve([]))
|
||||
};
|
||||
|
||||
let ctrl;
|
||||
let backendSrvPostMock;
|
||||
|
||||
beforeEach(() => {
|
||||
AclCtrl.prototype.dashboard = { id: 1 };
|
||||
AclCtrl.prototype.meta = { isFolder: false };
|
||||
|
||||
ctrl = new AclCtrl(
|
||||
backendSrv,
|
||||
{ trustAsHtml: t => t },
|
||||
{ $broadcast: () => {} }
|
||||
);
|
||||
backendSrvPostMock = backendSrv.post as any;
|
||||
});
|
||||
|
||||
describe("when permissions are added", () => {
|
||||
beforeEach(() => {
|
||||
const userItem = {
|
||||
id: 2,
|
||||
login: "user2"
|
||||
};
|
||||
|
||||
ctrl.userPicked(userItem);
|
||||
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: "ug1"
|
||||
};
|
||||
|
||||
ctrl.groupPicked(teamItem);
|
||||
|
||||
ctrl.newType = "Editor";
|
||||
ctrl.typeChanged();
|
||||
|
||||
ctrl.newType = "Viewer";
|
||||
ctrl.typeChanged();
|
||||
|
||||
return ctrl.update();
|
||||
});
|
||||
|
||||
it("should sort the result by role, team and user", () => {
|
||||
expect(ctrl.items[0].role).toBe("Viewer");
|
||||
expect(ctrl.items[1].role).toBe("Editor");
|
||||
expect(ctrl.items[2].teamId).toBe(2);
|
||||
expect(ctrl.items[3].userId).toBe(2);
|
||||
});
|
||||
|
||||
it("should save permissions to db", () => {
|
||||
expect(backendSrvPostMock.mock.calls[0][0]).toBe(
|
||||
"/api/dashboards/id/1/acl"
|
||||
);
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[0].role).toBe("Viewer");
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[0].permission).toBe(1);
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[1].role).toBe("Editor");
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[1].permission).toBe(1);
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[2].teamId).toBe(2);
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[2].permission).toBe(1);
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[3].userId).toBe(2);
|
||||
expect(backendSrvPostMock.mock.calls[0][1].items[3].permission).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when duplicate role permissions are added", () => {
|
||||
beforeEach(() => {
|
||||
ctrl.items = [];
|
||||
|
||||
ctrl.newType = "Editor";
|
||||
ctrl.typeChanged();
|
||||
|
||||
ctrl.newType = "Editor";
|
||||
ctrl.typeChanged();
|
||||
});
|
||||
|
||||
it("should throw a validation error", () => {
|
||||
expect(ctrl.error).toBe(ctrl.duplicateError);
|
||||
});
|
||||
|
||||
it("should not add the duplicate permission", () => {
|
||||
expect(ctrl.items.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when duplicate user permissions are added", () => {
|
||||
beforeEach(() => {
|
||||
ctrl.items = [];
|
||||
|
||||
const userItem = {
|
||||
id: 2,
|
||||
login: "user2"
|
||||
};
|
||||
|
||||
ctrl.userPicked(userItem);
|
||||
ctrl.userPicked(userItem);
|
||||
});
|
||||
|
||||
it("should throw a validation error", () => {
|
||||
expect(ctrl.error).toBe(ctrl.duplicateError);
|
||||
});
|
||||
|
||||
it("should not add the duplicate permission", () => {
|
||||
expect(ctrl.items.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when duplicate team permissions are added", () => {
|
||||
beforeEach(() => {
|
||||
ctrl.items = [];
|
||||
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: "ug1"
|
||||
};
|
||||
|
||||
ctrl.groupPicked(teamItem);
|
||||
ctrl.groupPicked(teamItem);
|
||||
});
|
||||
|
||||
it("should throw a validation error", () => {
|
||||
expect(ctrl.error).toBe(ctrl.duplicateError);
|
||||
});
|
||||
|
||||
it("should not add the duplicate permission", () => {
|
||||
expect(ctrl.items.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("when one inherited and one not inherited team permission are added", () => {
|
||||
beforeEach(() => {
|
||||
ctrl.items = [];
|
||||
|
||||
const inheritedTeamItem = {
|
||||
id: 2,
|
||||
name: "ug1",
|
||||
dashboardId: -1
|
||||
};
|
||||
|
||||
ctrl.items.push(inheritedTeamItem);
|
||||
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: "ug1"
|
||||
};
|
||||
ctrl.groupPicked(teamItem);
|
||||
});
|
||||
|
||||
it("should not throw a validation error", () => {
|
||||
expect(ctrl.error).toBe("");
|
||||
});
|
||||
|
||||
it("should add both permissions", () => {
|
||||
expect(ctrl.items.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
backendSrvPostMock.mockClear();
|
||||
});
|
||||
});
|
||||
@@ -3,13 +3,20 @@ import { FolderPageLoader } from './folder_page_loader';
|
||||
export class FolderPermissionsCtrl {
|
||||
navModel: any;
|
||||
folderId: number;
|
||||
dashboard: any;
|
||||
meta: any;
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, navModelSrv, private $routeParams) {
|
||||
if (this.$routeParams.folderId && this.$routeParams.slug) {
|
||||
this.folderId = $routeParams.folderId;
|
||||
|
||||
new FolderPageLoader(this.backendSrv, this.$routeParams).load(this, this.folderId, 'manage-folder-permissions');
|
||||
new FolderPageLoader(this.backendSrv, this.$routeParams)
|
||||
.load(this, this.folderId, "manage-folder-permissions")
|
||||
.then(result => {
|
||||
this.dashboard = result.dashboard;
|
||||
this.meta = result.meta;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<h2 class="page-sub-heading">
|
||||
Coming soon! Permissions will be added in Grafana 5.0 beta.
|
||||
</h2>
|
||||
<dash-acl-modal ng-if="ctrl.dashboard && ctrl.meta"
|
||||
dashboard="ctrl.dashboard"
|
||||
meta="ctrl.meta">
|
||||
</dash-acl-modal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user