Merge remote-tracking branch 'origin' into 13694-Data-operations-Project-creation
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 9 Jul 2018 13:52:10 +0000 (15:52 +0200)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Mon, 9 Jul 2018 13:52:10 +0000 (15:52 +0200)
refs #13694

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

14 files changed:
.env [new file with mode: 0644]
README.md
src/common/api/common-resource-service.test.ts
src/common/api/common-resource-service.ts
src/common/api/server-api.ts
src/components/context-menu/context-menu.tsx
src/index.tsx
src/models/group.ts
src/models/project.ts
src/services/groups-service/groups-service.ts
src/services/project-service/project-service.test.ts [new file with mode: 0644]
src/services/project-service/project-service.ts [new file with mode: 0644]
src/services/services.ts
src/store/project/project-action.ts

diff --git a/.env b/.env
new file mode 100644 (file)
index 0000000..13aaad5
--- /dev/null
+++ b/.env
@@ -0,0 +1,5 @@
+# Copyright (C) The Arvados Authors. All rights reserved.
+# 
+# SPDX-License-Identifier: AGPL-3.0
+
+REACT_APP_ARVADOS_API_HOST=https://qr1hi.arvadosapi.com
\ No newline at end of file
index eaed189471536f0a90efc8bebe71b9df6e28c7e9..864a54fa89a122aab17afffa18d9828e3de0c050 100644 (file)
--- a/README.md
+++ b/README.md
@@ -26,6 +26,14 @@ yarn install
 yarn build
 </pre>
 
+### Configuration
+You can customize project global variables using env variables. Default values are placed in the `.env` file.
+
+Example:
+```
+REACT_APP_ARVADOS_API_HOST=localhost:8000 yarn start
+```
+
 ### Licensing
 
 Arvados is Free Software. See COPYING for information about Arvados Free
