[ARVADOS-WORKBENCH2] created: 1.4.1-360-g2bb21b40
Git user
git at public.arvados.org
Mon Jun 8 15:59:14 UTC 2020
at 2bb21b40ca65f6be7ead9eb39d9eba64c9fd1f23 (commit)
commit 2bb21b40ca65f6be7ead9eb39d9eba64c9fd1f23
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: Daniel Kutyła <daniel.kutyla at contractors.roche.com>
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