[ARVADOS-WORKBENCH2] created: 1.4.1-358-g6828128e

Git user git at public.arvados.org
Fri Jun 5 20:39:23 UTC 2020


        at  6828128e5f3270c803231a2746b2b3435fc9bb23 (commit)


commit 6828128e5f3270c803231a2746b2b3435fc9bb23
Author: Daniel KutyƂa <daniel.kutyla at contractors.roche.com>
Date:   Fri Jun 5 22:35:40 2020 +0200

    16437: Removes context items when projects are not editable by user
    
    Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas at di-pentima.com.ar>

diff --git a/.env b/.env
index bd410081..cd0f6b08 100644
--- a/.env
+++ b/.env
@@ -3,5 +3,5 @@
 # SPDX-License-Identifier: AGPL-3.0
 
 REACT_APP_ARVADOS_CONFIG_URL=/config.json
-REACT_APP_ARVADOS_API_HOST=c97qk.arvadosapi.com
-HTTPS=true
\ No newline at end of file
+REACT_APP_ARVADOS_API_HOST=ce8i5.arvadosapi.com
+HTTPS=false
\ No newline at end of file
diff --git a/src/index.tsx b/src/index.tsx
index a12dabfa..2cee0540 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -21,7 +21,7 @@ import { CustomTheme } from '~/common/custom-theme';
 import { fetchConfig } from '~/common/config';
 import { addMenuActionSet, ContextMenuKind } from '~/views-components/context-menu/context-menu';
 import { rootProjectActionSet } from "~/views-components/context-menu/action-sets/root-project-action-set";
-import { projectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
+import { projectActionSet, readOnlyProjectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
 import { resourceActionSet } from '~/views-components/context-menu/action-sets/resource-action-set';
 import { favoriteActionSet } from "~/views-components/context-menu/action-sets/favorite-action-set";
 import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from '~/views-components/context-menu/action-sets/collection-files-action-set';
@@ -67,6 +67,7 @@ console.log(`Starting arvados [${getBuildInfo()}]`);
 
 addMenuActionSet(ContextMenuKind.ROOT_PROJECT, rootProjectActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT, projectActionSet);
+addMenuActionSet(ContextMenuKind.READONLY_PROJECT, readOnlyProjectActionSet);
 addMenuActionSet(ContextMenuKind.RESOURCE, resourceActionSet);
 addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
 addMenuActionSet(ContextMenuKind.COLLECTION_FILES, collectionFilesActionSet);
diff --git a/src/models/resource.ts b/src/models/resource.ts
index 4708a9da..d8cdd4a0 100644
--- a/src/models/resource.ts
+++ b/src/models/resource.ts
@@ -14,6 +14,10 @@ export interface Resource {
     etag: string;
 }
 
+export interface EditableResource extends Resource {
+    isEditable: boolean;
+}
+
 export interface TrashableResource extends Resource {
     trashAt: string;
     deleteAt: string;
diff --git a/src/store/context-menu/context-menu-actions.test.ts b/src/store/context-menu/context-menu-actions.test.ts
new file mode 100644
index 00000000..04657e90
--- /dev/null
+++ b/src/store/context-menu/context-menu-actions.test.ts
@@ -0,0 +1,161 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as resource from '~/models/resource';
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { resourceKindToContextMenuKind } from './context-menu-actions';
+
+describe('context-menu-actions', () => {
+    describe('resourceKindToContextMenuKind', () => {
+        const uuid = '123';
+
+        describe('ResourceKind.PROJECT', () => {
+            beforeEach(() => {
+                // setup
+                jest.spyOn(resource, 'extractUuidKind')
+                    .mockImplementation(() => resource.ResourceKind.PROJECT);
+            });
+
+            it('should return ContextMenuKind.PROJECT_ADMIN', () => {
+                // given
+                const isAdmin = true;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.PROJECT_ADMIN);
+            });
+    
+            it('should return ContextMenuKind.PROJECT', () => {
+                // given
+                const isAdmin = false;
+                const isEditable = true;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.PROJECT);
+            });
+    
+            it('should return ContextMenuKind.READONLY_PROJECT', () => {
+                // given
+                const isAdmin = false;
+                const isEditable = false;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.READONLY_PROJECT);
+            });
+        });
+
+        describe('ResourceKind.COLLECTION', () => {
+            beforeEach(() => {
+                // setup
+                jest.spyOn(resource, 'extractUuidKind')
+                    .mockImplementation(() => resource.ResourceKind.COLLECTION);
+            });
+
+            it('should return ContextMenuKind.COLLECTION_ADMIN', () => {
+                // given
+                const isAdmin = true;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.COLLECTION_ADMIN);
+            });
+    
+            it('should return ContextMenuKind.COLLECTION_RESOURCE', () => {
+                // given
+                const isAdmin = false;
+                const isEditable = true;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.COLLECTION_RESOURCE);
+            });
+    
+            it('should return ContextMenuKind.READONLY_COLLECTION', () => {
+                // given
+                const isAdmin = false;
+                const isEditable = false;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin, isEditable);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.READONLY_COLLECTION);
+            });
+        });
+
+        describe('ResourceKind.PROCESS', () => {
+            beforeEach(() => {
+                // setup
+                jest.spyOn(resource, 'extractUuidKind')
+                    .mockImplementation(() => resource.ResourceKind.PROCESS);
+            });
+
+            it('should return ContextMenuKind.PROCESS_ADMIN', () => {
+                // given
+                const isAdmin = true;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.PROCESS_ADMIN);
+            });
+
+            it('should return ContextMenuKind.PROCESS_RESOURCE', () => {
+                // given
+                const isAdmin = false;
+    
+                // when
+                const result = resourceKindToContextMenuKind(uuid, isAdmin);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.PROCESS_RESOURCE);
+            });
+        });
+
+        describe('ResourceKind.USER', () => {
+            beforeEach(() => {
+                // setup
+                jest.spyOn(resource, 'extractUuidKind')
+                    .mockImplementation(() => resource.ResourceKind.USER);
+            });
+
+            it('should return ContextMenuKind.ROOT_PROJECT', () => {    
+                // when
+                const result = resourceKindToContextMenuKind(uuid);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.ROOT_PROJECT);
+            });
+        });
+
+        describe('ResourceKind.LINK', () => {
+            beforeEach(() => {
+                // setup
+                jest.spyOn(resource, 'extractUuidKind')
+                    .mockImplementation(() => resource.ResourceKind.LINK);
+            });
+
+            it('should return ContextMenuKind.LINK', () => {    
+                // when
+                const result = resourceKindToContextMenuKind(uuid);
+    
+                // then
+                expect(result).toEqual(ContextMenuKind.LINK);
+            });
+        });
+    });
+});
\ No newline at end of file
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 2ba6bc2c..4afdb01d 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -34,6 +34,7 @@ export type ContextMenuResource = {
     kind: ResourceKind,
     menuKind: ContextMenuKind;
     isTrashed?: boolean;
+    isEditable?: boolean;
     outputUuid?: string;
     workflowUuid?: string;
 };
