From f360b6186b0d0726762382caec2a787c493cb386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Torkel=20=C3=96degaard?= Date: Thu, 13 Sep 2018 10:52:29 +0200 Subject: [PATCH] wip: first couple of things starting to work --- public/app/core/actions/permissions.ts | 24 +++++ .../DisabledPermissionListItem.tsx | 43 ++++++++ .../PermissionList/PermissionList.tsx | 61 +++++++++++ .../PermissionList/PermissionListItem.tsx | 100 ++++++++++++++++++ .../features/folders/FolderPermissions.tsx | 70 +++++++----- public/app/features/folders/state/actions.ts | 85 ++++++++++++++- public/app/features/folders/state/reducers.ts | 41 ++++++- public/app/types/acl.ts | 60 +++++++++++ public/app/types/folder.ts | 11 +- public/app/types/index.ts | 8 +- 10 files changed, 465 insertions(+), 38 deletions(-) create mode 100644 public/app/core/actions/permissions.ts create mode 100644 public/app/core/components/PermissionList/DisabledPermissionListItem.tsx create mode 100644 public/app/core/components/PermissionList/PermissionList.tsx create mode 100644 public/app/core/components/PermissionList/PermissionListItem.tsx create mode 100644 public/app/types/acl.ts diff --git a/public/app/core/actions/permissions.ts b/public/app/core/actions/permissions.ts new file mode 100644 index 00000000000..2b07b7145dd --- /dev/null +++ b/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 { + return async dispatch => { + const permissions = await backendSrv.get(`/api/folders/${uid}/permissions`); + dispatch(loadFolderPermissions(permissions)); + }; +} diff --git a/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx b/public/app/core/components/PermissionList/DisabledPermissionListItem.tsx new file mode 100644 index 00000000000..d65595dae66 --- /dev/null +++ b/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 { + render() { + const { item } = this.props; + + return ( + + + + + + {item.name} + (Role) + + + Can + +
+ {}} + value={item.permission} + disabled={true} + className={'gf-form-input--form-dropdown-right'} + /> +
+ + + + + + ); + } +} diff --git a/public/app/core/components/PermissionList/PermissionList.tsx b/public/app/core/components/PermissionList/PermissionList.tsx new file mode 100644 index 00000000000..29f810a4358 --- /dev/null +++ b/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 { + render() { + const { items, onRemoveItem, onPermissionChanged, isFetching, folderInfo } = this.props; + + return ( + + + + {items.map((item, idx) => { + return ( + + ); + })} + {isFetching === true && items.length < 1 ? ( + + + + ) : null} + + {isFetching === false && items.length < 1 ? ( + + + + ) : null} + +
+ Loading permissions... +
+ No permissions are set. Will only be accessible by admins. +
+ ); + } +} + +export default PermissionList; diff --git a/public/app/core/components/PermissionList/PermissionListItem.tsx b/public/app/core/components/PermissionList/PermissionListItem.tsx new file mode 100644 index 00000000000..3e5aaf3ab2f --- /dev/null +++ b/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 ; + } + if (item.teamAvatarUrl) { + return ; + } + if (item.role === 'Editor') { + return ; + } + + return ; +} + +function ItemDescription({ item }) { + if (item.userId) { + return (User); + } + if (item.teamId) { + return (Team); + } + return (Role); +} + +interface Props { + item: DashboardAcl; + onRemoveItem: (item: DashboardAcl) => void; + onPermissionChanged: (item: DashboardAcl, level: PermissionLevel) => void; + folderInfo?: FolderInfo; +} + +export default class PermissionsListItem extends PureComponent { + 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 ( + + + + + + {item.name} + + + {item.inherited && + folderInfo && ( + + Inherited from folder{' '} + + {folderInfo.title} + {' '} + + )} + {inheritedFromRoot && Default Permission} + + Can + +
+ +
+ + + {!item.inherited ? ( + + + + ) : ( + + )} + + + ); + } +} diff --git a/public/app/features/folders/FolderPermissions.tsx b/public/app/features/folders/FolderPermissions.tsx index 512927c24e6..25de5f8be16 100644 --- a/public/app/features/folders/FolderPermissions.tsx +++ b/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; } -@inject('permissions') -@observer -export class FolderPermissions extends Component { +export interface State { + isAdding: boolean; +} + +export class FolderPermissions extends Component { 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 ; } const dashboardId = folder.id; + const folderInfo = { title: folder.tile, url: folder.url, id: folder.id }; return (
@@ -64,18 +74,17 @@ export class FolderPermissions extends Component {
-
- - - - +
); @@ -93,6 +102,9 @@ const mapStateToProps = (state: StoreState) => { const mapDispatchToProps = { getFolderByUid, + getFolderPermissions, + updateFolderPermission, + removeFolderPermission, }; export default hot(module)(connect(mapStateToProps, mapDispatchToProps)(FolderPermissions)); diff --git a/public/app/features/folders/state/actions.ts b/public/app/features/folders/state/actions.ts index 5d153b2fb8a..29940cc7a31 100644 --- a/public/app/features/folders/state/actions.ts +++ b/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 = ThunkAction; + 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 = ThunkAction; - +export const loadFolderPermissions = (items: DashboardAclDTO[]): LoadFolderPermissionsAction => ({ + type: ActionTypes.LoadFolderPermissions, + payload: items, +}); export function getFolderByUid(uid: string): ThunkResult { return async dispatch => { @@ -65,3 +82,61 @@ export function deleteFolder(uid: string): ThunkResult { dispatch(updateLocation({ path: `dashboards` })); }; } + +export function getFolderPermissions(uid: string): ThunkResult { + 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 { + 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 { + 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)); + }; +} diff --git a/public/app/features/folders/state/reducers.ts b/public/app/features/folders/state/reducers.ts index 41ae10d19e5..6e6a671685a 100644 --- a/public/app/features/folders/state/reducers.ts +++ b/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, }; diff --git a/public/app/types/acl.ts b/public/app/types/acl.ts new file mode 100644 index 00000000000..d77fc4793fc --- /dev/null +++ b/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.', + }, +]; diff --git a/public/app/types/folder.ts b/public/app/types/folder.ts index 6fbe79cce8c..bbcae01fe59 100644 --- a/public/app/types/folder.ts +++ b/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; } diff --git a/public/app/types/index.ts b/public/app/types/index.ts index 52d1ba592c5..49f7fdb0f28 100644 --- a/public/app/types/index.ts +++ b/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 {