Merge branch 'master' into 13704-navigation-details-panel
authorMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 6 Jul 2018 10:42:33 +0000 (12:42 +0200)
committerMichal Klobukowski <michal.klobukowski@contractors.roche.com>
Fri, 6 Jul 2018 10:42:33 +0000 (12:42 +0200)
refs #13704

Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski <michal.klobukowski@contractors.roche.com>

13 files changed:
.gitignore
package.json
src/common/api/common-resource-service.test.ts [new file with mode: 0644]
src/common/api/common-resource-service.ts [new file with mode: 0644]
src/common/api/order-builder.test.ts [new file with mode: 0644]
src/common/api/order-builder.ts [new file with mode: 0644]
src/services/groups-service/groups-service.test.ts [new file with mode: 0644]
src/services/groups-service/groups-service.ts [new file with mode: 0644]
src/services/project-service/project-service.ts [deleted file]
src/services/services.ts
src/store/project/project-action.ts
src/store/project/project-reducer.test.ts
yarn.lock

index 280ad3f03d4d094023a94b68ff82a0affb7d400d..c18f27f6572824668327ebd997eafd3f0c77d76d 100644 (file)
@@ -1,8 +1,15 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
 # See https://help.github.com/ignore-files/ for more about ignoring files.
 
 # dependencies
 /node_modules
 
+# vscode
+/.vs
+
 # testing
 /coverage
 
index 3db240455871d95eecbcb89bb0014d0488a9ebea..f769acc98ff5c2ebeedb95c9e3e23fc1a5002ee4 100644 (file)
@@ -40,6 +40,7 @@
     "@types/react-router-dom": "4.2.7",
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
+    "axios-mock-adapter": "^1.15.0",
     "enzyme": "^3.3.0",
     "enzyme-adapter-react-16": "^1.1.1",
     "jest-localstorage-mock": "2.2.0",