@@ -198,13 +199,17 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
         }
     };
 
-export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean) => {
+export const resourceKindToContextMenuKind = (uuid: string, isAdmin?: boolean, isEditable?: boolean) => {
     const kind = extractUuidKind(uuid);
     switch (kind) {
         case ResourceKind.PROJECT:
-            return !isAdmin ? ContextMenuKind.PROJECT : ContextMenuKind.PROJECT_ADMIN;
+            return !isAdmin ?
+                isEditable ? ContextMenuKind.PROJECT : ContextMenuKind.READONLY_PROJECT :
+                ContextMenuKind.PROJECT_ADMIN;
         case ResourceKind.COLLECTION:
-            return !isAdmin ? ContextMenuKind.COLLECTION_RESOURCE : ContextMenuKind.COLLECTION_ADMIN;
+            return !isAdmin ?
+                isEditable ? ContextMenuKind.COLLECTION_RESOURCE : ContextMenuKind.READONLY_COLLECTION :
+                ContextMenuKind.COLLECTION_ADMIN;
         case ResourceKind.PROCESS:
             return !isAdmin ? ContextMenuKind.PROCESS_RESOURCE : ContextMenuKind.PROCESS_ADMIN;
         case ResourceKind.USER:
diff --git a/src/store/resources/resources.test.ts b/src/store/resources/resources.test.ts
new file mode 100644
index 00000000..56575510
--- /dev/null
+++ b/src/store/resources/resources.test.ts
@@ -0,0 +1,103 @@
+import { getResourceWithEditableStatus } from "./resources";
+import { ResourceKind } from "~/models/resource";
+
+describe('resources', () => {
+    describe('getResourceWithEditableStatus', () => {
+        const resourcesState = {
+            '123': {
+                uuid: '123',
+                ownerUuid: '789',
+                createdAt: 'string',
+                modifiedByClientUuid: 'string',
+                modifiedByUserUuid: 'string',
+                modifiedAt: 'string',
+                href: 'string',
+                kind: ResourceKind.PROJECT,
+                writableBy: ['789'],
+                etag: 'string',
+            },
+            '456': {
+                uuid: '456',
+                ownerUuid: '123',
+                createdAt: 'string',
+                modifiedByClientUuid: 'string',
+                modifiedByUserUuid: 'string',
+                modifiedAt: 'string',
+                href: 'string',
+                kind: ResourceKind.COLLECTION,
+                etag: 'string',
+            },
+            '321': {
+                uuid: '123',
+                ownerUuid: '987',
+                createdAt: 'string',
+                modifiedByClientUuid: 'string',
+                modifiedByUserUuid: 'string',
+                modifiedAt: 'string',
+                href: 'string',
+                kind: ResourceKind.PROJECT,
+                writableBy: ['987'],
+                etag: 'string',
+            },
+            '654': {
+                uuid: '456',
+                ownerUuid: '321',
+                createdAt: 'string',
+                modifiedByClientUuid: 'string',
+                modifiedByUserUuid: 'string',
+                modifiedAt: 'string',
+                href: 'string',
+                kind: ResourceKind.COLLECTION,
+                etag: 'string',
+            },
+        };
+
+        it('should return editable project resource', () => {
+            // given
+            const id = '123';
+            const userUuid = '789';
+
+            // when
+            const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+            // then
+            expect(result!.isEditable).toBeTruthy();
+        });
+
+        it('should return editable collection resource', () => {
+            // given
+            const id = '456';
+            const userUuid = '789';
+
+            // when
+            const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+            // then
+            expect(result!.isEditable).toBeTruthy();
+        });
+
+        it('should return not editable project resource', () => {
+            // given
+            const id = '321';
+            const userUuid = '789';
+
+            // when
+            const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+            // then
+            expect(result!.isEditable).toBeFalsy();
+        });
+
+        it('should return not editable collection resource', () => {
+            // given
+            const id = '654';
+            const userUuid = '789';
+
+            // when
+            const result = getResourceWithEditableStatus(id, userUuid)(resourcesState);
+
+            // then
+            expect(result!.isEditable).toBeFalsy();
+        });
+    });
+});
\ No newline at end of file
diff --git a/src/store/resources/resources.ts b/src/store/resources/resources.ts
index e7153dec..2d2b212b 100644
--- a/src/store/resources/resources.ts
+++ b/src/store/resources/resources.ts
@@ -2,11 +2,43 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { Resource } from "~/models/resource";
+import { Resource, EditableResource } from "~/models/resource";
 import { ResourceKind } from '~/models/resource';
+import { ProjectResource } from "~/models/project";
 
 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 ? writableBy : getResourceWritableBy(state, resource.ownerUuid, userUuid);
+};
+
+export const getResourceWithEditableStatus = <T extends EditableResource & ProjectResource>(id: string, userUuid?: string) => 
+    (state: ResourcesState): T | undefined => {
+        const resource = JSON.parse(JSON.stringify(state[id] as T));
+
+        if (resource) {
+            resource.isEditable = userUuid ? getResourceWritableBy(state, id, userUuid).indexOf(userUuid) > -1 : false;
+        }
+
+        return resource;
+    };
+
 export const getResource = <T extends Resource = Resource>(id: string) =>
     (state: ResourcesState): T | undefined =>
         state[id] as T;
diff --git a/src/views-components/context-menu/action-sets/project-action-set.test.ts b/src/views-components/context-menu/action-sets/project-action-set.test.ts
new file mode 100644
index 00000000..b00802cc
--- /dev/null
+++ b/src/views-components/context-menu/action-sets/project-action-set.test.ts
@@ -0,0 +1,27 @@
+import { projectActionSet, readOnlyProjectActionSet } from "./project-action-set";
+
+describe('project-action-set', () => {
+    describe('projectActionSet', () => {
+        it('should not be empty', () => {
+            // then
+            expect(projectActionSet).toHaveLength(2);
+        });
+
+        it('should contain readOnlyProjectActionSet items', () => {
+            // then
+            expect(projectActionSet).toContain(readOnlyProjectActionSet[0]);
+        })
+    });
+
+    describe('readOnlyProjectActionSet', () => {
+        it('should not be empty', () => {
+            // then
+            expect(readOnlyProjectActionSet).toHaveLength(1);
+        });
+
+        it('should not contain projectActionSet items', () => {
+            // then
+            expect(readOnlyProjectActionSet).toContain(projectActionSet[0]);
+        })
+    });
+});
\ No newline at end of file
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
index 32616fce..608cded4 100644
--- a/src/views-components/context-menu/action-sets/project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/project-action-set.ts
@@ -17,28 +17,7 @@ import { openSharingDialog } from "~/store/sharing-dialog/sharing-dialog-actions
 import { openAdvancedTabDialog } from "~/store/advanced-tab/advanced-tab";
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
-export const projectActionSet: ContextMenuActionSet = [[
-    {
-        icon: NewProjectIcon,
-        name: "New project",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProjectCreateDialog(resource.uuid));
-        }
-    },
-    {
-        icon: RenameIcon,
-        name: "Edit project",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProjectUpdateDialog(resource));
-        }
-    },
-    {
-        icon: ShareIcon,
-        name: "Share",
-        execute: (dispatch, { uuid }) => {
-            dispatch<any>(openSharingDialog(uuid));
-        }
-    },
+export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     {
         component: ToggleFavoriteAction,
         execute: (dispatch, resource) => {
@@ -47,13 +26,6 @@ export const projectActionSet: ContextMenuActionSet = [[
             });
         }
     },
