Просмотр исходного кода

wip: first couple of things starting to work

Torkel Ödegaard 7 лет назад
Родитель
Сommit
f360b6186b

+ 24 - 0
public/app/core/actions/permissions.ts

@@ -0,0 +1,24 @@
+import { DashboardAcl } from '../../types';
+
+export enum ActionTypes {
+  LoadFolderPermissions = 'LoadFolderPermissions',
+}
+
+export interface LoadFolderPermissionsAction {
+  type: ActionTypes.LoadFolderPermissions;
+  payload: DashboardAcl[];
+}
+
+export type Action = LoadFolderPermissions;
+
+export const loadFolderPermissions = (items: DashboardAcl[]): LoadFolderPermissionsAction => ({
+  type: ActionTypes.LoadFolderPermissions,
+  payload: items,
+});
+
+export function getFolderPermissions(uid: string): ThunkResult<void> {
+  return async dispatch => {
+    const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`);
+    dispatch(loadFolderPermissions(permissions));
+  };
+}

+ 43 - 0
public/app/core/components/PermissionList/DisabledPermissionListItem.tsx

@@ -0,0 +1,43 @@
+import React, { Component } from 'react';
+import DescriptionPicker from 'app/core/components/Picker/DescriptionPicker';
+import { permissionOptions } from 'app/stores/PermissionsStore/PermissionsStore';
+
+export interface Props {
+  item: any;
+}
+
+export default class DisabledPermissionListItem extends Component<Props, any> {
+  render() {
+    const { item } = this.props;
+
+    return (
+      <tr className="gf-form-disabled">
+        <td style={{ width: '1%' }}>
+          <i style={{ width: '25px', height: '25px' }} className="gicon gicon-shield" />
+        </td>
+        <td style={{ width: '90%' }}>
+          {item.name}
+          <span className="filter-table__weak-italic"> (Role)</span>
+        </td>
+        <td />
+        <td className="query-keyword">Can</td>
+        <td>
+          <div className="gf-form">
+            <DescriptionPicker
+              optionsWithDesc={permissionOptions}
+              onSelected={() => {}}
+              value={item.permission}
+              disabled={true}
+              className={'gf-form-input--form-dropdown-right'}
+            />
+          </div>
+        </td>
+        <td>
+          <button className="btn btn-inverse btn-small">
+            <i className="fa fa-lock" />
+          </button>
+        </td>
+      </tr>
+    );
+  }
+}

+ 61 - 0
public/app/core/components/PermissionList/PermissionList.tsx

@@ -0,0 +1,61 @@
+import React, { PureComponent } from 'react';
+import PermissionsListItem from './PermissionListItem';
+import DisabledPermissionsListItem from './DisabledPermissionListItem';
+import { DashboardAcl, FolderInfo } from 'app/types';
+
+export interface Props {
+  items: DashboardAcl[];
+  onRemoveItem: (item: DashboardAcl) => void;
+  onPermissionChanged: any;
+  isFetching: boolean;
+  folderInfo?: FolderInfo;
+}
+
+class PermissionList extends PureComponent<Props> {
+  render() {
+    const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props;
+
+    return (
+      <table className="filter-table gf-form-group">
+        <tbody>
+          <DisabledPermissionsListItem
+            key={0}
+            item={{
+              name: 'Admin',
+              permission: 4,
+              icon: 'fa fa-fw fa-street-view',
+            }}
+          />
+          {items.map((item, idx) => {
+            return (
+              <PermissionsListItem
+                key={idx + 1}
+                item={item}
+                onRemoveItem={onRemoveItem}
+                onPermissionChanged={onPermissionChanged}
+                folderInfo={folderInfo}
+              />
+            );
+          })}
+          {isFetching === true && items.length < 1 ? (
+            <tr>
+              <td colSpan={4}>
+                <em>Loading permissions...</em>
+              </td>
+            </tr>
+          ) : null}
+
+          {isFetching === false && items.length < 1 ? (
+            <tr>
+              <td colSpan={4}>
+                <em>No permissions are set. Will only be accessible by admins.</em>
+              </td>
+            </tr>
+          ) : null}
+        </tbody>
+      </table>
+    );
+  }
+}
+
+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 } from 'app/types/acl';
+import { DashboardAcl, FolderInfo, PermissionLevel } 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>
+    );
+  }
+}

+ 41 - 29
public/app/features/folders/FolderPermissions.tsx

@@ -1,6 +1,5 @@
 import React, { Component } 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';
@@ -9,50 +8,61 @@ 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 { NavModel, StoreState, FolderState, DashboardAcl, PermissionLevel } from 'app/types';
+import { getFolderByUid, getFolderPermissions, updateFolderPermission, removeFolderPermission } from './state/actions';
 import { getLoadingNav } from './state/navModel';
+import PermissionList from 'app/core/components/PermissionList/PermissionList';
 
 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;
+}
+
+export interface State {
+  isAdding: boolean;
 }
 
-@inject('permissions')
-@observer
-export class FolderPermissions extends Component<Props> {
+export class FolderPermissions extends Component<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);
+  };
 
   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.tile, url: folder.url, id: folder.id };
 
     return (
       <div>
@@ -64,18 +74,17 @@ 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>
-          <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 +102,9 @@ const mapStateToProps = (state: StoreState) => {
 
 const mapDispatchToProps = {
   getFolderByUid,
+  getFolderPermissions,
+  updateFolderPermission,
+  removeFolderPermission,
 };
 
 export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions));

+ 80 - 5
public/app/features/folders/state/actions.ts

@@ -1,7 +1,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 {
+  FolderDTO,
+  FolderState,
+  DashboardAcl,
+  DashboardAclDTO,
+  PermissionLevel,
+  DashboardAclUpdateDTO,
+} from 'app/types';
 import { updateNavIndex, updateLocation } from 'app/core/actions';
 import { buildNavModel } from './navModel';
 import appEvents from 'app/core/app_events';
@@ -10,6 +17,7 @@ export enum ActionTypes {
   LoadFolder = 'LOAD_FOLDER',
   SetFolderTitle = 'SET_FOLDER_TITLE',
   SaveFolder = 'SAVE_FOLDER',
+  LoadFolderPermissions = 'LOAD_FOLDER_PERMISSONS',
 }
 
 export interface LoadFolderAction {
@@ -22,6 +30,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 +49,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 +82,61 @@ 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));
+  };
+}

+ 39 - 2
public/app/features/folders/state/reducers.ts

@@ -1,4 +1,4 @@
-import { FolderState } from 'app/types';
+import { FolderState, DashboardAcl, DashboardAclDTO } from 'app/types';
 import { Action, ActionTypes } from './actions';
 
 export const inititalState: FolderState = {
@@ -8,13 +8,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,10 +26,45 @@ 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;
 };
 
+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;
+}
+
 export default {
   folder: folderReducer,
 };

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

@@ -0,0 +1,60 @@
+export interface DashboardAclDTO {
+  id?: number;
+  dashboardId?: number;
+  userId?: number;
+  userLogin?: string;
+  userEmail?: string;
+  teamId?: number;
+  team?: string;
+  permission?: PermissionLevel;
+  permissionName?: string;
+  role?: string;
+  icon?: string;
+  inherited?: boolean;
+}
+
+export interface DashboardAclUpdateDTO {
+  userId: number;
+  teamId: number;
+  role: string;
+  permission: PermissionLevel;
+}
+
+export interface DashboardAcl {
+  id?: number;
+  dashboardId?: number;
+  userId?: number;
+  userLogin?: string;
+  userEmail?: string;
+  teamId?: number;
+  team?: string;
+  permission?: PermissionLevel;
+  permissionName?: string;
+  role?: string;
+  icon?: string;
+  name?: string;
+  inherited?: boolean;
+  sortRank?: number;
+}
+
+export interface DashboardPermissionInfo {
+  value: PermissionLevel;
+  label: string;
+  description: string;
+}
+
+export enum PermissionLevel {
+  View = 1,
+  Edit = 2,
+  Admin = 4,
+}
+
+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.',
+  },
+];

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

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

@@ -2,7 +2,8 @@ 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 { DashboardAcl, DashboardAclDTO, PermissionLevel, DashboardAclUpdateDTO } from './acl';
 
 export {
   Team,
@@ -22,6 +23,11 @@ export {
   UrlQueryValue,
   FolderDTO,
   FolderState,
+  FolderInfo,
+  DashboardAcl,
+  DashboardAclDTO,
+  DashboardAclUpdateDTO,
+  PermissionLevel,
 };
 
 export interface StoreState {