diff --git a/src/common/api/common-resource-service.test.ts b/src/common/api/common-resource-service.test.ts
new file mode 100644 (file)
index 0000000..d28abc4
--- /dev/null
@@ -0,0 +1,62 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import CommonResourceService from "./common-resource-service";
+import axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+
+describe("CommonResourceService", () => {
+
+    const axiosMock = new MockAdapter(axios);
+
+    beforeEach(() => {
+        axiosMock.reset();
+    });
+
+    it("#delete", async () => {
+        axiosMock
+            .onDelete("/resource/uuid")
+            .reply(200, { deleted_at: "now" });
+
+        const commonResourceService = new CommonResourceService(axios, "resource");
+        const resource = await commonResourceService.delete("uuid");
+        expect(resource).toEqual({ deletedAt: "now" });
+    });
+
+    it("#get", async () => {
+        axiosMock
+            .onGet("/resource/uuid")
+            .reply(200, { modified_at: "now" });
+
+        const commonResourceService = new CommonResourceService(axios, "resource");
+        const resource = await commonResourceService.get("uuid");
+        expect(resource).toEqual({ modifiedAt: "now" });
+    });
+
+    it("#list", async () => {
+        axiosMock
+            .onGet("/resource/")
+            .reply(200, {
+                kind: "kind",
+                offset: 2,
+                limit: 10,
+                items: [{
+                    modified_at: "now"
+                }],
+                items_available: 20
+            });
+
+        const commonResourceService = new CommonResourceService(axios, "resource");
+        const resource = await commonResourceService.list({ limit: 10, offset: 1 });
+        expect(resource).toEqual({
+            kind: "kind",
+            offset: 2,
+            limit: 10,
+            items: [{
+                modifiedAt: "now"
+            }],
+            itemsAvailable: 20
+        });
+    });
+});
diff --git a/src/common/api/common-resource-service.ts b/src/common/api/common-resource-service.ts
new file mode 100644 (file)
index 0000000..3e147b2
--- /dev/null
@@ -0,0 +1,108 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as _ from "lodash";
+import FilterBuilder from "./filter-builder";
+import OrderBuilder from "./order-builder";
+import { AxiosInstance } from "axios";
+
+export interface Resource {
+    uuid: string;
+    ownerUuid: string;
+    createdAt: string;
+    modifiedByClientUuid: string;
+    modifiedByUserUuid: string;
+    modifiedAt: string;
+    href: string;
+    kind: string;
+    etag: string;
+}
+
+export interface ListArguments {
+    limit?: number;
+    offset?: number;
+    filters?: FilterBuilder;
+    order?: OrderBuilder;
+    select?: string[];
+    distinct?: boolean;
+    count?: string;
+}
+
+export interface ListResults<T> {
+    kind: string;
+    offset: number;
+    limit: number;
+    items: T[];
+    itemsAvailable: number;
+}
+
+export default class CommonResourceService<T extends Resource> {
+
+    static mapResponseKeys = (response: any): Promise<any> =>
+        CommonResourceService.mapKeys(_.camelCase)(response.data)
+
+    static mapKeys = (mapFn: (key: string) => string) =>
+        (value: any): any => {
+            switch (true) {
+                case _.isPlainObject(value):
+                    return Object
+                        .keys(value)
+                        .map(key => [key, mapFn(key)])
+                        .reduce((newValue, [key, newKey]) => ({
+                            ...newValue,
+                            [newKey]: CommonResourceService.mapKeys(mapFn)(value[key])
+                        }), {});
+                case _.isArray(value):
+                    return value.map(CommonResourceService.mapKeys(mapFn));
+                default:
+                    return value;
+            }
+        }
+
+    protected serverApi: AxiosInstance;
+    protected resourceType: string;
+
+    constructor(serverApi: AxiosInstance, resourceType: string) {
+        this.serverApi = serverApi;
+        this.resourceType = '/' + resourceType + '/';
+    }
+
+    create(data: Partial<T>) {
+        return this.serverApi
+            .post<T>(this.resourceType, data)
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
+    delete(uuid: string): Promise<T> {
+        return this.serverApi
+            .delete(this.resourceType + uuid)
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
+    get(uuid: string) {
+        return this.serverApi
+            .get<T>(this.resourceType + uuid)
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
+    list(args: ListArguments = {}): Promise<ListResults<T>> {
+        const { filters, order, ...other } = args;
+        const params = {
+            ...other,
+            filters: filters ? filters.get() : undefined,
+            order: order ? order.get() : undefined
+        };
+        return this.serverApi
+            .get(this.resourceType, {
+                params: CommonResourceService.mapKeys(_.snakeCase)(params)
+            })
+            .then(CommonResourceService.mapResponseKeys);
+    }
+
+    update(uuid: string) {
+        throw new Error("Not implemented");
+    }
+
+}
+
diff --git a/src/common/api/order-builder.test.ts b/src/common/api/order-builder.test.ts
new file mode 100644 (file)
index 0000000..c184ebc
--- /dev/null
@@ -0,0 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import OrderBuilder from "./order-builder";
+
+describe("OrderBuilder", () => {
+    it("should build correct order query", () => {
+        const orderBuilder = new OrderBuilder();
+        const order = orderBuilder
+            .addAsc("name")
+            .addDesc("modified_at")
+            .get();
+        expect(order).toEqual(["name asc","modified_at desc"]);
+    });
+});
diff --git a/src/common/api/order-builder.ts b/src/common/api/order-builder.ts
new file mode 100644 (file)
index 0000000..cc3eadb
--- /dev/null
@@ -0,0 +1,22 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+
+export default class OrderBuilder {
+    private order: string[] = [];
+
+    addAsc(attribute: string) {
+        this.order.push(`${attribute} asc`);
+        return this;
+    }
+
+    addDesc(attribute: string) {
+        this.order.push(`${attribute} desc`);
+        return this;
+    }
+
+    get() {
+        return this.order;
+    }
+}
diff --git a/src/services/groups-service/groups-service.test.ts b/src/services/groups-service/groups-service.test.ts
new file mode 100644 (file)
index 0000000..2562a59
--- /dev/null
@@ -0,0 +1,42 @@
+// 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 GroupsService from "./groups-service";
+
+describe("GroupsService", () => {
+
+    const axiosMock = new MockAdapter(axios);
+
+    beforeEach(() => {
+        axiosMock.reset();
+    });
+
+    it("#contents", async () => {
+        axiosMock
+            .onGet("/groups/1/contents/")
+            .reply(200, {
+                kind: "kind",
+                offset: 2,
+                limit: 10,
+                items: [{
+                    modified_at: "now"
+                }],
+                items_available: 20
+            });
+
+        const groupsService = new GroupsService(axios);
+        const resource = await groupsService.contents("1", { limit: 10, offset: 1 });
+        expect(resource).toEqual({
+            kind: "kind",
+            offset: 2,
+            limit: 10,
+            items: [{
+                modifiedAt: "now"
+            }],
+            itemsAvailable: 20
+        });
+    });
+});
diff --git a/src/services/groups-service/groups-service.ts b/src/services/groups-service/groups-service.ts
new file mode 100644 (file)
index 0000000..f230c70
--- /dev/null
@@ -0,0 +1,49 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as _ from "lodash";
+import CommonResourceService, { Resource, ListResults } from "../../common/api/common-resource-service";
+import FilterBuilder from "../../common/api/filter-builder";
+import OrderBuilder from "../../common/api/order-builder";
+import { AxiosInstance } from "axios";
+
+interface GroupResource extends Resource {
+    name: string;
+    groupClass: string;
+    description: string;
+    properties: string;
+    writeableBy: string[];
+    trashAt: string;
+    deleteAt: string;
+    isTrashed: boolean;
+}
+
+interface ContensArguments {
+    limit?: number;
+    offset?: number;
+    order?: OrderBuilder;
+    filters?: FilterBuilder;
+    recursive?: boolean;
+}
+
+export default class GroupsService extends CommonResourceService<GroupResource> {
+
+    constructor(serverApi: AxiosInstance) {
+        super(serverApi, "groups");
+    }
+
+    contents (uuid: string, args: ContensArguments = {}): Promise<ListResults<Resource>> {
+        const { filters, order, ...other } = args;
+        const params = {
+            ...other,
+            filters: filters ? filters.get() : undefined,
+            order: order ? order.get() : undefined
+        };
+        return this.serverApi
+            .get(this.resourceType + `${uuid}/contents/`, {
+                params: CommonResourceService.mapKeys(_.snakeCase)(params)
+            })
+            .then(CommonResourceService.mapResponseKeys);
+    }
+}
\ No newline at end of file
diff --git a/src/services/project-service/project-service.ts b/src/services/project-service/project-service.ts
deleted file mode 100644 (file)
index 5bfa544..0000000
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { serverApi } from "../../common/api/server-api";
-import { Dispatch } from "redux";
-import { Project } from "../../models/project";
-import FilterBuilder, { FilterField } from "../../common/api/filter-builder";
-import { ArvadosResource } from "../response";
-import { getResourceKind } from "../../models/resource";
-
-interface GroupResource extends ArvadosResource {
-    name: string;
-    group_class: string;
-    description: string;
-    writable_by: string[];
-    delete_at: string;
-    trash_at: string;
-    is_trashed: boolean;
-}
-
-interface GroupsResponse {
-    offset: number;
-    limit: number;
-    items: GroupResource[];
-}
-
-export default class ProjectService {
-    public getProjectList = (parentUuid?: string): Promise<Project[]> => {
-        if (parentUuid) {
-            const fb = new FilterBuilder();
-            fb.addLike(FilterField.OWNER_UUID, parentUuid);
-            return serverApi.get<GroupsResponse>('/groups', { params: {
-                filters: fb.get()
-            }}).then(resp => {
-                const projects = resp.data.items.map(g => ({
-                    name: g.name,
-                    createdAt: g.created_at,
-                    modifiedAt: g.modified_at,
-                    href: g.href,
-                    uuid: g.uuid,
-                    ownerUuid: g.owner_uuid,
-                    kind: getResourceKind(g.kind)
-                } as Project));
-                return projects;
-            });
-        } else {
-            return Promise.resolve([]);
-        }
-    }
-}
index 47a24b344aaa7d43c0f257d3c47aabd526995368..51d2b760e39d1f006772aab5a453dff1dfbb6e63 100644 (file)
@@ -3,9 +3,10 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import AuthService from "./auth-service/auth-service";
-import ProjectService from "./project-service/project-service";
 import CollectionService from "./collection-service/collection-service";
+import GroupsService from "./groups-service/groups-service";
+import { serverApi } from "../common/api/server-api";
 
 export const authService = new AuthService();
-export const projectService = new ProjectService();
 export const collectionService = new CollectionService();
+export const groupsService = new GroupsService(serverApi);
index 35ff445e43d0d09c660c8ea22f68e2d3cc9d6030..4cc219799e8cfcb371f14927925faf00555ddffb 100644 (file)
@@ -4,8 +4,9 @@
 import { default as unionize, ofType, UnionOf } from "unionize";
 
 import { Project } from "../../models/project";
-import { projectService } from "../../services/services";
+import { groupsService } from "../../services/services";
 import { Dispatch } from "redux";
+import { getResourceKind } from "../../models/resource";
 
 const actions = unionize({
     CREATE_PROJECT: ofType<Project>(),
@@ -19,15 +20,17 @@ const actions = unionize({
         tag: 'type',
         value: 'payload'
     });
-
-export const getProjectList = (parentUuid?: string) => (dispatch: Dispatch): Promise<Project[]> => {
-    if (parentUuid) {
+export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch) => {
         dispatch(actions.PROJECTS_REQUEST(parentUuid));
-        return projectService.getProjectList(parentUuid).then(projects => {
+        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;
         });
-    } return Promise.resolve([]);
 };
 
 export type ProjectAction = UnionOf<typeof actions>;
index f4301040d5b165b709092892873245e9c6c3ca1c..c80f18c82f1ef92a6032cbd60ce46687d0b7cd8f 100644 (file)
@@ -129,12 +129,13 @@ describe('project-reducer', () => {
                     modifiedAt: '2018-01-01',
                     ownerUuid: 'owner-test123',
                     uuid: 'test123',
-                    kind: ResourceKind.PROJECT
+                    kind: ResourceKind.PROJECT,
                 },
                 id: "1",
                 open: true,
                 active: true,
-                status: 1
+                status: 1,
+                toggled: true
             }],
             currentItemId: "1"
         };
index 9a3379b13a4082d1095a389ee2c1551c9bbff1ab..d71a56150c9eebd9558f20fcdb3813561226315f 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -477,6 +477,12 @@ aws4@^1.6.0:
   version "1.7.0"
   resolved "https://registry.yarnpkg.com/aws4/-/aws4-1.7.0.tgz#d4d0e9b9dbfca77bf08eeb0a8a471550fe39e289"
 
+axios-mock-adapter@^1.15.0:
+  version "1.15.0"
+  resolved "https://registry.yarnpkg.com/axios-mock-adapter/-/axios-mock-adapter-1.15.0.tgz#fbc06825d8302c95c3334d21023bba996255d45d"
+  dependencies:
+    deep-equal "^1.0.1"
+
 axios@0.18.0:
   version "0.18.0"
   resolved "https://registry.yarnpkg.com/axios/-/axios-0.18.0.tgz#32d53e4851efdc0a11993b6cd000789d70c05102"