فهرست منبع

Merge pull request #13273 from grafana/permissions-to-redux

Permissions to redux
Torkel Ödegaard 7 سال پیش
والد
کامیت
693f2fd8e9
29فایلهای تغییر یافته به همراه899 افزوده شده و 472 حذف شده
  1. 0 2
      public/app/core/angular_wrappers.ts
  2. 67 46
      public/app/core/components/PermissionList/AddPermission.tsx
  3. 0 0
      public/app/core/components/PermissionList/DisabledPermissionListItem.tsx
  4. 17 19
      public/app/core/components/PermissionList/PermissionList.tsx
  5. 100 0
      public/app/core/components/PermissionList/PermissionListItem.tsx
  6. 0 0
      public/app/core/components/PermissionList/PermissionsInfo.tsx
  7. 0 90
      public/app/core/components/Permissions/AddPermissions.test.tsx
  8. 0 71
      public/app/core/components/Permissions/DashboardPermissions.tsx
  9. 0 5
      public/app/core/components/Permissions/FolderInfo.ts
  10. 0 91
      public/app/core/components/Permissions/Permissions.tsx
  11. 0 91
      public/app/core/components/Permissions/PermissionsListItem.tsx
  12. 31 0
      public/app/core/reducers/processsAclItems.ts
  13. 31 0
      public/app/core/utils/acl.ts
  14. 6 0
      public/app/features/dashboard/all.ts
  15. 117 0
      public/app/features/dashboard/permissions/DashboardPermissions.tsx
  16. 115 0
      public/app/features/dashboard/state/actions.ts
  17. 24 0
      public/app/features/dashboard/state/reducers.test.ts
  18. 22 0
      public/app/features/dashboard/state/reducers.ts
  19. 62 32
      public/app/features/folders/FolderPermissions.tsx
  20. 1 0
      public/app/features/folders/FolderSettingsPage.test.tsx
  21. 104 4
      public/app/features/folders/state/actions.ts
  22. 73 18
      public/app/features/folders/state/reducers.test.ts
  23. 9 1
      public/app/features/folders/state/reducers.ts
  24. 2 0
      public/app/stores/configureStore.ts
  25. 91 0
      public/app/types/acl.ts
  26. 5 0
      public/app/types/dashboard.ts
  27. 10 1
      public/app/types/folder.ts
  28. 9 1
      public/app/types/index.ts
  29. 3 0
      scripts/webpack/webpack.common.js

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

@@ -5,7 +5,6 @@ import EmptyListCTA from './components/EmptyListCTA/EmptyListCTA';
 import { SearchResult } from './components/search/SearchResult';
 import { TagFilter } from './components/TagFilter/TagFilter';
 import { SideMenu } from './components/sidemenu/SideMenu';
-import DashboardPermissions from './components/Permissions/DashboardPermissions';
 
 export function registerAngularDirectives() {
   react2AngularDirective('passwordStrength', PasswordStrength, ['password']);
@@ -18,5 +17,4 @@ export function registerAngularDirectives() {
     ['onSelect', { watchDepth: 'reference' }],
     ['tagOptions', { watchDepth: 'reference' }],
   ]);
-  react2AngularDirective('dashboardPermissions', DashboardPermissions, ['backendSrv', 'dashboardId', 'folder']);
 }

+ 67 - 46
public/app/core/components/Permissions/AddPermissions.tsx → public/app/core/components/PermissionList/AddPermission.tsx

@@ -1,77 +1,90 @@
 import React, { Component } from 'react';
-import { observer } from 'mobx-react';
-import { aclTypes } from 'app/stores/PermissionsStore/PermissionsStore';
 import { UserPicker, User } from 'app/core/components/Picker/UserPicker';
 import { TeamPicker, Team } from 'app/core/components/Picker/TeamPicker';
 import DescriptionPicker, { OptionWithDescription } from 'app/core/components/Picker/DescriptionPicker';
-import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
+import {
+  dashboardPermissionLevels,
+  dashboardAclTargets,
+  AclTarget,
+  PermissionLevel,
+  NewDashboardAclItem,
+  OrgRole,
+} from 'app/types/acl';
 
 export interface Props {
-  permissions: any;
+  onAddPermission: (item: NewDashboardAclItem) => void;
+  onCancel: () => void;
 }
 
