20829: Migrate to canWrite and canManage for permission checks
authorPeter Amstutz <peter.amstutz@curii.com>
Tue, 22 Aug 2023 18:16:29 +0000 (14:16 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Tue, 22 Aug 2023 18:16:29 +0000 (14:16 -0400)
Replace use of writableBy with canWrite and canManage, and remove
writableBy from workbench 2 entirely.

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

17 files changed:
src/models/group.ts
src/models/test-utils.ts
src/models/user.ts
src/services/auth-service/auth-service.ts
src/services/user-service/user-service.ts
src/store/advanced-tab/advanced-tab.tsx
src/store/context-menu/context-menu-actions.test.ts
src/store/resources/resources.test.ts
src/store/resources/resources.ts
src/store/run-process-panel/run-process-panel-actions.ts
src/store/tree-picker/tree-picker-actions.ts
src/store/workflow-panel/workflow-panel-actions.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/side-panel-button/side-panel-button.tsx
src/views/collection-panel/collection-panel.tsx
src/views/group-details-panel/group-details-panel.tsx
src/views/run-process-panel/inputs/project-input.tsx

index df4ea6aafa3067e47342b101d70ea9e961c98d09..0932b3c95e63665e3cbe68e1c416a12e8803a470 100644 (file)
@@ -15,7 +15,6 @@ export interface GroupResource extends TrashableResource, ResourceWithProperties
     name: string;
     groupClass: GroupClass | null;
     description: string;
-    writableBy: string[];
     ensure_unique_name: boolean;
     canWrite: boolean;
     canManage: boolean;
@@ -49,7 +48,6 @@ export const selectedFieldsOfGroup = [
     "group_class",
     "description",
     "properties",
-    "writable_by",
     "can_write",
     "can_manage",
     "trash_at",
index a2a980bf2885ece4706889b277bd10818b9cab05..74667a915e9d15a3d7b1c08bda9cc1233fe8f2e5 100644 (file)
@@ -23,7 +23,6 @@ export const mockGroupResource = (data: Partial<GroupResource> = {}): GroupResou
     properties: "",
     trashAt: "",
     uuid: "",
-    writableBy: [],
     ensure_unique_name: true,
     canWrite: false,
     canManage: false,
index 87a2e8c13d472200bfdd97ebf333ed6defe8f57f..0df6eac24158809ce426560359bab96b0985fe1c 100644 (file)
@@ -24,6 +24,8 @@ export interface User {
     prefs: UserPrefs;
     isAdmin: boolean;
     isActive: boolean;
+    canWrite: boolean;
+    canManage: boolean;
 }
 
 export const getUserFullname = (user: User) => {
@@ -62,5 +64,4 @@ export const getUserClusterID = (user: User): string | undefined => {
 export interface UserResource extends Resource, User {
     kind: ResourceKind.USER;
     defaultOwnerUuid: string;
-    writableBy: string[];
 }
index b530e4cd3e8d4933cf3e445fe6f306f8994b6d4c..79a6b7e1960be50cf06a078fb7bcd15f85fb19fc 100644 (file)
@@ -35,6 +35,8 @@ export interface UserDetailsResponse {
     is_active: boolean;
     username: string;
     prefs: UserPrefs;
+    can_write: boolean;
+    can_manage: boolean;
 }
 
 export class AuthService {
@@ -146,6 +148,8 @@ export class AuthService {
                     isAdmin: resp.data.is_admin,
                     isActive: resp.data.is_active,
                     username: resp.data.username,
+                    canWrite: resp.data.can_write,
+                    canManage: resp.data.can_manage,
                     prefs
                 };
             })
index 8581b26766715fe57ce1d50657c8e9131f90daa4..75131f922d1f54a65ca4c47ca99b1f13d4b132aa 100644 (file)
@@ -12,8 +12,7 @@ export class UserService extends CommonResourceService<UserResource> {
     constructor(serverApi: AxiosInstance, actions: ApiActions, readOnlyFields: string[] = []) {
         super(serverApi, "users", actions, readOnlyFields.concat([
             'fullName',
-            'isInvited',
-            'writableBy',
+            'isInvited'
         ]));
     }
 
index 6333711783ccd00b6f83560a8c575a827b2a1bbb..fedd551864e2c5c80ecb9af1e1a804bc912c29e8 100644 (file)
@@ -468,7 +468,7 @@ const collectionApiResponse = (apiResponse: CollectionResource): JSX.Element =>
 
 const groupRequestApiResponse = (apiResponse: ProjectResource): JSX.Element => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name,
-        description, groupClass, trashAt, isTrashed, deleteAt, properties, writableBy,
+        description, groupClass, trashAt, isTrashed, deleteAt, properties,
         canWrite, canManage } = apiResponse;
     const response = `
 "uuid": "${uuid}",
@@ -485,8 +485,7 @@ const groupRequestApiResponse = (apiResponse: ProjectResource): JSX.Element => {
 "delete_at": ${stringify(deleteAt)},
 "properties": ${stringifyObject(properties)},
 "can_write": ${stringify(canWrite)},
-"can_manage": ${stringify(canManage)},
-"writable_by": ${stringifyObject(writableBy)}`;
+"can_manage": ${stringify(canManage)}`;
 
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
index d9e87b1a763760102d0d32c96cbe64c92cbd7f42..623c45088cc83922f137896effccf4969e19dc0b 100644 (file)
@@ -107,13 +107,13 @@ describe('context-menu-actions', () => {
                         [projectUuid]: {
                             uuid: projectUuid,
                             ownerUuid: isEditable ? userUuid : otherUserUuid,
-                            writableBy: isEditable ? [userUuid] : [otherUserUuid],
+                            canWrite: isEditable,
                             groupClass: GroupClass.PROJECT,
                         },
                         [filterGroupUuid]: {
                             uuid: filterGroupUuid,
                             ownerUuid: isEditable ? userUuid : otherUserUuid,
-                            writableBy: isEditable ? [userUuid] : [otherUserUuid],
+                            canWrite: isEditable,
                             groupClass: GroupClass.FILTER,
                         },
                         [linkUuid]: {
index 300e21d1068e6640b4acac29378e5bb28b0c8659..503e19a2e88b996fab068e580f5997b37edca4d9 100644 (file)
@@ -27,7 +27,7 @@ describe('resources', () => {
                 modifiedAt: 'string',
                 href: 'string',
                 kind: ResourceKind.PROJECT,
-                writableBy: [groupFixtures.user_uuid],
+                canWrite: true,
                 etag: 'string',
             },
             [groupFixtures.editable_collection_resource_uuid]: {
@@ -50,7 +50,7 @@ describe('resources', () => {
                 modifiedAt: 'string',
                 href: 'string',
                 kind: ResourceKind.PROJECT,
-                writableBy: [groupFixtures.unknown_user_resource_uuid],
+                canWrite: false,
                 etag: 'string',
             },
             [groupFixtures.not_editable_collection_resource_uuid]: {
@@ -137,4 +137,4 @@ describe('resources', () => {
             expect(result!.isEditable).toBeFalsy();
         });
     });
-});
\ No newline at end of file
+});
index 6f7acadae0f64cfac8ac2957e0dafb14eb1f3b76..a063db9709723bfebd5b7ad94e4c2777d0fda34f 100644 (file)
@@ -9,26 +9,6 @@ import { GroupResource } from "models/group";
 
 export type ResourcesState = { [key: string]: Resource };
 
-const getResourceWritableBy = (state: ResourcesState, id: string, userUuid: string): string[] => {
-    if (!id) {
-        return [];
-    }
-
-    if (id === userUuid) {
-        return [userUuid];
-    }
-
-    const resource = (state[id] as ProjectResource);
-
-    if (!resource) {
-        return [];
-    }
-
-    const { writableBy } = resource;
-
-    return writableBy || getResourceWritableBy(state, resource.ownerUuid, userUuid);
-};
-
 export const getResourceWithEditableStatus = <T extends EditableResource & GroupResource>(id: string, userUuid?: string) =>
     (state: ResourcesState): T | undefined => {
         if (state[id] === undefined) { return; }
@@ -36,7 +16,7 @@ export const getResourceWithEditableStatus = <T extends EditableResource & Group
         const resource = JSON.parse(JSON.stringify(state[id] as T));
 
         if (resource) {
-            resource.isEditable = userUuid ? getResourceWritableBy(state, id, userUuid).indexOf(userUuid) > -1 : false;
+            resource.isEditable = resource.canWrite;
         }
 
         return resource;
index 58796295f824f55d61ee0d82f8013d1ca0c1b0b2..67a89e4c7710237b25c1534cb2e8e00ab50e6b30 100644 (file)
@@ -104,7 +104,7 @@ export const setWorkflow = (workflow: WorkflowResource, isWorkflowChanged = true
 
         let owner = getResource<ProjectResource | UserResource>(getState().runProcessPanel.processOwnerUuid)(getState().resources);
         const userUuid = getUserUuid(getState());
-        if (!owner || !userUuid || owner.writableBy.indexOf(userUuid) === -1) {
+        if (!owner || !owner.canWrite) {
             owner = undefined;
         }
 
index ad657f14e6281da0ccc185be29f1734195d748f4..72d1cb65d969de803ffc93d162b69dfc250fb56b 100644 (file)
@@ -390,7 +390,7 @@ export const loadFavoritesProject = (params: LoadFavoritesProjectParams,
                 id: 'Favorites',
                 pickerId,
                 data: items.filter((item) => {
-                    if (options.showOnlyWritable && (item as GroupResource).writableBy && (item as GroupResource).writableBy.indexOf(uuid) === -1) {
+                    if (options.showOnlyWritable && !(item as GroupResource).canWrite) {
                         return false;
                     }
 
index eab16882e0a9af61c5a96d2cb3cb9b6853b2148a..2c44fae4e2495fbc9d1abe2e6c26745f4bcbe60c 100644 (file)
@@ -65,7 +65,7 @@ export const openRunProcess = (workflowUuid: string, ownerUuid?: string, name?:
                 // Must be writable.
                 const userUuid = getUserUuid(getState());
                 owner = getResource<ProjectResource | UserResource>(ownerUuid)(getState().resources);
-                if (!owner || !userUuid || owner.writableBy.indexOf(userUuid) === -1) {
+                if (!owner || !owner.canWrite) {
                     owner = undefined;
                 }
             }
index d274157c48e2b1cd22804179fa33954c4b8fb361..251304075b37f1c0132002d9356b94f797c00580 100644 (file)
@@ -91,7 +91,7 @@ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 };
 
 
-const FrozenProject = (props: {item: ProjectResource}) => {
+const FrozenProject = (props: { item: ProjectResource }) => {
     const [fullUsername, setFullusername] = React.useState<any>(null);
     const getFullName = React.useCallback(() => {
         if (props.item.frozenByUuid) {
@@ -102,7 +102,7 @@ const FrozenProject = (props: {item: ProjectResource}) => {
     if (props.item.frozenByUuid) {
 
         return <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
-            <FreezeIcon style={{ fontSize: "inherit" }}/>
+            <FreezeIcon style={{ fontSize: "inherit" }} />
         </Tooltip>;
     } else {
         return null;
@@ -115,7 +115,7 @@ export const ResourceName = connect(
         return resource;
     })((resource: GroupContentsResource & DispatchProp<any>) => renderName(resource.dispatch, resource));
 
-    
+
 const renderIcon = (item: GroupContentsResource) => {
     switch (item.kind) {
         case ResourceKind.PROJECT:
@@ -230,12 +230,12 @@ export const UserResourceFullName = connect(
 const renderUuid = (item: { uuid: string }) =>
     <Typography data-cy="uuid" noWrap>
         {item.uuid}
-        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-' }
+        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
     </Typography>;
 
 const renderUuidCopyIcon = (item: { uuid: string }) =>
     <Typography data-cy="uuid" noWrap>
-        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-' }
+        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
     </Typography>;
 
 export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
@@ -617,11 +617,8 @@ export const ResourceLinkTailPermissionLevel = connect(
 
 const getResourceLinkCanManage = (state: RootState, link: LinkResource) => {
     const headResource = getResource<Resource>(link.headUuid)(state.resources);
-    // const tailResource = getResource<Resource>(link.tailUuid)(state.resources);
-    const userUuid = getUserUuid(state);
-
     if (headResource && headResource.kind === ResourceKind.GROUP) {
-        return userUuid ? (headResource as GroupResource).writableBy?.includes(userUuid) : false;
+        return (headResource as GroupResource).canManage;
     } else {
         // true for now
         return true;
@@ -687,12 +684,12 @@ const renderUuidLinkWithCopyIcon = (dispatch: Dispatch, item: ProcessResource, c
     const selectedColumnUuid = item[column]
     return <Grid container alignItems="center" wrap="nowrap" >
         <Grid item>
-            {selectedColumnUuid ? 
-                <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} noWrap 
+            {selectedColumnUuid ?
+                <Typography color="primary" style={{ width: 'auto', cursor: 'pointer' }} noWrap
                     onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}>
-                    {selectedColumnUuid} 
-                </Typography> 
-            : '-' }
+                    {selectedColumnUuid}
+                </Typography>
+                : '-'}
         </Grid>
         <Grid item>
             {selectedColumnUuid && renderUuidCopyIcon({ uuid: selectedColumnUuid })}
@@ -716,20 +713,20 @@ export const ResourceParentProcess = connect(
     (state: RootState, props: { uuid: string }) => {
         const process = getProcess(props.uuid)(state.resources)
         return { parentProcess: process?.containerRequest?.requestingContainerUuid || '' };
-    })((props: { parentProcess: string }) => renderUuid({uuid: props.parentProcess}));
+    })((props: { parentProcess: string }) => renderUuid({ uuid: props.parentProcess }));
 
 export const ResourceModifiedByUserUuid = connect(
     (state: RootState, props: { uuid: string }) => {
         const process = getProcess(props.uuid)(state.resources)
         return { userUuid: process?.containerRequest?.modifiedByUserUuid || '' };
-    })((props: { userUuid: string }) => renderUuid({uuid: props.userUuid}));
+    })((props: { userUuid: string }) => renderUuid({ uuid: props.userUuid }));
 
-    export const ResourceCreatedAtDate = connect(
+export const ResourceCreatedAtDate = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
         return { date: resource ? resource.createdAt : '' };
     })((props: { date: string }) => renderDate(props.date));
-    
+
 export const ResourceLastModifiedDate = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
@@ -787,39 +784,39 @@ export const ResourceUUID = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
         return { uuid: resource ? resource.uuid : '' };
-    })((props: { uuid: string }) => renderUuid({uuid: props.uuid}));
+    })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
 
-const renderVersion = (version: number) =>{
+const renderVersion = (version: number) => {
     return <Typography>{version ?? '-'}</Typography>
 }
 
 export const ResourceVersion = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
-        return { version: resource ? resource.version: '' };
+        return { version: resource ? resource.version : '' };
     })((props: { version: number }) => renderVersion(props.version));
-    
-const renderPortableDataHash = (portableDataHash:string | null) => 
+
+const renderPortableDataHash = (portableDataHash: string | null) =>
     <Typography noWrap>
         {portableDataHash ? <>{portableDataHash}
-        <CopyToClipboardSnackbar value={portableDataHash} /></> : '-' }
+            <CopyToClipboardSnackbar value={portableDataHash} /></> : '-'}
     </Typography>
-    
+
 export const ResourcePortableDataHash = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
-        return { portableDataHash: resource ? resource.portableDataHash : '' };    
+        return { portableDataHash: resource ? resource.portableDataHash : '' };
     })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
 
 
-const renderFileCount = (fileCount: number) =>{
+const renderFileCount = (fileCount: number) => {
     return <Typography>{fileCount ?? '-'}</Typography>
 }
 
 export const ResourceFileCount = connect(
     (state: RootState, props: { uuid: string }) => {
         const resource = getResource<CollectionResource>(props.uuid)(state.resources);
-        return { fileCount: resource ? resource.fileCount: '' };
+        return { fileCount: resource ? resource.fileCount : '' };
     })((props: { fileCount: number }) => renderFileCount(props.fileCount));
 
 const userFromID =
@@ -968,12 +965,12 @@ export const CollectionStatus = connect((state: RootState, props: { uuid: string
 
 export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
     return {
-                collection: getResource<CollectionResource>(props.uuid)(state.resources),
-                uuid: props.uuid,
-                className: props.className,
-            };
+        collection: getResource<CollectionResource>(props.uuid)(state.resources),
+        uuid: props.uuid,
+        className: props.className,
+    };
 })((props: { collection: CollectionResource, uuid: string, className?: string }) =>
-        <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
+    <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
 );
 
 export const ProcessStatus = compose(
@@ -994,7 +991,7 @@ export const ProcessStatus = compose(
                 }}
             />
             : <Typography>-</Typography>
-        );
+    );
 
 export const ProcessStartDate = connect(
     (state: RootState, props: { uuid: string }) => {
index 7874441588d51344247950f50d043de1c81e88fb..6acbb1611883e2323b51d51029539a5b955d613c 100644 (file)
@@ -89,8 +89,7 @@ export const SidePanelButton = withStyles(styles)(
                     enabled = true;
                 } else if (matchProjectRoute(location ? location.pathname : '')) {
                     const currentProject = getResource<ProjectResource>(currentItemId)(resources);
-                    if (currentProject && currentProject.writableBy &&
-                        currentProject.writableBy.indexOf(currentUserUUID || '') >= 0 &&
+                    if (currentProject && currentProject.canWrite &&
                         !currentProject.frozenByUuid &&
                         !isProjectTrashed(currentProject, resources) &&
                         currentProject.groupClass !== GroupClass.FILTER) {
index 79cf07bfb46c1db297f8998123c5266ca497cbcc..8cf19c03fef183a24d93f52f31ea1a26af822b14 100644 (file)
@@ -150,8 +150,8 @@ export const CollectionPanel = withStyles(styles)(connect(
                 isWritable = true;
             } else {
                 const itemOwner = getResource<GroupResource | UserResource>(item.ownerUuid)(state.resources);
-                if (itemOwner && itemOwner.writableBy) {
-                    isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
+                if (itemOwner) {
+                    isWritable = itemOwner.canWrite;
                 }
             }
         }
@@ -351,7 +351,7 @@ export const CollectionDetailsAttributes = (props: CollectionDetailsProps) => {
         {/*
             NOTE: The property list should be kept at the bottom, because it spans
             the entire available width, without regards of the twoCol prop.
-        */}
+          */}
         <Grid item xs={12} md={12}>
             <DetailsAttribute classLabel={classes.label} classValue={classes.value}
                 label='Properties' />
index 798a7b67e34906d5cba6aee5255d38b90c670375..fdbc204ee78237dde9e1a4125139c8ac5f5c46c5 100644 (file)
@@ -136,8 +136,8 @@ const mapStateToProps = (state: RootState) => {
     return {
         resources: state.resources,
         groupCanManage: userUuid && !isBuiltinGroup(group?.uuid || '')
-                            ? group?.writableBy?.includes(userUuid)
-                            : false,
+            ? group?.canManage
+            : false,
     };
 };
 
@@ -158,7 +158,7 @@ export const GroupDetailsPanel = withStyles(styles)(connect(
 )(
     class GroupDetailsPanel extends React.Component<GroupDetailsPanelProps & WithStyles<CssRules>> {
         state = {
-          value: 0,
+            value: 0,
         };
 
         componentDidMount() {
@@ -169,56 +169,56 @@ export const GroupDetailsPanel = withStyles(styles)(connect(
             const { value } = this.state;
             return (
                 <Paper className={this.props.classes.root}>
-                  <Tabs value={value} onChange={this.handleChange} variant="fullWidth">
-                      <Tab data-cy="group-details-members-tab" label="MEMBERS" />
-                      <Tab data-cy="group-details-permissions-tab" label="PERMISSIONS" />
-                  </Tabs>
-                  <div className={this.props.classes.content}>
-                    {value === 0 &&
-                        <DataExplorer
-                            id={GROUP_DETAILS_MEMBERS_PANEL_ID}
-                            data-cy="group-members-data-explorer"
-                            onRowClick={noop}
-                            onRowDoubleClick={noop}
-                            onContextMenu={noop}
-                            contextMenuColumn={false}
-                            defaultViewIcon={UserPanelIcon}
-                            defaultViewMessages={[MEMBERS_DEFAULT_MESSAGE]}
-                            hideColumnSelector
-                            hideSearchInput
-                            actions={
+                    <Tabs value={value} onChange={this.handleChange} variant="fullWidth">
+                        <Tab data-cy="group-details-members-tab" label="MEMBERS" />
+                        <Tab data-cy="group-details-permissions-tab" label="PERMISSIONS" />
+                    </Tabs>
+                    <div className={this.props.classes.content}>
+                        {value === 0 &&
+                            <DataExplorer
+                                id={GROUP_DETAILS_MEMBERS_PANEL_ID}
+                                data-cy="group-members-data-explorer"
+                                onRowClick={noop}
+                                onRowDoubleClick={noop}
+                                onContextMenu={noop}
+                                contextMenuColumn={false}
+                                defaultViewIcon={UserPanelIcon}
+                                defaultViewMessages={[MEMBERS_DEFAULT_MESSAGE]}
+                                hideColumnSelector
+                                hideSearchInput
+                                actions={
                                     this.props.groupCanManage &&
                                     <Grid container justify='flex-end'>
                                         <Button
-                                        data-cy="group-member-add"
-                                        variant="contained"
-                                        color="primary"
-                                        onClick={this.props.onAddUser}>
-                                        <AddIcon /> Add user
+                                            data-cy="group-member-add"
+                                            variant="contained"
+                                            color="primary"
+                                            onClick={this.props.onAddUser}>
+                                            <AddIcon /> Add user
                                         </Button>
                                     </Grid>
-                            }
-                            paperProps={{
-                                elevation: 0,
-                            }} />
-                    }
-                    {value === 1 &&
-                        <DataExplorer
-                            id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
-                            data-cy="group-permissions-data-explorer"
-                            onRowClick={noop}
-                            onRowDoubleClick={noop}
-                            onContextMenu={noop}
-                            contextMenuColumn={false}
-                            defaultViewIcon={KeyIcon}
-                            defaultViewMessages={[PERMISSIONS_DEFAULT_MESSAGE]}
-                            hideColumnSelector
-                            hideSearchInput
-                            paperProps={{
-                                elevation: 0,
-                            }} />
-                    }
-                  </div>
+                                }
+                                paperProps={{
+                                    elevation: 0,
+                                }} />
+                        }
+                        {value === 1 &&
+                            <DataExplorer
+                                id={GROUP_DETAILS_PERMISSIONS_PANEL_ID}
+                                data-cy="group-permissions-data-explorer"
+                                onRowClick={noop}
+                                onRowDoubleClick={noop}
+                                onContextMenu={noop}
+                                contextMenuColumn={false}
+                                defaultViewIcon={KeyIcon}
+                                defaultViewMessages={[PERMISSIONS_DEFAULT_MESSAGE]}
+                                hideColumnSelector
+                                hideSearchInput
+                                paperProps={{
+                                    elevation: 0,
+                                }} />
+                        }
+                    </div>
                 </Paper>
             );
         }
index 688af4aafac9f244fe96e07c192efdc6d3bd17da..d91a6b8483f2265f2685e372c09585ab1286071c 100644 (file)
@@ -99,7 +99,7 @@ export const ProjectInputComponent = connect(mapStateToProps)(
             }
         }
 
-        invalid = () => (!this.state.project || this.state.project.writableBy.indexOf(this.props.userUuid) === -1);
+        invalid = () => (!this.state.project || !this.state.project.canWrite);
 
         renderInput() {
             return <GenericInput