-    {
-        icon: MoveToIcon,
-        name: "Move to",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openMoveProjectDialog(resource));
-        }
-    },
     // {
     //     icon: CopyIcon,
     //     name: "Copy to project",
@@ -75,10 +47,41 @@ export const projectActionSet: ContextMenuActionSet = [[
             dispatch<any>(openAdvancedTabDialog(resource.uuid));
         }
     },
+]];
+
+export const projectActionSet: ContextMenuActionSet = readOnlyProjectActionSet.concat([[
+    {
+        icon: NewProjectIcon,
+        name: "New project",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openProjectCreateDialog(resource.uuid));
+        }
+    },
+    {
+        icon: RenameIcon,
+        name: "Edit project",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openProjectUpdateDialog(resource));
+        }
+    },
+    {
+        icon: ShareIcon,
+        name: "Share",
+        execute: (dispatch, { uuid }) => {
+            dispatch<any>(openSharingDialog(uuid));
+        }
+    },
+    {
+        icon: MoveToIcon,
+        name: "Move to",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openMoveProjectDialog(resource));
+        }
+    },
     {
         component: ToggleTrashAction,
         execute: (dispatch, resource) => {
             dispatch<any>(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
         }
     },
-]];
+]]);
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index 55b0abd8..db5765ee 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -65,6 +65,7 @@ export enum ContextMenuKind {
     API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",
     ROOT_PROJECT = "RootProject",
     PROJECT = "Project",
+    READONLY_PROJECT = 'ReadOnlyProject',
     PROJECT_ADMIN = "ProjectAdmin",
     RESOURCE = "Resource",
     FAVORITE = "Favorite",
diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx
index 1e26bc0d..24c38b26 100644
--- a/src/views/project-panel/project-panel.tsx
+++ b/src/views/project-panel/project-panel.tsx
@@ -14,11 +14,11 @@ import { RootState } from '~/store/store';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { ContainerRequestState } from '~/models/container-request';
 import { SortDirection } from '~/components/data-table/data-column';
-import { ResourceKind, Resource } from '~/models/resource';
+import { ResourceKind, Resource, EditableResource } from '~/models/resource';
 import { ResourceFileSize, ResourceLastModifiedDate, ProcessStatus, ResourceType, ResourceOwner } from '~/views-components/data-explorer/renderers';
 import { ProjectIcon } from '~/components/icon/icon';
 import { ResourceName } from '~/views-components/data-explorer/renderers';
-import { ResourcesState, getResource } from '~/store/resources/resources';
+import { ResourcesState, getResourceWithEditableStatus } from '~/store/resources/resources';
 import { loadDetailsPanel } from '~/store/details-panel/details-panel-action';
 import { resourceKindToContextMenuKind, openContextMenu } from '~/store/context-menu/context-menu-actions';
 import { ProjectResource } from '~/models/project';
@@ -115,6 +115,7 @@ interface ProjectPanelDataProps {
     currentItemId: string;
     resources: ResourcesState;
     isAdmin: boolean;
+    userUuid?: string;
 }
 
 type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
@@ -124,7 +125,8 @@ export const ProjectPanel = withStyles(styles)(
     connect((state: RootState) => ({
         currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
         resources: state.resources,
-        isAdmin: state.auth.user!.isAdmin
+        isAdmin: state.auth.user!.isAdmin,
+        userUuid: state.auth.user!.uuid,
     }))(
         class extends React.Component<ProjectPanelProps> {
             render() {
@@ -149,8 +151,9 @@ export const ProjectPanel = withStyles(styles)(
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
-                const menuKind = resourceKindToContextMenuKind(resourceUuid, this.props.isAdmin);
-                const resource = getResource<ProjectResource>(resourceUuid)(this.props.resources);
+                const { isAdmin, userUuid, resources } = this.props;
+                const resource = getResourceWithEditableStatus<ProjectResource & EditableResource>(resourceUuid, userUuid)(resources);
+                const menuKind = resourceKindToContextMenuKind(resourceUuid, isAdmin, (resource || {} as EditableResource).isEditable);
                 if (menuKind && resource) {
                     this.props.dispatch<any>(openContextMenu(event, {
                         name: resource.name,

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list