index d28abc459448ce033d2d51580989f473da73d7b5..7093b59c555430953ed0e678ee56ff10566aa4bb 100644 (file)
@@ -7,19 +7,36 @@ import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 
 describe("CommonResourceService", () => {
-
-    const axiosMock = new MockAdapter(axios);
+    const axiosInstance = axios.create();
+    const axiosMock = new MockAdapter(axiosInstance);
 
     beforeEach(() => {
         axiosMock.reset();
     });
 
+    it("#create", async () => {
+        axiosMock
+            .onPost("/resource/")
+            .reply(200, { owner_uuid: "ownerUuidValue" });
+
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const resource = await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
+        expect(resource).toEqual({ ownerUuid: "ownerUuidValue" });
+    });
+
+    it("#create maps request params to snake case", async () => {
+        axiosInstance.post = jest.fn(() => Promise.resolve({data: {}}));
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
+        expect(axiosInstance.post).toHaveBeenCalledWith("/resource/", {owner_uuid: "ownerUuidValue"});
+    });
+
     it("#delete", async () => {
         axiosMock
             .onDelete("/resource/uuid")
             .reply(200, { deleted_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axios, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
         const resource = await commonResourceService.delete("uuid");
         expect(resource).toEqual({ deletedAt: "now" });
     });
@@ -29,7 +46,7 @@ describe("CommonResourceService", () => {
             .onGet("/resource/uuid")
             .reply(200, { modified_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axios, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
         const resource = await commonResourceService.get("uuid");
         expect(resource).toEqual({ modifiedAt: "now" });
     });
@@ -47,7 +64,7 @@ describe("CommonResourceService", () => {
                 items_available: 20
             });
 
-        const commonResourceService = new CommonResourceService(axios, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
         const resource = await commonResourceService.list({ limit: 10, offset: 1 });
         expect(resource).toEqual({
             kind: "kind",
index fe6c752c8ee1d4b2b42cc8d5bc64640ca8e93171..8df179cd65d2d8d3f369bbc6aa1dc1ef246a8c52 100644 (file)
@@ -70,7 +70,7 @@ export default class CommonResourceService<T extends Resource> {
 
     create(data: Partial<T>) {
         return this.serverApi
-            .post<T>(this.resourceType, data)
+            .post<T>(this.resourceType, CommonResourceService.mapKeys(_.snakeCase)(data))
             .then(CommonResourceService.mapResponseKeys);
     }
 
index 2e676dde5c4a5ae76aa5ed4a71319d5eb1740351..330ce657e23bb5cb54a21ecf4a5e82d135446348 100644 (file)
@@ -4,7 +4,7 @@
 
 import Axios, { AxiosInstance } from "axios";
 
-export const API_HOST = 'https://qr1hi.arvadosapi.com';
+export const API_HOST = process.env.REACT_APP_ARVADOS_API_HOST;
 
 export const serverApi: AxiosInstance = Axios.create({
     baseURL: API_HOST + '/arvados/v1'
index 48e63ba0b975916d42dd03845f52eb279a15a1eb..c892ba2616dda6480de47d2c3767596636267917 100644 (file)
@@ -28,7 +28,8 @@ export default class ContextMenu<T> extends React.PureComponent<ContextMenuProps
             open={!!anchorEl}
             onClose={onClose}
             transformOrigin={DefaultTransformOrigin}
-            anchorOrigin={DefaultTransformOrigin}>
+            anchorOrigin={DefaultTransformOrigin}
+            onContextMenu={this.handleContextMenu}>
             <List dense>
                 {actions.map((group, groupIndex) =>
                     <React.Fragment key={groupIndex}>
@@ -49,4 +50,9 @@ export default class ContextMenu<T> extends React.PureComponent<ContextMenuProps
             </List>
         </Popover>;
     }
+
+    handleContextMenu = (event: React.MouseEvent<HTMLElement>) => {
+        event.preventDefault();
+        this.props.onClose();
+    }
 }
index 21ecdab1ca0c5cf70dc84893501154d7601a3235..be01e0a0769505e43b55a9e320bad63949a32bbe 100644 (file)
@@ -23,22 +23,21 @@ const history = createBrowserHistory();
 const store = configureStore(history);
 
 store.dispatch(authActions.INIT());
-const rootUuid = authService.getRootUuid();
-store.dispatch<any>(getProjectList(rootUuid));
+store.dispatch<any>(getProjectList(authService.getUuid()));
 
 const App = () =>
     <MuiThemeProvider theme={CustomTheme}>
         <Provider store={store}>
             <ConnectedRouter history={history}>
                 <div>
-                    <Route path="/" component={Workbench}/>
-                    <Route path="/token" component={ApiToken}/>
+                    <Route path="/" component={Workbench} />
+                    <Route path="/token" component={ApiToken} />
                 </div>
             </ConnectedRouter>
         </Provider>
     </MuiThemeProvider>;
 
 ReactDOM.render(
-    <App/>,
+    <App />,
     document.getElementById('root') as HTMLElement
 );
index dae516bddb0f19548b0417bef78f33632aa53648..4bb9a7fd661eba4c836f7a735f0b8674ec6ff5af 100644 (file)
@@ -8,11 +8,15 @@ import { ResourceKind } from "./kinds";
 export interface GroupResource extends Resource {
     kind: ResourceKind.Group;
     name: string;
-    groupClass: string;
+    groupClass: GroupClass | null;
     description: string;
     properties: string;
     writeableBy: string[];
     trashAt: string;
     deleteAt: string;
     isTrashed: boolean;
+}
+
+export enum GroupClass {
+    Project = "project"
 }
\ No newline at end of file
index beb9810cc0282a8830719887194bb90de259cab8..c44c8cc7974a9162583ef6b01cfbb3e81281734d 100644 (file)
@@ -3,11 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { Resource as R } from "./resource";
-import { GroupResource } from "./group";
+import { GroupResource, GroupClass } from "./group";
 
 export interface Project extends R {
 }
 
 export interface ProjectResource extends GroupResource {
-    groupClass: "project";
+    groupClass: GroupClass.Project;
 }
index ed61297dabe1fb82f78a50a08d6df05917f070a4..2a5a51a851b8e48a0741e085164dbaff9f6c7e8c 100644 (file)
@@ -12,7 +12,7 @@ import { CollectionResource } from "../../models/collection";
 import { ProjectResource } from "../../models/project";
 import { ProcessResource } from "../../models/process";
 
-interface ContensArguments {
+export interface ContentsArguments {
     limit?: number;
     offset?: number;
     order?: OrderBuilder;
@@ -25,13 +25,13 @@ export type GroupContentsResource =
     ProjectResource |
     ProcessResource;
 
-export default class GroupsService extends CommonResourceService<GroupResource> {
+export default class GroupsService<T extends GroupResource = GroupResource> extends CommonResourceService<T> {
 
     constructor(serverApi: AxiosInstance) {
         super(serverApi, "groups");
     }
 
-    contents(uuid: string, args: ContensArguments = {}): Promise<ListResults<GroupContentsResource>> {
+    contents(uuid: string, args: ContentsArguments = {}): Promise<ListResults<GroupContentsResource>> {
         const { filters, order, ...other } = args;
         const params = {
             ...other,
diff --git a/src/services/project-service/project-service.test.ts b/src/services/project-service/project-service.test.ts
new file mode 100644 (file)
index 0000000..68df245
--- /dev/null
@@ -0,0 +1,39 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import ProjectService from "./project-service";
+import FilterBuilder from "../../common/api/filter-builder";
+import { ProjectResource } from "../../models/project";
+
+describe("CommonResourceService", () => {
+    const axiosInstance = axios.create();
+
+    it(`#create has groupClass set to "project"`, async () => {
+        axiosInstance.post = jest.fn(() => Promise.resolve({ data: {} }));
+        const projectService = new ProjectService(axiosInstance);
+        const resource = await projectService.create({ name: "nameValue" });
+        expect(axiosInstance.post).toHaveBeenCalledWith("/groups/", {
+            name: "nameValue",
+            group_class: "project"
+        });
+    });
+
+
+    it("#list has groupClass filter set by default", async () => {
+        axiosInstance.get = jest.fn(() => Promise.resolve({ data: {} }));
+        const projectService = new ProjectService(axiosInstance);
+        const resource = await projectService.list();
+        expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
+            params: {
+                filters: FilterBuilder
+                    .create<ProjectResource>()
+                    .addEqual("groupClass", "project")
+                    .serialize()
+            }
+        });
+    });
+    
+});
diff --git a/src/services/project-service/project-service.ts b/src/services/project-service/project-service.ts
new file mode 100644 (file)
index 0000000..9ce9e21
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import GroupsService, { ContentsArguments } from "../groups-service/groups-service";
+import { ProjectResource } from "../../models/project";
+import { GroupClass } from "../../models/group";
+import { ListArguments } from "../../common/api/common-resource-service";
+import FilterBuilder from "../../common/api/filter-builder";
+
+export default class ProjectService extends GroupsService<ProjectResource> {
+
+    create(data: Partial<ProjectResource>) {
+        const projectData = { ...data, groupClass: GroupClass.Project };
+        return super.create(projectData);
+    }
+
+    list(args: ListArguments = {}) {
+        return super.list({
+            ...args,
+            filters: this.addProjectFilter(args.filters)
+        });
+    }
+
+    private addProjectFilter(filters?: FilterBuilder) {
+        return FilterBuilder
+            .create()
+            .concat(filters
+                ? filters
+                : FilterBuilder.create())
+            .concat(FilterBuilder
+                .create<ProjectResource>()
+                .addEqual("groupClass", GroupClass.Project));
+    }
+
+}
\ No newline at end of file
index 51d2b760e39d1f006772aab5a453dff1dfbb6e63..1e9a74dd3fbf3b22d50c4ea8faec96f5a8452cff 100644 (file)
@@ -6,7 +6,9 @@ import AuthService from "./auth-service/auth-service";
 import CollectionService from "./collection-service/collection-service";
 import GroupsService from "./groups-service/groups-service";
 import { serverApi } from "../common/api/server-api";
+import ProjectService from "./project-service/project-service";
 
 export const authService = new AuthService();
 export const collectionService = new CollectionService();
 export const groupsService = new GroupsService(serverApi);
+export const projectService = new ProjectService(serverApi);
index 4cc219799e8cfcb371f14927925faf00555ddffb..516934969310fa7c5331d49719b190c5fc8d990b 100644 (file)
@@ -3,10 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 import { default as unionize, ofType, UnionOf } from "unionize";
 
-import { Project } from "../../models/project";
-import { groupsService } from "../../services/services";
+import { Project, ProjectResource } from "../../models/project";
+import { projectService } from "../../services/services";
 import { Dispatch } from "redux";
 import { getResourceKind } from "../../models/resource";
+import FilterBuilder from "../../common/api/filter-builder";
 
 const actions = unionize({
     CREATE_PROJECT: ofType<Project>(),
@@ -20,17 +21,21 @@ const actions = unionize({
         tag: 'type',
         value: 'payload'
     });
+
 export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch) => {
-        dispatch(actions.PROJECTS_REQUEST(parentUuid));
-        return groupsService.list().then(listResults => {
-            const projects = listResults.items.map(item => ({
-                ...item,
-                kind: getResourceKind(item.kind)
-            }));
-            dispatch(actions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
-            return projects;
-        });
+    dispatch(actions.PROJECTS_REQUEST(parentUuid));
+    return projectService.list({
+        filters: FilterBuilder
+            .create<ProjectResource>()
+            .addEqual("ownerUuid", parentUuid)
+    }).then(listResults => {
+        const projects = listResults.items.map(item => ({
+            ...item,
+            kind: getResourceKind(item.kind)
+        }));
+        dispatch(actions.PROJECTS_SUCCESS({ projects, parentItemId: parentUuid }));
+        return projects;
+    });
 };
 
 export type ProjectAction = UnionOf<typeof actions>;