mirror of
https://github.com/grafana/grafana.git
synced 2025-02-25 18:55:37 -06:00
dashfolders: use react component for dashboard permissions
Switch out the angular component for the new react component on the dashboard permissions editview on the settings page.
This commit is contained in:
@@ -21,7 +21,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
}
|
||||
|
||||
render() {
|
||||
const { nav, folder, permissions } = this.props;
|
||||
const { nav, folder, permissions, backendSrv } = this.props;
|
||||
|
||||
if (!folder.folder || !nav.main) {
|
||||
return <h2>Loading</h2>;
|
||||
@@ -34,12 +34,7 @@ export class FolderPermissions extends Component<IContainerProps, any> {
|
||||
<PageHeader model={nav as any} />
|
||||
<div className="page-container page-body">
|
||||
<h2 className="page-sub-heading">Folder Permissions</h2>
|
||||
<Permissions
|
||||
permissions={permissions}
|
||||
isFolder={true}
|
||||
dashboardId={dashboardId}
|
||||
backendSrv={this.props.backendSrv}
|
||||
/>
|
||||
<Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -6,7 +6,7 @@ import LoginBackground from './components/Login/LoginBackground';
|
||||
import { SearchResult } from './components/search/SearchResult';
|
||||
import { TagFilter } from './components/TagFilter/TagFilter';
|
||||
import UserPicker from './components/Picker/UserPicker';
|
||||
import Permissions from './components/Permissions/Permissions';
|
||||
import DashboardPermissions from './components/Permissions/DashboardPermissions';
|
||||
|
||||
export function registerAngularDirectives() {
|
||||
react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
|
||||
@@ -20,5 +20,5 @@ export function registerAngularDirectives() {
|
||||
['tagOptions', { watchDepth: 'reference' }],
|
||||
]);
|
||||
react2AngularDirective('selectUserPicker', UserPicker, ['backendSrv', 'handlePicked']);
|
||||
react2AngularDirective('permissions', Permissions, ['error', 'aclTypes', 'typeChanged', 'backendSrv', 'dashboardId']);
|
||||
react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId']);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
import React, { Component } from 'react';
|
||||
import { observer } from 'mobx-react';
|
||||
import { store } from 'app/stores/store';
|
||||
import Permissions from 'app/core/components/Permissions/Permissions';
|
||||
|
||||
export interface IProps {
|
||||
dashboardId: number;
|
||||
backendSrv: any;
|
||||
}
|
||||
|
||||
@observer
|
||||
class DashboardPermissions extends Component<IProps, any> {
|
||||
permissions: any;
|
||||
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.permissions = store.permissions;
|
||||
}
|
||||
|
||||
render() {
|
||||
const { dashboardId, backendSrv } = this.props;
|
||||
|
||||
return (
|
||||
<Permissions permissions={this.permissions} isFolder={false} dashboardId={dashboardId} backendSrv={backendSrv} />
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export default DashboardPermissions;
|
||||
73
public/app/core/components/Permissions/Permissions.jest.tsx
Normal file
73
public/app/core/components/Permissions/Permissions.jest.tsx
Normal file
@@ -0,0 +1,73 @@
|
||||
import React from 'react';
|
||||
import Permissions from './Permissions';
|
||||
import { RootStore } from 'app/stores/RootStore/RootStore';
|
||||
import { backendSrv } from 'test/mocks/common';
|
||||
import { shallow } from 'enzyme';
|
||||
|
||||
describe('Permissions', () => {
|
||||
let wrapper;
|
||||
|
||||
beforeAll(() => {
|
||||
backendSrv.get.mockReturnValue(
|
||||
Promise.resolve([
|
||||
{ id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
|
||||
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
|
||||
{
|
||||
id: 4,
|
||||
dashboardId: 1,
|
||||
userId: 2,
|
||||
userLogin: 'danlimerick',
|
||||
userEmail: 'dan.limerick@gmail.com',
|
||||
permission: 4,
|
||||
permissionName: 'Admin',
|
||||
},
|
||||
])
|
||||
);
|
||||
|
||||
backendSrv.post = jest.fn();
|
||||
|
||||
const store = RootStore.create(
|
||||
{},
|
||||
{
|
||||
backendSrv: backendSrv,
|
||||
}
|
||||
);
|
||||
|
||||
wrapper = shallow(<Permissions backendSrv={backendSrv} isFolder={true} dashboardId={1} {...store} />);
|
||||
return wrapper.instance().loadStore(1, true);
|
||||
});
|
||||
|
||||
describe('when permission for a user is added', () => {
|
||||
it('should save permission to db', () => {
|
||||
const userItem = {
|
||||
id: 2,
|
||||
login: 'user2',
|
||||
};
|
||||
|
||||
wrapper
|
||||
.instance()
|
||||
.userPicked(userItem)
|
||||
.then(() => {
|
||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('when permission for team is added', () => {
|
||||
it('should save permission to db', () => {
|
||||
const teamItem = {
|
||||
id: 2,
|
||||
name: 'ug1',
|
||||
};
|
||||
|
||||
wrapper
|
||||
.instance()
|
||||
.teamPicked(teamItem)
|
||||
.then(() => {
|
||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -77,12 +77,12 @@ class Permissions extends Component<IProps, any> {
|
||||
|
||||
userPicked(user: User) {
|
||||
const { permissions } = this.props;
|
||||
permissions.addStoreItem({ userId: user.id, userLogin: user.login, permission: 1 });
|
||||
return permissions.addStoreItem({ userId: user.id, userLogin: user.login, permission: 1 });
|
||||
}
|
||||
|
||||
teamPicked(team: Team) {
|
||||
const { permissions } = this.props;
|
||||
permissions.addStoreItem({ teamId: team.id, team: team.name, permission: 1 });
|
||||
return permissions.addStoreItem({ teamId: team.id, team: team.name, permission: 1 });
|
||||
}
|
||||
|
||||
render() {
|
||||
|
||||
@@ -23,7 +23,6 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex }) =>
|
||||
return (
|
||||
<tr className={setClassNameHelper(item.inherited)}>
|
||||
<td style={{ width: '100%' }}>
|
||||
{/* style="width: 100%;" */}
|
||||
<i className={item.icon} />
|
||||
<span dangerouslySetInnerHTML={{ __html: item.nameHtml }} />
|
||||
</td>
|
||||
@@ -55,7 +54,7 @@ export default observer(({ item, removeItem, permissionChanged, itemIndex }) =>
|
||||
</td>
|
||||
<td>
|
||||
{!item.inherited ? (
|
||||
<a className="btn btn-inverse btn-small" onClick={handleRemoveItem}>
|
||||
<a className="btn btn-danger btn-small" onClick={handleRemoveItem}>
|
||||
<i className="fa fa-remove" />
|
||||
</a>
|
||||
) : null}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import './directives/dash_class';
|
||||
import './directives/dash_edit_link';
|
||||
import './directives/dropdown_typeahead';
|
||||
import './directives/metric_segment';
|
||||
import './directives/misc';
|
||||
|
||||
@@ -1,150 +0,0 @@
|
||||
define([
|
||||
'jquery',
|
||||
'angular',
|
||||
'../core_module',
|
||||
'lodash',
|
||||
],
|
||||
function ($, angular, coreModule, _) {
|
||||
'use strict';
|
||||
|
||||
var editViewMap = {
|
||||
'settings': { src: 'public/app/features/dashboard/partials/settings.html'},
|
||||
'annotations': { src: 'public/app/features/annotations/partials/editor.html'},
|
||||
'templating': { src: 'public/app/features/templating/partials/editor.html'},
|
||||
'history': { html: '<gf-dashboard-history dashboard="dashboard"></gf-dashboard-history>'},
|
||||
'timepicker': { src: 'public/app/features/dashboard/timepicker/dropdown.html' },
|
||||
'import': { html: '<dash-import dismiss="dismiss()"></dash-import>', isModal: true },
|
||||
'permissions': { html: '<dash-acl-modal dismiss="dismiss()"></dash-acl-modal>', isModal: true },
|
||||
'new-folder': {
|
||||
isModal: true,
|
||||
html: '<folder-modal dismiss="dismiss()"></folder-modal>',
|
||||
modalClass: 'modal--narrow'
|
||||
}
|
||||
};
|
||||
|
||||
coreModule.default.directive('dashEditorView', function($compile, $location, $rootScope) {
|
||||
return {
|
||||
restrict: 'A',
|
||||
link: function(scope, elem) {
|
||||
var editorScope;
|
||||
var modalScope;
|
||||
var lastEditView;
|
||||
|
||||
function hideEditorPane(hideToShowOtherView) {
|
||||
if (editorScope) {
|
||||
editorScope.dismiss(hideToShowOtherView);
|
||||
}
|
||||
}
|
||||
|
||||
function showEditorPane(evt, options) {
|
||||
if (options.editview) {
|
||||
_.defaults(options, editViewMap[options.editview]);
|
||||
}
|
||||
|
||||
if (lastEditView && lastEditView === options.editview) {
|
||||
hideEditorPane(false);
|
||||
return;
|
||||
}
|
||||
|
||||
hideEditorPane(true);
|
||||
|
||||
lastEditView = options.editview;
|
||||
editorScope = options.scope ? options.scope.$new() : scope.$new();
|
||||
|
||||
editorScope.dismiss = function(hideToShowOtherView) {
|
||||
if (modalScope) {
|
||||
modalScope.dismiss();
|
||||
modalScope = null;
|
||||
}
|
||||
|
||||
editorScope.$destroy();
|
||||
lastEditView = null;
|
||||
editorScope = null;
|
||||
elem.removeClass('dash-edit-view--open');
|
||||
|
||||
if (!hideToShowOtherView) {
|
||||
setTimeout(function() {
|
||||
elem.empty();
|
||||
}, 250);
|
||||
}
|
||||
|
||||
if (options.editview) {
|
||||
var urlParams = $location.search();
|
||||
if (options.editview === urlParams.editview) {
|
||||
delete urlParams.editview;
|
||||
|
||||
// even though we always are in apply phase here
|
||||
// some angular bug is causing location search updates to
|
||||
// not happen always so this is a hack fix or that
|
||||
setTimeout(function() {
|
||||
$rootScope.$apply(function() {
|
||||
$location.search(urlParams);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (options.isModal) {
|
||||
modalScope = $rootScope.$new();
|
||||
modalScope.$on("$destroy", function() {
|
||||
editorScope.dismiss();
|
||||
});
|
||||
|
||||
$rootScope.appEvent('show-modal', {
|
||||
templateHtml: options.html,
|
||||
scope: modalScope,
|
||||
backdrop: 'static',
|
||||
modalClass: options.modalClass,
|
||||
});
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
var view;
|
||||
if (options.src) {
|
||||
view = angular.element(document.createElement('div'));
|
||||
view.html('<div class="tabbed-view" ng-include="' + "'" + options.src + "'" + '"></div>');
|
||||
} else {
|
||||
view = angular.element(document.createElement('div'));
|
||||
view.addClass('tabbed-view');
|
||||
view.html(options.html);
|
||||
}
|
||||
|
||||
$compile(view)(editorScope);
|
||||
|
||||
setTimeout(function() {
|
||||
elem.empty();
|
||||
elem.append(view);
|
||||
setTimeout(function() {
|
||||
elem.addClass('dash-edit-view--open');
|
||||
}, 10);
|
||||
}, 10);
|
||||
}
|
||||
|
||||
scope.$watch("ctrl.dashboardViewState.state.editview", function(newValue, oldValue) {
|
||||
if (newValue) {
|
||||
showEditorPane(null, {editview: newValue});
|
||||
} else if (oldValue) {
|
||||
if (lastEditView === oldValue) {
|
||||
hideEditorPane();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
scope.$on("$destroy", hideEditorPane);
|
||||
|
||||
scope.onAppEvent('hide-dash-editor', function() {
|
||||
hideEditorPane(false);
|
||||
});
|
||||
|
||||
scope.onAppEvent('show-dash-editor', showEditorPane);
|
||||
|
||||
scope.onAppEvent('panel-fullscreen-enter', function() {
|
||||
scope.appEvent('hide-dash-editor');
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
<permissions
|
||||
error="{{ctrl.error}}"
|
||||
newType="ctrl.newType"
|
||||
aclTypes="{{ctrl.aclTypes}}"
|
||||
typeChanged="ctrl.typeChanged"
|
||||
dashboardId="ctrl.dashboard.id"
|
||||
backendSrv="ctrl.backendSrv" />
|
||||
|
||||
<div class="gf-form-group">
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</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">
|
||||
<button type="button" class="btn btn-danger" ng-disabled="!ctrl.canUpdate" ng-click="ctrl.update()">
|
||||
Update Permissions
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="empty-list-cta">
|
||||
<div class="grafana-info-box">
|
||||
<h5>What are Permissions?</h5>
|
||||
<p>An Access Control List (ACL) model is used for to limit access to Dashboard Folders. A user or a Team can be assigned permissions for a folder or for a single dashboard.</p>
|
||||
<p>The permissions that can be assigned for a folder/dashboard are:</p>
|
||||
<p>View, Edit and Admin.</p>
|
||||
Checkout the <a class="external-link" target="_blank" href="http://docs.grafana.org/reference/dashboard_folders/">Dashboard Folders documentation</a> for more information.
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<!-- <br> -->
|
||||
<!-- <br> -->
|
||||
<!-- <br> -->
|
||||
<!-- -->
|
||||
<!-- <div class="permissionlist"> -->
|
||||
<!-- <div class="permissionlist__section"> -->
|
||||
<!-- <div class="permissionlist__section-header"> -->
|
||||
<!-- <h6>Permissions</h6> -->
|
||||
<!-- </div> -->
|
||||
<!-- <table class="filter-table form-inline"> -->
|
||||
<!-- <thead> -->
|
||||
<!-- <tr> -->
|
||||
<!-- <th style="width: 50px;"></th> -->
|
||||
<!-- <th>Name</th> -->
|
||||
<!-- <th style="width: 220px;">Permission</th> -->
|
||||
<!-- <th style="width: 120px"></th> -->
|
||||
<!-- </tr> -->
|
||||
<!-- </thead> -->
|
||||
<!-- <tbody> -->
|
||||
<!-- <tr ng-repeat="permission in ctrl.userPermissions" class="permissionlist__item"> -->
|
||||
<!-- <td><i class="fa fa-fw fa-user"></i></td> -->
|
||||
<!-- <td>{{permission.userLogin}}</td> -->
|
||||
<!-- <td class="text-right"> -->
|
||||
<!-- <a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small"> -->
|
||||
<!-- <i class="fa fa-remove"></i> -->
|
||||
<!-- </a> -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- <tr ng-repeat="permission in ctrl.teamPermissions" class="permissionlist__item"> -->
|
||||
<!-- <td><i class="fa fa-fw fa-users"></i></td> -->
|
||||
<!-- <td>{{permission.team}}</td> -->
|
||||
<!-- <td><select class="gf-form-input gf-size-auto" ng-model="permission.permissions" ng-options="p.value as p.text for p in ctrl.permissionTypeOptions" ng-change="ctrl.updatePermission(permission)"></select></td> -->
|
||||
<!-- <td class="text-right"> -->
|
||||
<!-- <a ng-click="ctrl.removePermission(permission)" class="btn btn-danger btn-small"> -->
|
||||
<!-- <i class="fa fa-remove"></i> -->
|
||||
<!-- </a> -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- <tr ng-repeat="role in ctrl.roles" class="permissionlist__item"> -->
|
||||
<!-- <td></td> -->
|
||||
<!-- <td>{{role.name}}</td> -->
|
||||
<!-- <td><select class="gf-form-input gf-size-auto" ng-model="role.permissions" ng-options="p.value as p.text for p in ctrl.roleOptions" ng-change="ctrl.updatePermission(role)"></select></td> -->
|
||||
<!-- <td class="text-right"> -->
|
||||
<!-- -->
|
||||
<!-- </td> -->
|
||||
<!-- </tr> -->
|
||||
<!-- </tbody> -->
|
||||
<!-- </table> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
<!-- </div> -->
|
||||
@@ -1,207 +0,0 @@
|
||||
import coreModule from 'app/core/core_module';
|
||||
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 = [
|
||||
{ value: 'Group', text: 'Team' },
|
||||
{ value: 'User', text: 'User' },
|
||||
{ value: 'Viewer', text: 'Everyone With Viewer Role' },
|
||||
{ value: 'Editor', text: 'Everyone With Editor Role' },
|
||||
];
|
||||
|
||||
newType: string;
|
||||
canUpdate: boolean;
|
||||
error: string;
|
||||
|
||||
readonly duplicateError = 'This permission exists already.';
|
||||
|
||||
/** @ngInject */
|
||||
constructor(private backendSrv, private $sce, private $scope) {
|
||||
this.items = [];
|
||||
this.resetNewType();
|
||||
this.getAcl(this.dashboard.id);
|
||||
}
|
||||
|
||||
resetNewType() {
|
||||
this.newType = 'Group';
|
||||
}
|
||||
|
||||
getAcl(dashboardId: number) {
|
||||
return this.backendSrv.get(`/api/dashboards/id/${dashboardId}/acl`).then(result => {
|
||||
this.items = _.map(result, this.prepareViewModel.bind(this));
|
||||
this.sortItems();
|
||||
});
|
||||
}
|
||||
|
||||
sortItems() {
|
||||
this.items = _.orderBy(this.items, ['sortRank', 'sortName'], ['desc', 'asc']);
|
||||
}
|
||||
|
||||
prepareViewModel(item: DashboardAcl): DashboardAcl {
|
||||
item.inherited =
|
||||
!this.meta.isFolder && this.dashboard.id !== item.dashboardId;
|
||||
item.sortRank = 0;
|
||||
|
||||
if (item.userId > 0) {
|
||||
item.icon = 'fa fa-fw fa-user';
|
||||
item.nameHtml = this.$sce.trustAsHtml(item.userLogin);
|
||||
item.sortName = item.userLogin;
|
||||
item.sortRank = 10;
|
||||
} else if (item.teamId > 0) {
|
||||
item.icon = 'fa fa-fw fa-users';
|
||||
item.nameHtml = this.$sce.trustAsHtml(item.team);
|
||||
item.sortName = item.team;
|
||||
item.sortRank = 20;
|
||||
} else if (item.role) {
|
||||
item.icon = 'fa fa-fw fa-street-view';
|
||||
item.nameHtml = this.$sce.trustAsHtml(`Everyone with <span class="query-keyword">${item.role}</span> Role`);
|
||||
item.sortName = item.role;
|
||||
item.sortRank = 30;
|
||||
if (item.role === 'Viewer') {
|
||||
item.sortRank += 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (item.inherited) {
|
||||
item.sortRank += 100;
|
||||
}
|
||||
|
||||
return item;
|
||||
}
|
||||
|
||||
update() {
|
||||
var updated = [];
|
||||
for (let item of this.items) {
|
||||
if (item.inherited) {
|
||||
continue;
|
||||
}
|
||||
updated.push({
|
||||
id: item.id,
|
||||
userId: item.userId,
|
||||
teamId: item.teamId,
|
||||
role: item.role,
|
||||
permission: item.permission,
|
||||
});
|
||||
}
|
||||
|
||||
return this.backendSrv
|
||||
.post(`/api/dashboards/id/${this.dashboard.id}/acl`, {
|
||||
items: updated,
|
||||
})
|
||||
.then(() => {
|
||||
this.canUpdate = false;
|
||||
});
|
||||
}
|
||||
|
||||
typeChanged() {
|
||||
if (this.newType === 'Viewer' || this.newType === 'Editor') {
|
||||
this.addNewItem({ permission: 1, role: this.newType });
|
||||
this.canUpdate = true;
|
||||
this.resetNewType();
|
||||
}
|
||||
}
|
||||
|
||||
permissionChanged() {
|
||||
this.canUpdate = true;
|
||||
}
|
||||
|
||||
addNewItem(item) {
|
||||
if (!this.isValid(item)) {
|
||||
return;
|
||||
}
|
||||
this.error = '';
|
||||
|
||||
item.dashboardId = this.dashboard.id;
|
||||
|
||||
this.items.push(this.prepareViewModel(item));
|
||||
this.sortItems();
|
||||
|
||||
this.canUpdate = true;
|
||||
}
|
||||
|
||||
isValid(item) {
|
||||
const dupe = _.find(this.items, it => {
|
||||
return this.isDuplicate(it, item);
|
||||
});
|
||||
|
||||
if (dupe) {
|
||||
this.error = this.duplicateError;
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
isDuplicate(origItem, newItem) {
|
||||
if (origItem.inherited) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return (
|
||||
(origItem.role && newItem.role && origItem.role === newItem.role) ||
|
||||
(origItem.userId && newItem.userId && origItem.userId === newItem.userId) ||
|
||||
(origItem.teamId && newItem.teamId && origItem.teamId === newItem.teamId)
|
||||
);
|
||||
}
|
||||
|
||||
userPicked(user) {
|
||||
this.addNewItem({ userId: user.id, userLogin: user.login, permission: 1 });
|
||||
this.$scope.$broadcast('user-picker-reset');
|
||||
}
|
||||
|
||||
groupPicked(group) {
|
||||
this.addNewItem({ teamId: group.id, team: group.name, permission: 1 });
|
||||
this.$scope.$broadcast('team-picker-reset');
|
||||
}
|
||||
|
||||
removeItem(index) {
|
||||
this.items.splice(index, 1);
|
||||
this.canUpdate = true;
|
||||
}
|
||||
}
|
||||
|
||||
export function dashAclModal() {
|
||||
return {
|
||||
restrict: 'E',
|
||||
templateUrl: 'public/app/features/dashboard/acl/acl.html',
|
||||
controller: AclCtrl,
|
||||
bindToController: true,
|
||||
controllerAs: 'ctrl',
|
||||
scope: {
|
||||
dashboard: '=',
|
||||
meta: '=',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
export interface FormModel {
|
||||
dashboardId: number;
|
||||
userId?: number;
|
||||
teamId?: number;
|
||||
PermissionType: number;
|
||||
}
|
||||
|
||||
export interface DashboardAcl {
|
||||
id?: number;
|
||||
dashboardId?: number;
|
||||
userId?: number;
|
||||
userLogin?: string;
|
||||
userEmail?: string;
|
||||
teamId?: number;
|
||||
team?: string;
|
||||
permission?: number;
|
||||
permissionName?: string;
|
||||
role?: string;
|
||||
icon?: string;
|
||||
nameHtml?: string;
|
||||
inherited?: boolean;
|
||||
sortName?: string;
|
||||
sortRank?: number;
|
||||
}
|
||||
|
||||
coreModule.directive('dashAclModal', dashAclModal);
|
||||
@@ -1,169 +0,0 @@
|
||||
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();
|
||||
});
|
||||
});
|
||||
@@ -23,7 +23,6 @@ import './repeat_option/repeat_option';
|
||||
import './dashgrid/DashboardGridDirective';
|
||||
import './dashgrid/PanelLoader';
|
||||
import './dashgrid/RowOptions';
|
||||
import './acl/acl';
|
||||
import './folder_picker/folder_picker';
|
||||
import './move_to_folder_modal/move_to_folder';
|
||||
import './settings/settings';
|
||||
@@ -31,14 +30,12 @@ import './settings/settings';
|
||||
import coreModule from 'app/core/core_module';
|
||||
import { DashboardListCtrl } from './dashboard_list_ctrl';
|
||||
import { FolderDashboardsCtrl } from './folder_dashboards_ctrl';
|
||||
import { FolderPermissionsCtrl } from './folder_permissions_ctrl';
|
||||
import { FolderSettingsCtrl } from './folder_settings_ctrl';
|
||||
import { DashboardImportCtrl } from './dashboard_import_ctrl';
|
||||
import { CreateFolderCtrl } from './create_folder_ctrl';
|
||||
|
||||
coreModule.controller('DashboardListCtrl', DashboardListCtrl);
|
||||
coreModule.controller('FolderDashboardsCtrl', FolderDashboardsCtrl);
|
||||
coreModule.controller('FolderPermissionsCtrl', FolderPermissionsCtrl);
|
||||
coreModule.controller('FolderSettingsCtrl', FolderSettingsCtrl);
|
||||
coreModule.controller('DashboardImportCtrl', DashboardImportCtrl);
|
||||
coreModule.controller('CreateFolderCtrl', CreateFolderCtrl);
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
<page-header model="ctrl.navModel"></page-header>
|
||||
|
||||
<div class="page-container page-body">
|
||||
<dash-acl-modal ng-if="ctrl.dashboard && ctrl.meta"
|
||||
dashboard="ctrl.dashboard"
|
||||
meta="ctrl.meta">
|
||||
</dash-acl-modal>
|
||||
<dashboard-permissions ng-if="ctrl.dashboard && ctrl.meta"
|
||||
dashboardId="ctrl.dashboard.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -97,10 +97,10 @@
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === 'permissions'" >
|
||||
<h3 class="dashboard-settings__header">Permissions</h3>
|
||||
<dash-acl-modal ng-if="ctrl.dashboard"
|
||||
dashboard="ctrl.dashboard"
|
||||
meta="ctrl.dashboard.meta">
|
||||
</dash-acl-modal>
|
||||
<dashboard-permissions ng-if="ctrl.dashboard"
|
||||
dashboardId="ctrl.dashboard.id"
|
||||
backendSrv="ctrl.backendSrv">
|
||||
</dashboard-permissions>
|
||||
</div>
|
||||
|
||||
<div class="dashboard-settings__content" ng-if="ctrl.viewId === '404'">
|
||||
|
||||
@@ -11,12 +11,11 @@ describe('PermissionsStore', () => {
|
||||
{ id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
|
||||
{
|
||||
id: 4,
|
||||
dashboardId: 1,
|
||||
userId: 2,
|
||||
userLogin: 'danlimerick',
|
||||
userEmail: 'dan.limerick@gmail.com',
|
||||
permission: 4,
|
||||
permissionName: 'Admin',
|
||||
dashboardId: 10,
|
||||
permission: 1,
|
||||
permissionName: 'View',
|
||||
teamId: 1,
|
||||
teamName: 'MyTestTeam',
|
||||
},
|
||||
])
|
||||
);
|
||||
@@ -33,7 +32,7 @@ describe('PermissionsStore', () => {
|
||||
}
|
||||
);
|
||||
|
||||
return store.load(1, true);
|
||||
return store.load(1, false);
|
||||
});
|
||||
|
||||
it('should save update on permission change', () => {
|
||||
@@ -72,4 +71,78 @@ describe('PermissionsStore', () => {
|
||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||
expect(backendSrv.post.mock.calls[0][0]).toBe('/api/dashboards/id/1/acl');
|
||||
});
|
||||
|
||||
describe('when duplicate user permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
const newItem = {
|
||||
userId: 10,
|
||||
userLogin: 'tester1',
|
||||
permission: 1,
|
||||
};
|
||||
store.addStoreItem(newItem);
|
||||
store.addStoreItem(newItem);
|
||||
});
|
||||
|
||||
it('should return a validation error', () => {
|
||||
expect(store.items.length).toBe(4);
|
||||
expect(store.error).toBe('This permission exists already.');
|
||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when duplicate team permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
const newItem = {
|
||||
teamId: 1,
|
||||
teamName: 'testerteam',
|
||||
permission: 1,
|
||||
};
|
||||
store.addStoreItem(newItem);
|
||||
store.addStoreItem(newItem);
|
||||
});
|
||||
|
||||
it('should return a validation error', () => {
|
||||
expect(store.items.length).toBe(4);
|
||||
expect(store.error).toBe('This permission exists already.');
|
||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when duplicate role permissions are added', () => {
|
||||
beforeEach(() => {
|
||||
const newItem = {
|
||||
team: 'MyTestTeam',
|
||||
teamId: 1,
|
||||
permission: 1,
|
||||
};
|
||||
store.addStoreItem(newItem);
|
||||
store.addStoreItem(newItem);
|
||||
});
|
||||
|
||||
it('should return a validation error', () => {
|
||||
expect(store.items.length).toBe(4);
|
||||
expect(store.error).toBe('This permission exists already.');
|
||||
expect(backendSrv.post.mock.calls.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when one inherited and one not inherited team permission are added', () => {
|
||||
beforeEach(() => {
|
||||
const teamItem = {
|
||||
team: 'MyTestTeam',
|
||||
dashboardId: 1,
|
||||
teamId: 1,
|
||||
permission: 2,
|
||||
};
|
||||
store.addStoreItem(teamItem);
|
||||
});
|
||||
|
||||
it('should not throw a validation error', () => {
|
||||
expect(store.error).toBe(null);
|
||||
});
|
||||
|
||||
it('should add both permissions', () => {
|
||||
expect(store.items.length).toBe(4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -57,12 +57,12 @@ export const PermissionsStore = types
|
||||
}
|
||||
|
||||
self.items.push(prepareItem(item, self.dashboardId, self.isFolder));
|
||||
updateItems(self);
|
||||
return updateItems(self);
|
||||
}),
|
||||
removeStoreItem: flow(function* removeStoreItem(idx: number) {
|
||||
self.error = null;
|
||||
self.items.splice(idx, 1);
|
||||
updateItems(self);
|
||||
return updateItems(self);
|
||||
}),
|
||||
updatePermissionOnIndex: flow(function* updatePermissionOnIndex(
|
||||
idx: number,
|
||||
@@ -71,7 +71,7 @@ export const PermissionsStore = types
|
||||
) {
|
||||
self.error = null;
|
||||
self.items[idx].updatePermission(permission, permissionName);
|
||||
updateItems(self);
|
||||
return updateItems(self);
|
||||
}),
|
||||
setNewType(newType: string) {
|
||||
self.newType = newType;
|
||||
@@ -118,8 +118,7 @@ const prepareServerResponse = (response, dashboardId: number, isFolder: boolean)
|
||||
};
|
||||
|
||||
const prepareItem = (item, dashboardId: number, isFolder: boolean) => {
|
||||
item.inherited = !isFolder && dashboardId !== item.dashboardId;
|
||||
|
||||
item.inherited = !isFolder && item.dashboardId > 0 && dashboardId !== item.dashboardId;
|
||||
item.sortRank = 0;
|
||||
if (item.userId > 0) {
|
||||
item.icon = 'fa fa-fw fa-user';
|
||||
|
||||
Reference in New Issue
Block a user