-@observer
-class AddPermissions extends Component<Props, any> {
+class AddPermissions extends Component<Props, NewDashboardAclItem> {
   constructor(props) {
     super(props);
+    this.state = this.getCleanState();
   }
 
-  componentWillMount() {
-    const { permissions } = this.props;
-    permissions.resetNewType();
+  getCleanState() {
+    return {
+      userId: 0,
+      teamId: 0,
+      type: AclTarget.Team,
+      permission: PermissionLevel.View,
+    };
   }
 
   onTypeChanged = evt => {
-    const { value } = evt.target;
-    const { permissions } = this.props;
-
-    permissions.setNewType(value);
+    const type = evt.target.value as AclTarget;
+
+    switch (type) {
+      case AclTarget.User:
+      case AclTarget.Team:
+        this.setState({ type: type, userId: 0, teamId: 0, role: undefined });
+        break;
+      case AclTarget.Editor:
+        this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Editor });
+        break;
+      case AclTarget.Viewer:
+        this.setState({ type: type, userId: 0, teamId: 0, role: OrgRole.Viewer });
+        break;
+    }
   };
 
   onUserSelected = (user: User) => {
-    const { permissions } = this.props;
-    if (!user) {
-      permissions.newItem.setUser(null, null);
-      return;
-    }
-    return permissions.newItem.setUser(user.id, user.login, user.avatarUrl);
+    this.setState({ userId: user ? user.id : 0 });
   };
 
   onTeamSelected = (team: Team) => {
-    const { permissions } = this.props;
-    if (!team) {
-      permissions.newItem.setTeam(null, null);
-      return;
-    }
-    return permissions.newItem.setTeam(team.id, team.name, team.avatarUrl);
+    this.setState({ teamId: team ? team.id : 0 });
   };
 
   onPermissionChanged = (permission: OptionWithDescription) => {
-    const { permissions } = this.props;
-    return permissions.newItem.setPermission(permission.value);
+    this.setState({ permission: permission.value });
   };
 
-  resetNewType() {
-    const { permissions } = this.props;
-    return permissions.resetNewType();
-  }
-
-  onSubmit = evt => {
+  onSubmit = async evt => {
     evt.preventDefault();
-    const { permissions } = this.props;
-    permissions.addStoreItem();
+    await this.props.onAddPermission(this.state);
+    this.setState(this.getCleanState());
   };
 
+  isValid() {
+    switch (this.state.type) {
+      case AclTarget.Team:
+        return this.state.teamId > 0;
+      case AclTarget.User:
+        return this.state.userId > 0;
+    }
+    return true;
+  }
+
   render() {
-    const { permissions } = this.props;
-    const newItem = permissions.newItem;
+    const { onCancel } = this.props;
+    const newItem = this.state;
     const pickerClassName = 'width-20';
-
-    const isValid = newItem.isValid();
+    const isValid = this.isValid();
 
     return (
       <div className="gf-form-inline cta-form">
-        <button className="cta-form__close btn btn-transparent" onClick={permissions.hideAddPermissions}>
+        <button className="cta-form__close btn btn-transparent" onClick={onCancel}>
           <i className="fa fa-close" />
         </button>
         <form name="addPermission" onSubmit={this.onSubmit}>
@@ -80,7 +93,7 @@ class AddPermissions extends Component<Props, any> {
             <div className="gf-form">
               <div className="gf-form-select-wrapper">
                 <select className="gf-form-input gf-size-auto" value={newItem.type} onChange={this.onTypeChanged}>
-                  {aclTypes.map((option, idx) => {
+                  {dashboardAclTargets.map((option, idx) => {
                     return (
                       <option key={idx} value={option.value}>
                         {option.text}
@@ -91,21 +104,29 @@ class AddPermissions extends Component<Props, any> {
               </div>
             </div>
 
-            {newItem.type === 'User' ? (
+            {newItem.type === AclTarget.User ? (
               <div className="gf-form">
-                <UserPicker onSelected={this.onUserSelected} value={newItem.userId} className={pickerClassName} />
+                <UserPicker
+                  onSelected={this.onUserSelected}
+                  value={newItem.userId.toString()}
+                  className={pickerClassName}
+                />
               </div>
             ) : null}
 
-            {newItem.type === 'Group' ? (
+            {newItem.type === AclTarget.Team ? (
               <div className="gf-form">
-                <TeamPicker onSelected={this.onTeamSelected} value={newItem.teamId} className={pickerClassName} />
+                <TeamPicker
+                  onSelected={this.onTeamSelected}
+                  value={newItem.teamId.toString()}
+                  className={pickerClassName}
+                />
               </div>
             ) : null}
 
             <div className="gf-form">
               <DescriptionPicker
-                optionsWithDesc={permissionOptions}
+                optionsWithDesc={dashboardPermissionLevels}
                 onSelected={this.onPermissionChanged}
                 value={newItem.permission}
                 disabled={false}

+ 0 - 0
public/app/core/components/Permissions/DisabledPermissionsListItem.tsx → public/app/core/components/PermissionList/DisabledPermissionListItem.tsx


+ 17 - 19
public/app/core/components/Permissions/PermissionsList.tsx → public/app/core/components/PermissionList/PermissionList.tsx

@@ -1,21 +1,20 @@
-import React, { Component } from 'react';
-import PermissionsListItem from './PermissionsListItem';
-import DisabledPermissionsListItem from './DisabledPermissionsListItem';
-import { observer } from 'mobx-react';
-import { FolderInfo } from './FolderInfo';
+import React, { PureComponent } from 'react';
+import PermissionsListItem from './PermissionListItem';
+import DisabledPermissionsListItem from './DisabledPermissionListItem';
+import { FolderInfo } from 'app/types';
+import { DashboardAcl } from 'app/types/acl';
 
 export interface Props {
-  permissions: any[];
-  removeItem: any;
-  permissionChanged: any;
-  fetching: boolean;
+  items: DashboardAcl[];
+  onRemoveItem: (item: DashboardAcl) => void;
+  onPermissionChanged: any;
+  isFetching: boolean;
   folderInfo?: FolderInfo;
 }
 
-@observer
-class PermissionsList extends Component<Props, any> {
+class PermissionList extends PureComponent<Props> {
   render() {
-    const { permissions, removeItem, permissionChanged, fetching, folderInfo } = this.props;
+    const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props;
 
     return (
       <table className="filter-table gf-form-group">
@@ -28,19 +27,18 @@ class PermissionsList extends Component<Props, any> {
               icon: 'fa fa-fw fa-street-view',
             }}
           />
-          {permissions.map((item, idx) => {
+          {items.map((item, idx) => {
             return (
               <PermissionsListItem
                 key={idx + 1}
                 item={item}
-                itemIndex={idx}
-                removeItem={removeItem}
-                permissionChanged={permissionChanged}
+                onRemoveItem={onRemoveItem}
+                onPermissionChanged={onPermissionChanged}
                 folderInfo={folderInfo}
               />
             );
           })}
-          {fetching === true && permissions.length < 1 ? (
+          {isFetching === true && items.length < 1 ? (
             <tr>
               <td colSpan={4}>
                 <em>Loading permissions...</em>
@@ -48,7 +46,7 @@ class PermissionsList extends Component<Props, any> {
             </tr>
           ) : null}
 
-          {fetching === false && permissions.length < 1 ? (
+          {isFetching === false && items.length < 1 ? (
             <tr>
               <td colSpan={4}>
                 <em>No permissions are set. Will only be accessible by admins.</em>
@@ -61,4 +59,4 @@ class PermissionsList extends Component<Props, any> {
   }
 }
 
-export default PermissionsList;
+export default PermissionList;

+ 100 - 0
public/app/core/components/PermissionList/PermissionListItem.tsx

@@ -0,0 +1,100 @@
+import React, { PureComponent } from 'react';
+import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
+import { dashboardPermissionLevels, DashboardAcl, PermissionLevel } from 'app/types/acl';
+import { FolderInfo } from 'app/types';
+
+const setClassNameHelper = inherited => {
+  return inherited ? 'gf-form-disabled' : '';
+};
+
+function ItemAvatar({ item }) {
+  if (item.userAvatarUrl) {
+    return <img className="filter-table__avatar" src={item.userAvatarUrl} />;
+  }
+  if (item.teamAvatarUrl) {
+    return <img className="filter-table__avatar" src={item.teamAvatarUrl} />;
+  }
+  if (item.role === 'Editor') {
+    return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-editor" />;
+  }
+
+  return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-viewer" />;
+}
+
+function ItemDescription({ item }) {
+  if (item.userId) {
+    return <span className="filter-table__weak-italic">(User)</span>;
+  }
+  if (item.teamId) {
+    return <span className="filter-table__weak-italic">(Team)</span>;
+  }
+  return <span className="filter-table__weak-italic">(Role)</span>;
+}
+
+interface Props {
+  item: DashboardAcl;
+  onRemoveItem: (item: DashboardAcl) => void;
+  onPermissionChanged: (item: DashboardAcl, level: PermissionLevel) => void;
+  folderInfo?: FolderInfo;
+}
+
+export default class PermissionsListItem extends PureComponent<Props> {
+  onPermissionChanged = option => {
+    this.props.onPermissionChanged(this.props.item, option.value as PermissionLevel);
+  };
+
+  onRemoveItem = () => {
+    this.props.onRemoveItem(this.props.item);
+  };
+
+  render() {
+    const { item, folderInfo } = this.props;
+    const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
+
+    return (
+      <tr className={setClassNameHelper(item.inherited)}>
+        <td style={{ width: '1%' }}>
+          <ItemAvatar item={item} />
+        </td>
+        <td style={{ width: '90%' }}>
+          {item.name} <ItemDescription item={item} />
+        </td>
+        <td>
+          {item.inherited &&
+            folderInfo && (
+              <em className="muted no-wrap">
+                Inherited from folder{' '}
+                <a className="text-link" href={`${folderInfo.url}/permissions`}>
+                  {folderInfo.title}
+                </a>{' '}
+              </em>
+            )}
+          {inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
+        </td>
+        <td className="query-keyword">Can</td>
+        <td>
+          <div className="gf-form">
+            <DescriptionPicker
+              optionsWithDesc={dashboardPermissionLevels}
+              onSelected={this.onPermissionChanged}
+              value={item.permission}
+              disabled={item.inherited}
+              className={'gf-form-input--form-dropdown-right'}
+            />
+          </div>
+        </td>
+        <td>
+          {!item.inherited ? (
+            <a className="btn btn-danger btn-small" onClick={this.onRemoveItem}>
+              <i className="fa fa-remove" />
+            </a>
+          ) : (
+            <button className="btn btn-inverse btn-small">
+              <i className="fa fa-lock" />
+            </button>
+          )}
+        </td>
+      </tr>
+    );
+  }
+}

+ 0 - 0
public/app/core/components/Permissions/PermissionsInfo.tsx → public/app/core/components/PermissionList/PermissionsInfo.tsx


+ 0 - 90
public/app/core/components/Permissions/AddPermissions.test.tsx

@@ -1,90 +0,0 @@
-import React from 'react';
-import { shallow } from 'enzyme';
-import AddPermissions from './AddPermissions';
-import { RootStore } from 'app/stores/RootStore/RootStore';
-import { getBackendSrv } from 'app/core/services/backend_srv';
-
-jest.mock('app/core/services/backend_srv', () => ({
-  getBackendSrv: () => {
-    return {
-      get: () => {
-        return Promise.resolve([
-          { id: 2, dashboardId: 1, role: 'Viewer', permission: 1, permissionName: 'View' },
-          { id: 3, dashboardId: 1, role: 'Editor', permission: 1, permissionName: 'Edit' },
-        ]);
-      },
-      post: jest.fn(() => Promise.resolve({})),
-    };
-  },
-}));
-
-describe('AddPermissions', () => {
-  let wrapper;
-  let store;
-  let instance;
-  const backendSrv: any = getBackendSrv();
-
-  beforeAll(() => {
-    store = RootStore.create({}, { backendSrv: backendSrv });
-    wrapper = shallow(<AddPermissions permissions={store.permissions} />);
-    instance = wrapper.instance();
-    return store.permissions.load(1, true, false);
-  });
-
-  describe('when permission for a user is added', () => {
-    it('should save permission to db', () => {
-      const evt = {
-        target: {
-          value: 'User',
-        },
-      };
-      const userItem = {
-        id: 2,
-        login: 'user2',
-      };
-
-      instance.onTypeChanged(evt);
-      instance.onUserSelected(userItem);
-
-      wrapper.update();
-
-      expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false);
-
-      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/permissions');
-    });
-  });
-
-  describe('when permission for team is added', () => {
-    it('should save permission to db', () => {
-      const evt = {
-        target: {
-          value: 'Group',
-        },
-      };
-
-      const teamItem = {
-        id: 2,
-        name: 'ug1',
-      };
-
-      instance.onTypeChanged(evt);
-      instance.onTeamSelected(teamItem);
-
-      wrapper.update();
-
-      expect(wrapper.find('[data-save-permission]').prop('disabled')).toBe(false);
-
-      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/permissions');
-    });
-  });
-
-  afterEach(() => {
-    backendSrv.post.mockClear();
-  });
-});

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

@@ -1,71 +0,0 @@
-import React, { Component } from 'react';
-import { observer } from 'mobx-react';
-import { store } from 'app/stores/store';
-import Permissions from 'app/core/components/Permissions/Permissions';
-import Tooltip from 'app/core/components/Tooltip/Tooltip';
-import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
-import AddPermissions from 'app/core/components/Permissions/AddPermissions';
-import SlideDown from 'app/core/components/Animations/SlideDown';
-import { FolderInfo } from './FolderInfo';
-
-export interface Props {
-  dashboardId: number;
-  folder?: FolderInfo;
-  backendSrv: any;
-}
-
-@observer
-class DashboardPermissions extends Component<Props, any> {
-  permissions: any;
-
-  constructor(props) {
-    super(props);
-    this.handleAddPermission = this.handleAddPermission.bind(this);
-    this.permissions = store.permissions;
-  }
-
-  handleAddPermission() {
-    this.permissions.toggleAddPermissions();
-  }
-
-  componentWillUnmount() {
-    this.permissions.hideAddPermissions();
-  }
-
-  render() {
-    const { dashboardId, folder, backendSrv } = this.props;
-
-    return (
-      <div>
-        <div className="dashboard-settings__header">
-          <div className="page-action-bar">
-            <h3 className="d-inline-block">Permissions</h3>
-            <Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
-              <i className="gicon gicon-question gicon--has-hover" />
-            </Tooltip>
-            <div className="page-action-bar__spacer" />
-            <button
-              className="btn btn-success pull-right"
-              onClick={this.handleAddPermission}
-              disabled={this.permissions.isAddPermissionsVisible}
-            >
-              <i className="fa fa-plus" /> Add Permission
-            </button>
-          </div>
-        </div>
-        <SlideDown in={this.permissions.isAddPermissionsVisible}>
-          <AddPermissions permissions={this.permissions} />
-        </SlideDown>
-        <Permissions
-          permissions={this.permissions}
-          isFolder={false}
-          dashboardId={dashboardId}
-          folderInfo={folder}
-          backendSrv={backendSrv}
-        />
-      </div>
-    );
-  }
-}
-
-export default DashboardPermissions;

+ 0 - 5
public/app/core/components/Permissions/FolderInfo.ts

@@ -1,5 +0,0 @@
-export interface FolderInfo {
-  id: number;
-  title: string;
-  url: string;
-}

+ 0 - 91
public/app/core/components/Permissions/Permissions.tsx

@@ -1,91 +0,0 @@
-import React, { Component } from 'react';
-import PermissionsList from './PermissionsList';
-import { observer } from 'mobx-react';
-import { FolderInfo } from './FolderInfo';
-
-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;
-  name?: string;
-  inherited?: boolean;
-  sortRank?: number;
-}
-
-export interface Props {
-  dashboardId: number;
-  folderInfo?: FolderInfo;
-  permissions?: any;
-  isFolder: boolean;
-  backendSrv: any;
-}
-
-@observer
-class Permissions extends Component<Props, any> {
-  constructor(props) {
-    super(props);
-    const { dashboardId, isFolder, folderInfo } = this.props;
-    this.permissionChanged = this.permissionChanged.bind(this);
-    this.typeChanged = this.typeChanged.bind(this);
-    this.removeItem = this.removeItem.bind(this);
-    this.loadStore(dashboardId, isFolder, folderInfo && folderInfo.id === 0);
-  }
-
-  loadStore(dashboardId, isFolder, isInRoot = false) {
-    return this.props.permissions.load(dashboardId, isFolder, isInRoot);
-  }
-
-  permissionChanged(index: number, permission: number, permissionName: string) {
-    const { permissions } = this.props;
-    permissions.updatePermissionOnIndex(index, permission, permissionName);
-  }
-
-  removeItem(index: number) {
-    const { permissions } = this.props;
-    permissions.removeStoreItem(index);
-  }
-
-  resetNewType() {
-    const { permissions } = this.props;
-    permissions.resetNewType();
-  }
-
-  typeChanged(evt) {
-    const { value } = evt.target;
-    const { permissions, dashboardId } = this.props;
-
-    if (value === 'Viewer' || value === 'Editor') {
-      permissions.addStoreItem({ permission: 1, role: value, dashboardId: dashboardId }, dashboardId);
-      this.resetNewType();
-      return;
-    }
-
-    permissions.setNewType(value);
-  }
-
-  render() {
-    const { permissions, folderInfo } = this.props;
-
-    return (
-      <div className="gf-form-group">
-        <PermissionsList
-          permissions={permissions.items}
-          removeItem={this.removeItem}
-          permissionChanged={this.permissionChanged}
-          fetching={permissions.fetching}
-          folderInfo={folderInfo}
-        />
-      </div>
-    );
-  }
-}
-
-export default Permissions;

+ 0 - 91
public/app/core/components/Permissions/PermissionsListItem.tsx

@@ -1,91 +0,0 @@
-import React from 'react';
-import { observer } from 'mobx-react';
-import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
-import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
-
-const setClassNameHelper = inherited => {
-  return inherited ? 'gf-form-disabled' : '';
-};
-
-function ItemAvatar({ item }) {
-  if (item.userAvatarUrl) {
-    return <img className="filter-table__avatar" src={item.userAvatarUrl} />;
-  }
-  if (item.teamAvatarUrl) {
-    return <img className="filter-table__avatar" src={item.teamAvatarUrl} />;
-  }
-  if (item.role === 'Editor') {
-    return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-editor" />;
-  }
-
-  return <i style={{ width: '25px', height: '25px' }} className="gicon gicon-viewer" />;
-}
-
-function ItemDescription({ item }) {
-  if (item.userId) {
-    return <span className="filter-table__weak-italic">(User)</span>;
-  }
-  if (item.teamId) {
-    return <span className="filter-table__weak-italic">(Team)</span>;
-  }
-  return <span className="filter-table__weak-italic">(Role)</span>;
-}
-
-export default observer(({ item, removeItem, permissionChanged, itemIndex, folderInfo }) => {
-  const handleRemoveItem = evt => {
-    evt.preventDefault();
-    removeItem(itemIndex);
-  };
-
-  const handleChangePermission = permissionOption => {
-    permissionChanged(itemIndex, permissionOption.value, permissionOption.label);
-  };
-
-  const inheritedFromRoot = item.dashboardId === -1 && !item.inherited;
-
-  return (
-    <tr className={setClassNameHelper(item.inherited)}>
-      <td style={{ width: '1%' }}>
-        <ItemAvatar item={item} />
-      </td>
-      <td style={{ width: '90%' }}>
-        {item.name} <ItemDescription item={item} />
-      </td>
-      <td>
-        {item.inherited &&
-          folderInfo && (
-            <em className="muted no-wrap">
-              Inherited from folder{' '}
-              <a className="text-link" href={`${folderInfo.url}/permissions`}>
-                {folderInfo.title}
-              </a>{' '}
-            </em>
-          )}
-        {inheritedFromRoot && <em className="muted no-wrap">Default Permission</em>}
-      </td>
-      <td className="query-keyword">Can</td>
-      <td>
-        <div className="gf-form">
-          <DescriptionPicker
-            optionsWithDesc={permissionOptions}
-            onSelected={handleChangePermission}
-            value={item.permission}
-            disabled={item.inherited}
-            className={'gf-form-input--form-dropdown-right'}
-          />
-        </div>
-      </td>
-      <td>
-        {!item.inherited ? (
-          <a className="btn btn-danger btn-small" onClick={handleRemoveItem}>
-            <i className="fa fa-remove" />
-          </a>
-        ) : (
-          <button className="btn btn-inverse btn-small">
-            <i className="fa fa-lock" />
-          </button>
-        )}
-      </td>
-    </tr>
-  );
-});

+ 31 - 0
public/app/core/reducers/processsAclItems.ts

@@ -0,0 +1,31 @@
+import { DashboardAcl, DashboardAclDTO } from 'app/types/acl';
+
+export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] {
+  return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name));
+}
+
+function processAclItem(dto: DashboardAclDTO): DashboardAcl {
+  const item = dto as DashboardAcl;
+
+  item.sortRank = 0;
+  if (item.userId > 0) {
+    item.name = item.userLogin;
+    item.sortRank = 10;
+  } else if (item.teamId > 0) {
+    item.name = item.team;
+    item.sortRank = 20;
+  } else if (item.role) {
+    item.icon = 'fa fa-fw fa-street-view';
+    item.name = item.role;
+    item.sortRank = 30;
+    if (item.role === 'Editor') {
+      item.sortRank += 1;
+    }
+  }
+
+  if (item.inherited) {
+    item.sortRank += 100;
+  }
+
+  return item;
+}

+ 31 - 0
public/app/core/utils/acl.ts

@@ -0,0 +1,31 @@
+import { DashboardAcl, DashboardAclDTO } from 'app/types/acl';
+
+export function processAclItems(items: DashboardAclDTO[]): DashboardAcl[] {
+  return items.map(processAclItem).sort((a, b) => b.sortRank - a.sortRank || a.name.localeCompare(b.name));
+}
+
+function processAclItem(dto: DashboardAclDTO): DashboardAcl {
+  const item = dto as DashboardAcl;
+
+  item.sortRank = 0;
+  if (item.userId > 0) {
+    item.name = item.userLogin;
+    item.sortRank = 10;
+  } else if (item.teamId > 0) {
+    item.name = item.team;
+    item.sortRank = 20;
+  } else if (item.role) {
+    item.icon = 'fa fa-fw fa-street-view';
+    item.name = item.role;
+    item.sortRank = 30;
+    if (item.role === 'Editor') {
+      item.sortRank += 1;
+    }
+  }
+
+  if (item.inherited) {
+    item.sortRank += 100;
+  }
+
+  return item;
+}

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

@@ -30,6 +30,12 @@ import './settings/settings';
 import './panellinks/module';
 import './dashlinks/module';
 
+// angular wrappers
+import { react2AngularDirective } from 'app/core/utils/react2angular';
+import DashboardPermissions from './permissions/DashboardPermissions';
+
+react2AngularDirective('dashboardPermissions', DashboardPermissions, ['dashboardId', 'folder']);
+
 import coreModule from 'app/core/core_module';
 import { FolderDashboardsCtrl } from './folder_dashboards_ctrl';
 import { DashboardImportCtrl } from './dashboard_import_ctrl';

+ 117 - 0
public/app/features/dashboard/permissions/DashboardPermissions.tsx

@@ -0,0 +1,117 @@
+import React, { PureComponent } from 'react';
+import { connect } from 'react-redux';
+import Tooltip from 'app/core/components/Tooltip/Tooltip';
+import SlideDown from 'app/core/components/Animations/SlideDown';
+import { StoreState, FolderInfo } from 'app/types';
+import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
+import {
+  getDashboardPermissions,
+  addDashboardPermission,
+  removeDashboardPermission,
+  updateDashboardPermission,
+} from '../state/actions';
+import PermissionList from 'app/core/components/PermissionList/PermissionList';
+import AddPermission from 'app/core/components/PermissionList/AddPermission';
+import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
+import { store } from 'app/stores/configureStore';
+
+export interface Props {
+  dashboardId: number;
+  folder?: FolderInfo;
+  permissions: DashboardAcl[];
+  getDashboardPermissions: typeof getDashboardPermissions;
+  updateDashboardPermission: typeof updateDashboardPermission;
+  removeDashboardPermission: typeof removeDashboardPermission;
+  addDashboardPermission: typeof addDashboardPermission;
+}
+
+export interface State {
+  isAdding: boolean;
+}
+
+export class DashboardPermissions extends PureComponent<Props, State> {
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      isAdding: false,
+    };
+  }
+
+  componentDidMount() {
+    this.props.getDashboardPermissions(this.props.dashboardId);
+  }
+
+  onOpenAddPermissions = () => {
+    this.setState({ isAdding: true });
+  };
+
+  onRemoveItem = (item: DashboardAcl) => {
+    this.props.removeDashboardPermission(this.props.dashboardId, item);
+  };
+
+  onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
+    this.props.updateDashboardPermission(this.props.dashboardId, item, level);
+  };
+
+  onAddPermission = (newItem: NewDashboardAclItem) => {
+    return this.props.addDashboardPermission(this.props.dashboardId, newItem);
+  };
+
+  onCancelAddPermission = () => {
+    this.setState({ isAdding: false });
+  };
+
+  render() {
+    const { permissions, folder } = this.props;
+    const { isAdding } = this.state;
+    console.log('DashboardPermissions', this.props);
+
+    return (
+      <div>
+        <div className="dashboard-settings__header">
+          <div className="page-action-bar">
+            <h3 className="d-inline-block">Permissions</h3>
+            <Tooltip className="page-sub-heading-icon" placement="auto" content={PermissionsInfo}>
+              <i className="gicon gicon-question gicon--has-hover" />
+            </Tooltip>
+            <div className="page-action-bar__spacer" />
+            <button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
+              <i className="fa fa-plus" /> Add Permission
+            </button>
+          </div>
+        </div>
+        <SlideDown in={isAdding}>
+          <AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
+        </SlideDown>
+        <PermissionList
+          items={permissions}
+          onRemoveItem={this.onRemoveItem}
+          onPermissionChanged={this.onPermissionChanged}
+          isFetching={false}
+          folderInfo={folder}
+        />
+      </div>
+    );
+  }
+}
+
+function connectWithStore(WrappedComponent, ...args) {
+  const ConnectedWrappedComponent = connect(...args)(WrappedComponent);
+  return props => {
+    return <ConnectedWrappedComponent {...props} store={store} />;
+  };
+}
+
+const mapStateToProps = (state: StoreState) => ({
+  permissions: state.dashboard.permissions,
+});
+
+const mapDispatchToProps = {
+  getDashboardPermissions,
+  addDashboardPermission,
+  removeDashboardPermission,
+  updateDashboardPermission,
+};
+
+export default connectWithStore(DashboardPermissions, mapStateToProps, mapDispatchToProps);

+ 115 - 0
public/app/features/dashboard/state/actions.ts

@@ -0,0 +1,115 @@
+import { StoreState } from 'app/types';
+import { ThunkAction } from 'redux-thunk';
+import { getBackendSrv } from 'app/core/services/backend_srv';
+
+import {
+  DashboardAcl,
+  DashboardAclDTO,
+  PermissionLevel,
+  DashboardAclUpdateDTO,
+  NewDashboardAclItem,
+} from 'app/types/acl';
+
+export enum ActionTypes {
+  LoadDashboardPermissions = 'LOAD_DASHBOARD_PERMISSIONS',
+}
+
+export interface LoadDashboardPermissionsAction {
+  type: ActionTypes.LoadDashboardPermissions;
+  payload: DashboardAcl[];
+}
+
+export type Action = LoadDashboardPermissionsAction;
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
+
+export const loadDashboardPermissions = (items: DashboardAclDTO[]): LoadDashboardPermissionsAction => ({
+  type: ActionTypes.LoadDashboardPermissions,
+  payload: items,
+});
+
+export function getDashboardPermissions(id: number): ThunkResult<void> {
+  return async dispatch => {
+    const permissions = await getBackendSrv().get(`/api/dashboards/id/${id}/permissions`);
+    dispatch(loadDashboardPermissions(permissions));
+  };
+}
+
+function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO {
+  return {
+    userId: item.userId,
+    teamId: item.teamId,
+    role: item.role,
+    permission: item.permission,
+  };
+}
+
+export function updateDashboardPermission(
+  dashboardId: number,
+  itemToUpdate: DashboardAcl,
+  level: PermissionLevel
+): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const { dashboard } = getStore();
+    const itemsToUpdate = [];
+
+    for (const item of dashboard.permissions) {
+      if (item.inherited) {
+        continue;
+      }
+
+      const updated = toUpdateItem(itemToUpdate);
+
+      // if this is the item we want to update, update it's permisssion
+      if (itemToUpdate === item) {
+        updated.permission = level;
+      }
+
+      itemsToUpdate.push(updated);
+    }
+
+    await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate });
+    await dispatch(getDashboardPermissions(dashboardId));
+  };
+}
+
+export function removeDashboardPermission(dashboardId: number, itemToDelete: DashboardAcl): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const dashboard = getStore().dashboard;
+    const itemsToUpdate = [];
+
+    for (const item of dashboard.permissions) {
+      if (item.inherited || item === itemToDelete) {
+        continue;
+      }
+      itemsToUpdate.push(toUpdateItem(item));
+    }
+
+    await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate });
+    await dispatch(getDashboardPermissions(dashboardId));
+  };
+}
+
+export function addDashboardPermission(dashboardId: number, newItem: NewDashboardAclItem): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const { dashboard } = getStore();
+    const itemsToUpdate = [];
+
+    for (const item of dashboard.permissions) {
+      if (item.inherited) {
+        continue;
+      }
+      itemsToUpdate.push(toUpdateItem(item));
+    }
+
+    itemsToUpdate.push({
+      userId: newItem.userId,
+      teamId: newItem.teamId,
+      role: newItem.role,
+      permission: newItem.permission,
+    });
+
+    await getBackendSrv().post(`/api/dashboards/id/${dashboardId}/permissions`, { items: itemsToUpdate });
+    await dispatch(getDashboardPermissions(dashboardId));
+  };
+}

+ 24 - 0
public/app/features/dashboard/state/reducers.test.ts

@@ -0,0 +1,24 @@
+import { Action, ActionTypes } from './actions';
+import { OrgRole, PermissionLevel, DashboardState } from 'app/types';
+import { inititalState, dashboardReducer } from './reducers';
+
+describe('dashboard reducer', () => {
+  describe('loadDashboardPermissions', () => {
+    let state: DashboardState;
+
+    beforeEach(() => {
+      const action: Action = {
+        type: ActionTypes.LoadDashboardPermissions,
+        payload: [
+          { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
+          { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
+        ],
+      };
+      state = dashboardReducer(inititalState, action);
+    });
+
+    it('should add permissions to state', async () => {
+      expect(state.permissions.length).toBe(2);
+    });
+  });
+});

+ 22 - 0
public/app/features/dashboard/state/reducers.ts

@@ -0,0 +1,22 @@
+import { DashboardState } from 'app/types';
+import { Action, ActionTypes } from './actions';
+import { processAclItems } from 'app/core/utils/acl';
+
+export const inititalState: DashboardState = {
+  permissions: [],
+};
+
+export const dashboardReducer = (state = inititalState, action: Action): DashboardState => {
+  switch (action.type) {
+    case ActionTypes.LoadDashboardPermissions:
+      return {
+        ...state,
+        permissions: processAclItems(action.payload),
+      };
+  }
+  return state;
+};
+
+export default {
+  dashboard: dashboardReducer,
+};

+ 62 - 32
public/app/features/folders/FolderPermissions.tsx

@@ -1,58 +1,82 @@
-import React, { Component } from 'react';
+import React, { PureComponent } from 'react';
 import { hot } from 'react-hot-loader';
-import { inject, observer } from 'mobx-react';
 import { connect } from 'react-redux';
 import PageHeader from 'app/core/components/PageHeader/PageHeader';
-import Permissions from 'app/core/components/Permissions/Permissions';
 import Tooltip from 'app/core/components/Tooltip/Tooltip';
-import PermissionsInfo from 'app/core/components/Permissions/PermissionsInfo';
-import AddPermissions from 'app/core/components/Permissions/AddPermissions';
 import SlideDown from 'app/core/components/Animations/SlideDown';
 import { getNavModel } from 'app/core/selectors/navModel';
 import { NavModel, StoreState, FolderState } from 'app/types';
-import { getFolderByUid } from './state/actions';
-import { PermissionsStore } from 'app/stores/PermissionsStore/PermissionsStore';
+import { DashboardAcl, PermissionLevel, NewDashboardAclItem } from 'app/types/acl';
+import {
+  getFolderByUid,
+  getFolderPermissions,
+  updateFolderPermission,
+  removeFolderPermission,
+  addFolderPermission,
+} from './state/actions';
 import { getLoadingNav } from './state/navModel';
+import PermissionList from 'app/core/components/PermissionList/PermissionList';
+import AddPermission from 'app/core/components/PermissionList/AddPermission';
+import PermissionsInfo from 'app/core/components/PermissionList/PermissionsInfo';
 
 export interface Props {
   navModel: NavModel;
-  getFolderByUid: typeof getFolderByUid;
   folderUid: string;
   folder: FolderState;
-  permissions: typeof PermissionsStore.Type;
-  backendSrv: any;
+  getFolderByUid: typeof getFolderByUid;
+  getFolderPermissions: typeof getFolderPermissions;
+  updateFolderPermission: typeof updateFolderPermission;
+  removeFolderPermission: typeof removeFolderPermission;
+  addFolderPermission: typeof addFolderPermission;
+}
+
+export interface State {
+  isAdding: boolean;
 }
 
-@inject('permissions')
-@observer
-export class FolderPermissions extends Component<Props> {
+export class FolderPermissions extends PureComponent<Props, State> {
   constructor(props) {
     super(props);
-    this.handleAddPermission = this.handleAddPermission.bind(this);
+
+    this.state = {
+      isAdding: false,
+    };
   }
 
   componentDidMount() {
     this.props.getFolderByUid(this.props.folderUid);
+    this.props.getFolderPermissions(this.props.folderUid);
   }
 
-  componentWillUnmount() {
-    const { permissions } = this.props;
-    permissions.hideAddPermissions();
-  }
+  onOpenAddPermissions = () => {
+    this.setState({ isAdding: true });
+  };
 
-  handleAddPermission() {
-    const { permissions } = this.props;
-    permissions.toggleAddPermissions();
-  }
+  onRemoveItem = (item: DashboardAcl) => {
+    this.props.removeFolderPermission(item);
+  };
+
+  onPermissionChanged = (item: DashboardAcl, level: PermissionLevel) => {
+    this.props.updateFolderPermission(item, level);
+  };
+
+  onAddPermission = (newItem: NewDashboardAclItem) => {
+    return this.props.addFolderPermission(newItem);
+  };
+
+  onCancelAddPermission = () => {
+    this.setState({ isAdding: false });
+  };
 
   render() {
-    const { navModel, permissions, backendSrv, folder } = this.props;
+    const { navModel, folder } = this.props;
+    const { isAdding } = this.state;
 
     if (folder.id === 0) {
       return <PageHeader model={navModel} />;
     }
 
-    const dashboardId = folder.id;
+    const folderInfo = { title: folder.title, url: folder.url, id: folder.id };
 
     return (
       <div>
@@ -64,18 +88,20 @@ export class FolderPermissions extends Component<Props> {
               <i className="gicon gicon-question gicon--has-hover" />
             </Tooltip>
             <div className="page-action-bar__spacer" />
-            <button
-              className="btn btn-success pull-right"
-              onClick={this.handleAddPermission}
-              disabled={permissions.isAddPermissionsVisible}
-            >
+            <button className="btn btn-success pull-right" onClick={this.onOpenAddPermissions} disabled={isAdding}>
               <i className="fa fa-plus" /> Add Permission
             </button>
           </div>
-          <SlideDown in={permissions.isAddPermissionsVisible}>
-            <AddPermissions permissions={permissions} />
+          <SlideDown in={isAdding}>
+            <AddPermission onAddPermission={this.onAddPermission} onCancel={this.onCancelAddPermission} />
           </SlideDown>
-          <Permissions permissions={permissions} isFolder={true} dashboardId={dashboardId} backendSrv={backendSrv} />
+          <PermissionList
+            items={folder.permissions}
+            onRemoveItem={this.onRemoveItem}
+            onPermissionChanged={this.onPermissionChanged}
+            isFetching={false}
+            folderInfo={folderInfo}
+          />
         </div>
       </div>
     );
@@ -93,6 +119,10 @@ const mapStateToProps = (state: StoreState) => {
 
 const mapDispatchToProps = {
   getFolderByUid,
+  getFolderPermissions,
+  updateFolderPermission,
+  removeFolderPermission,
+  addFolderPermission,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions));

+ 1 - 0
public/app/features/folders/FolderSettingsPage.test.tsx

@@ -15,6 +15,7 @@ const setup = (propOverrides?: object) => {
       url: 'url',
       hasChanged: false,
       version: 1,
+      permissions: [],
     },
     getFolderByUid: jest.fn(),
     setFolderTitle: jest.fn(),

+ 104 - 4
public/app/features/folders/state/actions.ts

@@ -2,6 +2,14 @@ import { getBackendSrv } from 'app/core/services/backend_srv';
 import { StoreState } from 'app/types';
 import { ThunkAction } from 'redux-thunk';
 import { FolderDTO, FolderState } from 'app/types';
+import {
+  DashboardAcl,
+  DashboardAclDTO,
+  PermissionLevel,
+  DashboardAclUpdateDTO,
+  NewDashboardAclItem,
+} from 'app/types/acl';
+
 import { updateNavIndex, updateLocation } from 'app/core/actions';
 import { buildNavModel } from './navModel';
 import appEvents from 'app/core/app_events';
@@ -10,6 +18,7 @@ export enum ActionTypes {
   LoadFolder = 'LOAD_FOLDER',
   SetFolderTitle = 'SET_FOLDER_TITLE',
   SaveFolder = 'SAVE_FOLDER',
+  LoadFolderPermissions = 'LOAD_FOLDER_PERMISSONS',
 }
 
 export interface LoadFolderAction {
@@ -22,6 +31,15 @@ export interface SetFolderTitleAction {
   payload: string;
 }
 
+export interface LoadFolderPermissionsAction {
+  type: ActionTypes.LoadFolderPermissions;
+  payload: DashboardAcl[];
+}
+
+export type Action = LoadFolderAction | SetFolderTitleAction | LoadFolderPermissionsAction;
+
+type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
+
 export const loadFolder = (folder: FolderDTO): LoadFolderAction => ({
   type: ActionTypes.LoadFolder,
   payload: folder,
@@ -32,10 +50,10 @@ export const setFolderTitle = (newTitle: string): SetFolderTitleAction => ({
   payload: newTitle,
 });
 
-export type Action = LoadFolderAction | SetFolderTitleAction;
-
-type ThunkResult<R> = ThunkAction<R, StoreState, undefined, any>;
-
+export const loadFolderPermissions = (items: DashboardAclDTO[]): LoadFolderPermissionsAction => ({
+  type: ActionTypes.LoadFolderPermissions,
+  payload: items,
+});
 
 export function getFolderByUid(uid: string): ThunkResult<void> {
   return async dispatch => {
@@ -65,3 +83,85 @@ export function deleteFolder(uid: string): ThunkResult<void> {
     dispatch(updateLocation({ path: `dashboards` }));
   };
 }
+
+export function getFolderPermissions(uid: string): ThunkResult<void> {
+  return async dispatch => {
+    const permissions = await getBackendSrv().get(`/api/folders/${uid}/permissions`);
+    dispatch(loadFolderPermissions(permissions));
+  };
+}
+
+function toUpdateItem(item: DashboardAcl): DashboardAclUpdateDTO {
+  return {
+    userId: item.userId,
+    teamId: item.teamId,
+    role: item.role,
+    permission: item.permission,
+  };
+}
+
+export function updateFolderPermission(itemToUpdate: DashboardAcl, level: PermissionLevel): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const folder = getStore().folder;
+    const itemsToUpdate = [];
+
+    for (const item of folder.permissions) {
+      if (item.inherited) {
+        continue;
+      }
+
+      const updated = toUpdateItem(itemToUpdate);
+
+      // if this is the item we want to update, update it's permisssion
+      if (itemToUpdate === item) {
+        updated.permission = level;
+      }
+
+      itemsToUpdate.push(updated);
+    }
+
+    await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
+    await dispatch(getFolderPermissions(folder.uid));
+  };
+}
+
+export function removeFolderPermission(itemToDelete: DashboardAcl): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const folder = getStore().folder;
+    const itemsToUpdate = [];
+
+    for (const item of folder.permissions) {
+      if (item.inherited || item === itemToDelete) {
+        continue;
+      }
+      itemsToUpdate.push(toUpdateItem(item));
+    }
+
+    await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
+    await dispatch(getFolderPermissions(folder.uid));
+  };
+}
+
+export function addFolderPermission(newItem: NewDashboardAclItem): ThunkResult<void> {
+  return async (dispatch, getStore) => {
+    const folder = getStore().folder;
+    const itemsToUpdate = [];
+
+    for (const item of folder.permissions) {
+      if (item.inherited) {
+        continue;
+      }
+      itemsToUpdate.push(toUpdateItem(item));
+    }
+
+    itemsToUpdate.push({
+      userId: newItem.userId,
+      teamId: newItem.teamId,
+      role: newItem.role,
+      permission: newItem.permission,
+    });
+
+    await getBackendSrv().post(`/api/folders/${folder.uid}/permissions`, { items: itemsToUpdate });
+    await dispatch(getFolderPermissions(folder.uid));
+  };
+}

+ 73 - 18
public/app/features/folders/state/reducers.test.ts

@@ -1,5 +1,5 @@
 import { Action, ActionTypes } from './actions';
-import { FolderDTO } from 'app/types';
+import { FolderDTO, OrgRole, PermissionLevel, FolderState } from 'app/types';
 import { inititalState, folderReducer } from './reducers';
 
 function getTestFolder(): FolderDTO {
@@ -14,29 +14,84 @@ function getTestFolder(): FolderDTO {
 }
 
 describe('folder reducer', () => {
-  it('should load folder and set hasChanged to false', () => {
-    const folder = getTestFolder();
+  describe('loadFolder', () => {
+    it('should load folder and set hasChanged to false', () => {
+      const folder = getTestFolder();
 
-    const action: Action = {
-      type: ActionTypes.LoadFolder,
-      payload: folder,
-    };
+      const action: Action = {
+        type: ActionTypes.LoadFolder,
+        payload: folder,
+      };
 
-    const state = folderReducer(inititalState, action);
+      const state = folderReducer(inititalState, action);
 
-    expect(state.hasChanged).toEqual(false);
-    expect(state.title).toEqual('test folder');
+      expect(state.hasChanged).toEqual(false);
+      expect(state.title).toEqual('test folder');
+    });
   });
 
-  it('should set title', () => {
-    const action: Action = {
-      type: ActionTypes.SetFolderTitle,
-      payload: 'new title',
-    };
+  describe('detFolderTitle', () => {
+    it('should set title', () => {
+      const action: Action = {
+        type: ActionTypes.SetFolderTitle,
+        payload: 'new title',
+      };
 
-    const state = folderReducer(inititalState, action);
+      const state = folderReducer(inititalState, action);
 
-    expect(state.hasChanged).toEqual(true);
-    expect(state.title).toEqual('new title');
+      expect(state.hasChanged).toEqual(true);
+      expect(state.title).toEqual('new title');
+    });
+  });
+
+  describe('loadFolderPermissions', () => {
+    let state: FolderState;
+
+    beforeEach(() => {
+      const action: Action = {
+        type: ActionTypes.LoadFolderPermissions,
+        payload: [
+          { id: 2, dashboardId: 1, role: OrgRole.Viewer, permission: PermissionLevel.View },
+          { id: 3, dashboardId: 1, role: OrgRole.Editor, permission: PermissionLevel.Edit },
+          {
+            id: 4,
+            dashboardId: 10,
+            permission: PermissionLevel.View,
+            teamId: 1,
+            team: 'MyTestTeam',
+            inherited: true,
+          },
+          {
+            id: 5,
+            dashboardId: 1,
+            permission: PermissionLevel.View,
+            userId: 1,
+            userLogin: 'MyTestUser',
+          },
+          {
+            id: 6,
+            dashboardId: 1,
+            permission: PermissionLevel.Edit,
+            teamId: 2,
+            team: 'MyTestTeam2',
+          },
+        ],
+      };
+
+      state = folderReducer(inititalState, action);
+    });
+
+    it('should add permissions to state', async () => {
+      expect(state.permissions.length).toBe(5);
+    });
+
+    it('should be sorted by sort rank and alphabetically', async () => {
+      expect(state.permissions[0].name).toBe('MyTestTeam');
+      expect(state.permissions[0].dashboardId).toBe(10);
+      expect(state.permissions[1].name).toBe('Editor');
+      expect(state.permissions[2].name).toBe('Viewer');
+      expect(state.permissions[3].name).toBe('MyTestTeam2');
+      expect(state.permissions[4].name).toBe('MyTestUser');
+    });
   });
 });

+ 9 - 1
public/app/features/folders/state/reducers.ts

@@ -1,5 +1,6 @@
 import { FolderState } from 'app/types';
 import { Action, ActionTypes } from './actions';
+import { processAclItems } from 'app/core/utils/acl';
 
 export const inititalState: FolderState = {
   id: 0,
@@ -8,13 +9,15 @@ export const inititalState: FolderState = {
   url: '',
   canSave: false,
   hasChanged: false,
-  version: 0,
+  version: 1,
+  permissions: [],
 };
 
 export const folderReducer = (state = inititalState, action: Action): FolderState => {
   switch (action.type) {
     case ActionTypes.LoadFolder:
       return {
+        ...state,
         ...action.payload,
         hasChanged: false,
       };
@@ -24,6 +27,11 @@ export const folderReducer = (state = inititalState, action: Action): FolderStat
         title: action.payload,
         hasChanged: action.payload.trim().length > 0,
       };
+    case ActionTypes.LoadFolderPermissions:
+      return {
+        ...state,
+        permissions: processAclItems(action.payload),
+      };
   }
   return state;
 };

+ 2 - 0
public/app/stores/configureStore.ts

@@ -5,12 +5,14 @@ import sharedReducers from 'app/core/reducers';
 import alertingReducers from 'app/features/alerting/state/reducers';
 import teamsReducers from 'app/features/teams/state/reducers';
 import foldersReducers from 'app/features/folders/state/reducers';
+import dashboardReducers from 'app/features/dashboard/state/reducers';
 
 const rootReducer = combineReducers({
   ...sharedReducers,
   ...alertingReducers,
   ...teamsReducers,
   ...foldersReducers,
+  ...dashboardReducers,
 });
 
 export let store;

+ 91 - 0
public/app/types/acl.ts

@@ -0,0 +1,91 @@
+export enum OrgRole {
+  Viewer = 'Viewer',
+  Editor = 'Editor',
+  Admin = 'Admin',
+}
+
+export interface DashboardAclDTO {
+  id?: number;
+  dashboardId?: number;
+  userId?: number;
+  userLogin?: string;
+  userEmail?: string;
+  teamId?: number;
+  team?: string;
+  permission?: PermissionLevel;
+  role?: OrgRole;
+  icon?: string;
+  inherited?: boolean;
+}
+
+export interface DashboardAclUpdateDTO {
+  userId: number;
+  teamId: number;
+  role: OrgRole;
+  permission: PermissionLevel;
+}
+
+export interface DashboardAcl {
+  id?: number;
+  dashboardId?: number;
+  userId?: number;
+  userLogin?: string;
+  userEmail?: string;
+  teamId?: number;
+  team?: string;
+  permission?: PermissionLevel;
+  role?: OrgRole;
+  icon?: string;
+  name?: string;
+  inherited?: boolean;
+  sortRank?: number;
+}
+
+export interface DashboardPermissionInfo {
+  value: PermissionLevel;
+  label: string;
+  description: string;
+}
+
+export interface NewDashboardAclItem {
+  teamId: number;
+  userId: number;
+  role?: OrgRole;
+  permission: PermissionLevel;
+  type: AclTarget;
+}
+
+export enum PermissionLevel {
+  View = 1,
+  Edit = 2,
+  Admin = 4,
+}
+
+export enum AclTarget {
+  Team = 'Team',
+  User = 'User',
+  Viewer = 'Viewer',
+  Editor = 'Editor',
+}
+
+export interface AclTargetInfo {
+  value: AclTarget;
+  text: string;
+}
+
+export const dashboardAclTargets: AclTargetInfo[] = [
+  { value: AclTarget.Team, text: 'Team' },
+  { value: AclTarget.User, text: 'User' },
+  { value: AclTarget.Viewer, text: 'Everyone With Viewer Role' },
+  { value: AclTarget.Editor, text: 'Everyone With Editor Role' },
+];
+
+export const dashboardPermissionLevels: DashboardPermissionInfo[] = [
+  { value: PermissionLevel.View, label: 'View', description: 'Can view dashboards.' },
+  { value: PermissionLevel.Edit, label: 'Edit', description: 'Can add, edit and delete dashboards.' },
+  {
+    value: PermissionLevel.Admin,
+    label: 'Admin',
+    description: 'Can add/remove permissions and can add, edit and delete dashboards.',
+  },
+];

+ 5 - 0
public/app/types/dashboard.ts

@@ -0,0 +1,5 @@
+import { DashboardAcl } from './acl';
+
+export interface DashboardState {
+  permissions: DashboardAcl[];
+}

+ 10 - 1
public/app/types/folder.ts

@@ -1,3 +1,5 @@
+import { DashboardAcl } from './acl';
+
 export interface FolderDTO {
   id: number;
   uid: string;
@@ -12,7 +14,14 @@ export interface FolderState {
   uid: string;
   title: string;
   url: string;
-  version: number;
   canSave: boolean;
   hasChanged: boolean;
+  version: number;
+  permissions: DashboardAcl[];
+}
+
+export interface FolderInfo {
+  id: number;
+  title: string;
+  url: string;
 }

+ 9 - 1
public/app/types/index.ts

@@ -2,7 +2,9 @@ import { Team, TeamsState, TeamState, TeamGroup, TeamMember } from './teams';
 import { AlertRuleDTO, AlertRule, AlertRulesState } from './alerting';
 import { LocationState, LocationUpdate, UrlQueryMap, UrlQueryValue } from './location';
 import { NavModel, NavModelItem, NavIndex } from './navModel';
-import { FolderDTO, FolderState } from './folder';
+import { FolderDTO, FolderState, FolderInfo } from './folder';
+import { DashboardState } from './dashboard';
+import { DashboardAcl, OrgRole, PermissionLevel } from './acl';
 
 export {
   Team,
@@ -22,6 +24,11 @@ export {
   UrlQueryValue,
   FolderDTO,
   FolderState,
+  FolderInfo,
+  DashboardState,
+  DashboardAcl,
+  OrgRole,
+  PermissionLevel,
 };
 
 export interface StoreState {
@@ -31,4 +38,5 @@ export interface StoreState {
   teams: TeamsState;
   team: TeamState;
   folder: FolderState;
+  dashboard: DashboardState;
 }

+ 3 - 0
scripts/webpack/webpack.common.js

@@ -24,6 +24,9 @@ module.exports = {
       path.resolve('node_modules')
     ],
   },
+  stats: {
+    warningsFilter: /export .* was not found in/
+  },
   node: {
     fs: 'empty',
   },