瀏覽代碼

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.
Daniel Lee 8 年之前
父節點
當前提交
50b20a0e5a

+ 2 - 7
public/app/containers/ManageDashboards/FolderPermissions.tsx

@@ -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>
     );

+ 2 - 2
public/app/core/angular_wrappers.ts

@@ -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']);
 }

+ 29 - 0
public/app/core/components/Permissions/DashboardPermissions.tsx

@@ -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 - 0
public/app/core/components/Permissions/Permissions.jest.tsx

@@ -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');
+        });
+    });
+  });
+});

+ 2 - 2
public/app/core/components/Permissions/Permissions.tsx

@@ -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() {

+ 1 - 2
public/app/core/components/Permissions/PermissionsListItem.tsx

@@ -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}

+ 0 - 1
public/app/core/core.ts

@@ -1,5 +1,4 @@
 import './directives/dash_class';
-import './directives/dash_edit_link';
 import './directives/dropdown_typeahead';
 import './directives/metric_segment';
 import './directives/misc';

+ 0 - 150
public/app/core/directives/dash_edit_link.js

@@ -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');
-        });
-      }
-    };
-  });
-});
-

+ 0 - 132
public/app/features/dashboard/acl/acl.html

@@ -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&#45;header"> -->
-  <!--       <h6>Permissions</h6> -->
-  <!--     </div> -->
-  <!--     <table class="filter&#45;table form&#45;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&#45;repeat="permission in ctrl.userPermissions" class="permissionlist__item"> -->
-  <!--           <td><i class="fa fa&#45;fw fa&#45;user"></i></td> -->
-  <!--           <td>{{permission.userLogin}}</td> -->
-  <!--           <td class="text&#45;right"> -->
-  <!--             <a ng&#45;click="ctrl.removePermission(permission)" class="btn btn&#45;danger btn&#45;small"> -->
-  <!--               <i class="fa fa&#45;remove"></i> -->
-  <!--             </a> -->
-  <!--           </td> -->
-  <!--         </tr> -->
-  <!--         <tr ng&#45;repeat="permission in ctrl.teamPermissions" class="permissionlist__item"> -->
-  <!--           <td><i class="fa fa&#45;fw fa&#45;users"></i></td> -->
-  <!--           <td>{{permission.team}}</td> -->
-  <!--           <td><select class="gf&#45;form&#45;input gf&#45;size&#45;auto" ng&#45;model="permission.permissions" ng&#45;options="p.value as p.text for p in ctrl.permissionTypeOptions" ng&#45;change="ctrl.updatePermission(permission)"></select></td> -->
-  <!--           <td class="text&#45;right"> -->
-  <!--             <a ng&#45;click="ctrl.removePermission(permission)" class="btn btn&#45;danger btn&#45;small"> -->
-  <!--               <i class="fa fa&#45;remove"></i> -->
-  <!--             </a> -->
-  <!--           </td> -->
-  <!--         </tr> -->
-  <!--         <tr ng&#45;repeat="role in ctrl.roles" class="permissionlist__item"> -->
-  <!--           <td></td> -->
-  <!--           <td>{{role.name}}</td> -->
-  <!--           <td><select class="gf&#45;form&#45;input gf&#45;size&#45;auto" ng&#45;model="role.permissions" ng&#45;options="p.value as p.text for p in ctrl.roleOptions" ng&#45;change="ctrl.updatePermission(role)"></select></td> -->
-  <!--           <td class="text&#45;right"> -->
-  <!--  -->
-  <!--           </td> -->
-  <!--         </tr> -->
-  <!--       </tbody> -->
-  <!--     </table> -->
-  <!--   </div> -->
-  <!--   </div> -->
-  <!-- </div> -->

+ 0 - 207
public/app/features/dashboard/acl/acl.ts

@@ -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);

+ 0 - 169
public/app/features/dashboard/acl/specs/acl.jest.ts

@@ -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();
-  });
-});

+ 0 - 3
public/app/features/dashboard/all.ts

@@ -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);

+ 3 - 4
public/app/features/dashboard/partials/folder_permissions.html

@@ -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>

+ 4 - 4
public/app/features/dashboard/settings/settings.html

@@ -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'">

+ 80 - 7
public/app/stores/PermissionsStore/PermissionsStore.jest.ts

@@ -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);
+    });
+  });
 });

+ 4 - 5
public/app/stores/PermissionsStore/PermissionsStore.ts

@@ -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';