Rename progressFn to api actions, add colors to snackbar
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 18 Sep 2018 05:44:55 +0000 (07:44 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 18 Sep 2018 05:44:55 +0000 (07:44 +0200)
Feature #14186

Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

22 files changed:
src/index.tsx
src/services/api/api-actions.ts [moved from src/services/api/api-progress.ts with 53% similarity]
src/services/auth-service/auth-service.ts
src/services/collection-service/collection-service.ts
src/services/common-service/common-resource-service.test.ts
src/services/common-service/common-resource-service.ts
src/services/common-service/trashable-resource-service.ts
src/services/container-request-service/container-request-service.ts
src/services/container-service/container-service.ts
src/services/groups-service/groups-service.test.ts
src/services/groups-service/groups-service.ts
src/services/keep-service/keep-service.ts
src/services/link-service/link-service.ts
src/services/log-service/log-service.ts
src/services/project-service/project-service.test.ts
src/services/services.ts
src/services/user-service/user-service.ts
src/store/auth/auth-actions.test.ts
src/store/auth/auth-reducer.test.ts
src/store/snackbar/snackbar-actions.ts
src/store/snackbar/snackbar-reducer.ts
src/views-components/snackbar/snackbar.tsx

index 2fb236d011bd118c2f3918861b0628a7295a7c5b..b74329f6f2ce310573c60f09266313a8c9b7bbb3 100644 (file)
@@ -38,6 +38,7 @@ import { addRouteChangeHandlers } from './routes/route-change-handlers';
 import { setCurrentTokenDialogApiHost } from '~/store/current-token-dialog/current-token-dialog-actions';
 import { processResourceActionSet } from './views-components/context-menu/action-sets/process-resource-action-set';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
+import { snackbarActions } from "~/store/snackbar/snackbar-actions";
 
 const getBuildNumber = () => "BN-" + (process.env.REACT_APP_BUILD_NUMBER || "dev");
 const getGitCommit = () => "GIT-" + (process.env.REACT_APP_GIT_COMMIT || "latest").substr(0, 7);
@@ -62,8 +63,13 @@ addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 fetchConfig()
     .then(({ config, apiHost }) => {
         const history = createBrowserHistory();
-        const services = createServices(config, (id, working) => {
-            store.dispatch(progressIndicatorActions.TOGGLE({ id, working }));
+        const services = createServices(config, {
+            progressFn: (id, working) => {
+                store.dispatch(progressIndicatorActions.TOGGLE({ id, working }));
+            },
+            errorFn: (id, message) => {
+                store.dispatch(snackbarActions.OPEN_SNACKBAR({ message }));
+            }
         });
         const store = configureStore(history, services);
 
similarity index 53%
rename from src/services/api/api-progress.ts
rename to src/services/api/api-actions.ts
index 14dc584ec5ff77021ab97c366aee3689c9685f04..d47eeaa878612a4f3d6fa20b269b365ee6286343 100644 (file)
@@ -3,3 +3,9 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 export type ProgressFn = (id: string, working: boolean) => void;
+export type ErrorFn = (id: string, message: string) => void;
+
+export interface ApiActions {
+    progressFn: ProgressFn;
+    errorFn: ErrorFn;
+}
index 89545c1f2166e942276475cbe659b40b9e259c66..50760bb4d8493b5384b1564ca6e936c00001b40e 100644 (file)
@@ -4,7 +4,7 @@
 
 import { User } from "~/models/user";
 import { AxiosInstance } from "axios";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions, ProgressFn } from "~/services/api/api-actions";
 import * as uuid from "uuid/v4";
 
 export const API_TOKEN_KEY = 'apiToken';
@@ -28,7 +28,7 @@ export class AuthService {
     constructor(
         protected apiClient: AxiosInstance,
         protected baseUrl: string,
-        protected progressFn: ProgressFn) { }
+        protected actions: ApiActions) { }
 
     public saveApiToken(token: string) {
         localStorage.setItem(API_TOKEN_KEY, token);
@@ -90,11 +90,11 @@ export class AuthService {
 
     public getUserDetails = (): Promise<User> => {
         const reqId = uuid();
-        this.progressFn(reqId, true);
+        this.actions.progressFn(reqId, true);
         return this.apiClient
             .get<UserDetailsResponse>('/users/current')
             .then(resp => {
-                this.progressFn(reqId, false);
+                this.actions.progressFn(reqId, false);
                 return {
                     email: resp.data.email,
                     firstName: resp.data.first_name,
@@ -104,7 +104,8 @@ export class AuthService {
                 };
             })
             .catch(e => {
-                this.progressFn(reqId, false);
+                this.actions.progressFn(reqId, false);
+                this.actions.errorFn(reqId, e);
                 throw e;
             });
     }
index 6a60ebf33d02b3857367eb0d4d87268ccb5deacd..28de14f51595fcd067bacdd0a08c940dac8a6726 100644 (file)
@@ -11,13 +11,13 @@ import { mapTreeValues } from "~/models/tree";
 import { parseFilesResponse } from "./collection-service-files-response";
 import { fileToArrayBuffer } from "~/common/file";
 import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
 
 export class CollectionService extends TrashableResourceService<CollectionResource> {
-    constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, progressFn: ProgressFn) {
-        super(serverApi, "collections", progressFn);
+    constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
+        super(serverApi, "collections", actions);
     }
 
     async files(uuid: string) {
index 385485dbca623a18ce0ee06898eba9dec39a7489..5a3bae25fdf005d71245ef821b6cea7693d03a8d 100644 (file)
@@ -6,13 +6,18 @@ import { CommonResourceService } from "./common-resource-service";
 import axios, { AxiosInstance } from "axios";
 import MockAdapter from "axios-mock-adapter";
 import { Resource } from "src/models/resource";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
+
+const actions: ApiActions = {
+    progressFn: (id: string, working: boolean) => {},
+    errorFn: (id: string, message: string) => {}
+};
 
 export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(
-    Service: new (client: AxiosInstance, progressFn: ProgressFn) => C) => {
+    Service: new (client: AxiosInstance, actions: ApiActions) => C) => {
     const axiosInstance = axios.create();
     const axiosMock = new MockAdapter(axiosInstance);
-    const service = new Service(axiosInstance, (id, working) => {});
+    const service = new Service(axiosInstance, actions);
     Object.keys(service).map(key => service[key] = jest.fn());
     return service;
 };
@@ -20,7 +25,6 @@ export const mockResourceService = <R extends Resource, C extends CommonResource
 describe("CommonResourceService", () => {
     const axiosInstance = axios.create();
     const axiosMock = new MockAdapter(axiosInstance);
-    const progressFn = (id: string, working: boolean) => {};
 
     beforeEach(() => {
         axiosMock.reset();
@@ -31,14 +35,14 @@ describe("CommonResourceService", () => {
             .onPost("/resource/")
             .reply(200, { owner_uuid: "ownerUuidValue" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", progressFn);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         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", progressFn);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
         expect(axiosInstance.post).toHaveBeenCalledWith("/resource/", {owner_uuid: "ownerUuidValue"});
     });
@@ -48,7 +52,7 @@ describe("CommonResourceService", () => {
             .onDelete("/resource/uuid")
             .reply(200, { deleted_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", progressFn);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.delete("uuid");
         expect(resource).toEqual({ deletedAt: "now" });
     });
@@ -58,7 +62,7 @@ describe("CommonResourceService", () => {
             .onGet("/resource/uuid")
             .reply(200, { modified_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", progressFn);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.get("uuid");
         expect(resource).toEqual({ modifiedAt: "now" });
     });
@@ -76,7 +80,7 @@ describe("CommonResourceService", () => {
                 items_available: 20
             });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource", progressFn);
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.list({ limit: 10, offset: 1 });
         expect(resource).toEqual({
             kind: "kind",
index 0ad6fbce1f3df525f4fd139af33e4a9c91c75fd6..cfae106b2dd5ed38746ca5ea1f5078990d77992f 100644 (file)
@@ -6,7 +6,7 @@ import * as _ from "lodash";
 import { AxiosInstance, AxiosPromise } from "axios";
 import { Resource } from "src/models/resource";
 import * as uuid from "uuid/v4";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export interface ListArguments {
     limit?: number;
@@ -62,36 +62,37 @@ export class CommonResourceService<T extends Resource> {
             }
         }
 
-    static defaultResponse<R>(promise: AxiosPromise<R>, progressFn: ProgressFn): Promise<R> {
+    static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions): Promise<R> {
         const reqId = uuid();
-        progressFn(reqId, true);
+        actions.progressFn(reqId, true);
         return promise
             .then(data => {
-                progressFn(reqId, false);
+                actions.progressFn(reqId, false);
                 return data;
             })
             .then(CommonResourceService.mapResponseKeys)
             .catch(({ response }) => {
-                progressFn(reqId, false);
+                actions.progressFn(reqId, false);
+                actions.errorFn(reqId, response.message);
                 Promise.reject<Errors>(CommonResourceService.mapResponseKeys(response));
             });
     }
 
     protected serverApi: AxiosInstance;
     protected resourceType: string;
-    protected progressFn: ProgressFn;
+    protected actions: ApiActions;
 
-    constructor(serverApi: AxiosInstance, resourceType: string, onProgress: ProgressFn) {
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
         this.serverApi = serverApi;
         this.resourceType = '/' + resourceType + '/';
-        this.progressFn = onProgress;
+        this.actions = actions;
     }
 
     create(data?: Partial<T> | any) {
         return CommonResourceService.defaultResponse(
             this.serverApi
                 .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
-            this.progressFn
+            this.actions
         );
     }
 
@@ -99,7 +100,7 @@ export class CommonResourceService<T extends Resource> {
         return CommonResourceService.defaultResponse(
             this.serverApi
                 .delete(this.resourceType + uuid),
-            this.progressFn
+            this.actions
         );
     }
 
@@ -107,7 +108,7 @@ export class CommonResourceService<T extends Resource> {
         return CommonResourceService.defaultResponse(
             this.serverApi
                 .get<T>(this.resourceType + uuid),
-            this.progressFn
+            this.actions
         );
     }
 
@@ -123,7 +124,7 @@ export class CommonResourceService<T extends Resource> {
                 .get(this.resourceType, {
                     params: CommonResourceService.mapKeys(_.snakeCase)(params)
                 }),
-            this.progressFn
+            this.actions
         );
     }
 
@@ -131,7 +132,7 @@ export class CommonResourceService<T extends Resource> {
         return CommonResourceService.defaultResponse(
             this.serverApi
                 .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
-            this.progressFn
+            this.actions
         );
     }
 }
index 92e02734806a5b2716453d3a6905ba9de5b1be4d..633b2fbd89cdf09041e4c93ee001916599763d62 100644 (file)
@@ -6,19 +6,19 @@ import * as _ from "lodash";
 import { AxiosInstance } from "axios";
 import { TrashableResource } from "src/models/resource";
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
 
-    constructor(serverApi: AxiosInstance, resourceType: string, progressFn: ProgressFn) {
-        super(serverApi, resourceType, progressFn);
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
+        super(serverApi, resourceType, actions);
     }
 
     trash(uuid: string): Promise<T> {
         return CommonResourceService.defaultResponse(
             this.serverApi
                 .post(this.resourceType + `${uuid}/trash`),
-            this.progressFn
+            this.actions
         );
     }
 
@@ -31,7 +31,7 @@ export class TrashableResourceService<T extends TrashableResource> extends Commo
                 .post(this.resourceType + `${uuid}/untrash`, {
                     params: CommonResourceService.mapKeys(_.snakeCase)(params)
                 }),
-            this.progressFn
+            this.actions
         );
     }
 }
index 6ee44d25e7c2b300d1f6be9b1d43adb0f623c619..e035ed5328fbecfef416212daa1183cb5d51b748 100644 (file)
@@ -5,10 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
 import { ContainerRequestResource } from '~/models/container-request';
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class ContainerRequestService extends CommonResourceService<ContainerRequestResource> {
-    constructor(serverApi: AxiosInstance, progressFn: ProgressFn) {
-        super(serverApi, "container_requests", progressFn);
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "container_requests", actions);
     }
 }
index 2f5b71276fd45ef96f561f232eb02a6a2a51cebb..86b3d2dc8ca97c1a87bf6d0f14ef94235582569a 100644 (file)
@@ -5,10 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
 import { ContainerResource } from '~/models/container';
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class ContainerService extends CommonResourceService<ContainerResource> {
-    constructor(serverApi: AxiosInstance, progressFn: ProgressFn) {
-        super(serverApi, "containers", progressFn);
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "containers", actions);
     }
 }
index d88b3c50683ea878ed96cc3494ba4f67b420d6fe..95355440e8e067a212d47a262b8a9c329f877b2d 100644 (file)
@@ -5,11 +5,17 @@
 import axios from "axios";
 import MockAdapter from "axios-mock-adapter";
 import { GroupsService } from "./groups-service";
+import { ApiActions } from "~/services/api/api-actions";
 
 describe("GroupsService", () => {
 
     const axiosMock = new MockAdapter(axios);
 
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
+
     beforeEach(() => {
         axiosMock.reset();
     });
@@ -27,7 +33,7 @@ describe("GroupsService", () => {
                 items_available: 20
             });
 
-        const groupsService = new GroupsService(axios, (id, working) => {});
+        const groupsService = new GroupsService(axios, actions);
         const resource = await groupsService.contents("1", { limit: 10, offset: 1 });
         expect(resource).toEqual({
             kind: "kind",
index fe337ef14eee9049cb26acf29e617ea1fa5ecd88..c2b559b78b164b1139aa0df920c255c6991b584a 100644 (file)
@@ -10,7 +10,7 @@ import { ProjectResource } from "~/models/project";
 import { ProcessResource } from "~/models/process";
 import { TrashableResource } from "~/models/resource";
 import { TrashableResourceService } from "~/services/common-service/trashable-resource-service";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export interface ContentsArguments {
     limit?: number;
@@ -28,8 +28,8 @@ export type GroupContentsResource =
 
 export class GroupsService<T extends TrashableResource = TrashableResource> extends TrashableResourceService<T> {
 
-    constructor(serverApi: AxiosInstance, progressFn: ProgressFn) {
-        super(serverApi, "groups", progressFn);
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "groups", actions);
     }
 
     contents(uuid: string, args: ContentsArguments = {}): Promise<ListResults<GroupContentsResource>> {
@@ -44,7 +44,7 @@ export class GroupsService<T extends TrashableResource = TrashableResource> exte
                 .get(this.resourceType + `${uuid}/contents`, {
                     params: CommonResourceService.mapKeys(_.snakeCase)(params)
                 }),
-            this.progressFn
+            this.actions
         );
     }
 }
index f28629f1bbe8a8c1e501f65bc0c1fe9286aa86ec..17ee522e4dea00d7c6ecea813a3111a43f64fb4a 100644 (file)
@@ -5,10 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";\r
 import { AxiosInstance } from "axios";\r
 import { KeepResource } from "~/models/keep";\r
-import { ProgressFn } from "~/services/api/api-progress";\r
+import { ApiActions } from "~/services/api/api-actions";\r
 \r
 export class KeepService extends CommonResourceService<KeepResource> {\r
-    constructor(serverApi: AxiosInstance, progressFn: ProgressFn) {\r
-        super(serverApi, "keep_services", progressFn);\r
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {\r
+        super(serverApi, "keep_services", actions);\r
     }\r
 }\r
index 67c1a870ec5b7b95440a41b6fafef7a384ea7a42..2701279e7c5cee5dfabc6f8bf2d52babea51fc06 100644 (file)
@@ -5,10 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { LinkResource } from "~/models/link";
 import { AxiosInstance } from "axios";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class LinkService extends CommonResourceService<LinkResource> {
-    constructor(serverApi: AxiosInstance, progressFn: ProgressFn) {
-        super(serverApi, "links", progressFn);
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "links", actions);
     }
 }
index 7f78d95834d496f5ad7c82eadee94050c5fcad67..3a049a60b8b48f341b0c459961b8429effb7f0f0 100644 (file)
@@ -5,10 +5,10 @@
 import { AxiosInstance } from "axios";
 import { LogResource } from '~/models/log';
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class LogService extends CommonResourceService<LogResource> {
-    constructor(serverApi: AxiosInstance, progressFn: ProgressFn) {
-        super(serverApi, "logs", progressFn);
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "logs", actions);
     }
 }
index 5647ded8beb60b25a4c86835867b6456344b9401..9052360627c6f68ff0d95253efa6b0f4ae88bc55 100644 (file)
@@ -5,13 +5,18 @@
 import axios from "axios";
 import { ProjectService } from "./project-service";
 import { FilterBuilder } from "~/services/api/filter-builder";
+import { ApiActions } from "~/services/api/api-actions";
 
 describe("CommonResourceService", () => {
     const axiosInstance = axios.create();
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     it(`#create has groupClass set to "project"`, async () => {
         axiosInstance.post = jest.fn(() => Promise.resolve({ data: {} }));
-        const projectService = new ProjectService(axiosInstance, (id, working) => {});
+        const projectService = new ProjectService(axiosInstance, actions);
         const resource = await projectService.create({ name: "nameValue" });
         expect(axiosInstance.post).toHaveBeenCalledWith("/groups/", {
             name: "nameValue",
@@ -21,7 +26,7 @@ describe("CommonResourceService", () => {
 
     it("#list has groupClass filter set by default", async () => {
         axiosInstance.get = jest.fn(() => Promise.resolve({ data: {} }));
-        const projectService = new ProjectService(axiosInstance, (id, working) => {});
+        const projectService = new ProjectService(axiosInstance, actions);
         const resource = await projectService.list();
         expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
             params: {
index bd73c745d8e30a0a9747c8948c2696a1193f170f..9c764b0910a2b2338347951b56f427cfcfe9cfa3 100644 (file)
@@ -20,28 +20,29 @@ import { ResourceKind } from "~/models/resource";
 import { ContainerRequestService } from './container-request-service/container-request-service';
 import { ContainerService } from './container-service/container-service';
 import { LogService } from './log-service/log-service';
+import { ApiActions } from "~/services/api/api-actions";
 
 export type ServiceRepository = ReturnType<typeof createServices>;
 
-export const createServices = (config: Config, progressFn: (id: string, working: boolean) => void) => {
+export const createServices = (config: Config, actions: ApiActions) => {
     const apiClient = Axios.create();
     apiClient.defaults.baseURL = config.baseUrl;
 
     const webdavClient = new WebDAV();
     webdavClient.defaults.baseURL = config.keepWebServiceUrl;
 
-    const containerRequestService = new ContainerRequestService(apiClient, progressFn);
-    const containerService = new ContainerService(apiClient, progressFn);
-    const groupsService = new GroupsService(apiClient, progressFn);
-    const keepService = new KeepService(apiClient, progressFn);
-    const linkService = new LinkService(apiClient, progressFn);
-    const logService = new LogService(apiClient, progressFn);
-    const projectService = new ProjectService(apiClient, progressFn);
-    const userService = new UserService(apiClient, progressFn);
+    const containerRequestService = new ContainerRequestService(apiClient, actions);
+    const containerService = new ContainerService(apiClient, actions);
+    const groupsService = new GroupsService(apiClient, actions);
+    const keepService = new KeepService(apiClient, actions);
+    const linkService = new LinkService(apiClient, actions);
+    const logService = new LogService(apiClient, actions);
+    const projectService = new ProjectService(apiClient, actions);
+    const userService = new UserService(apiClient, actions);
 
     const ancestorsService = new AncestorService(groupsService, userService);
-    const authService = new AuthService(apiClient, config.rootUrl, progressFn);
-    const collectionService = new CollectionService(apiClient, webdavClient, authService, progressFn);
+    const authService = new AuthService(apiClient, config.rootUrl, actions);
+    const collectionService = new CollectionService(apiClient, webdavClient, authService, actions);
     const collectionFilesService = new CollectionFilesService(collectionService);
     const favoriteService = new FavoriteService(linkService, groupsService);
     const tagService = new TagService(linkService);
index cd8b6a47c0bf8ca6e5cabfb685dd9b473f8f512d..a69203dc5bece0c4c3e1f29aceba92c3de998849 100644 (file)
@@ -5,10 +5,10 @@
 import { AxiosInstance } from "axios";
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { UserResource } from "~/models/user";
-import { ProgressFn } from "~/services/api/api-progress";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class UserService extends CommonResourceService<UserResource> {
-    constructor(serverApi: AxiosInstance, progressFn: ProgressFn) {
-        super(serverApi, "users", progressFn);
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "users", actions);
     }
 }
index 46d28354e99a84adc0136a722a41ad09fb3b8e5f..a1cd7f4f776776831956deae615b0ae0ed30878c 100644 (file)
@@ -18,16 +18,20 @@ import { createServices } from "~/services/services";
 import { configureStore, RootStore } from "../store";
 import createBrowserHistory from "history/createBrowserHistory";
 import { mockConfig } from '~/common/config';
+import { ApiActions } from "~/services/api/api-actions";
 
 describe('auth-actions', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
     let store: RootStore;
-    const progressFn = (id: string, working: boolean) => {};
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     beforeEach(() => {
-        store = configureStore(createBrowserHistory(), createServices(mockConfig({}), progressFn));
+        store = configureStore(createBrowserHistory(), createServices(mockConfig({}), actions));
         localStorage.clear();
-        reducer = authReducer(createServices(mockConfig({}), progressFn));
+        reducer = authReducer(createServices(mockConfig({}), actions));
     });
 
     it('should initialise state with user and api token from local storage', () => {
index c8e2ccb1e9af4845b28b7bdcb1ab2334340ea20f..1202bacb125b5e1afc61908cb64f9953946b41f6 100644 (file)
@@ -8,14 +8,18 @@ import { AuthAction, authActions } from "./auth-action";
 import 'jest-localstorage-mock';
 import { createServices } from "~/services/services";
 import { mockConfig } from '~/common/config';
+import { ApiActions } from "~/services/api/api-actions";
 
 describe('auth-reducer', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
-    const progressFn = (id: string, working: boolean) => {};
+    const actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     beforeAll(() => {
         localStorage.clear();
-        reducer = authReducer(createServices(mockConfig({}), progressFn));
+        reducer = authReducer(createServices(mockConfig({}), actions));
     });
 
     it('should correctly initialise state', () => {
index 55d9f3a8651b86afecc516aa48dc1af0abc412e1..dd34895f77c1d7bfa92ffae94e385329d1c2fbef 100644 (file)
@@ -4,8 +4,15 @@
 
 import { unionize, ofType, UnionOf } from "~/common/unionize";
 
+export enum SnackbarKind {
+    SUCCESS,
+    ERROR,
+    INFO,
+    WARNING
+}
+
 export const snackbarActions = unionize({
-    OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number}>(),
+    OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number, kind?: SnackbarKind}>(),
     CLOSE_SNACKBAR: ofType<{}>()
 });
 
index fc2f4a1964e27627ff5d02e63150713bbe78eb3b..0595d2822b3f910bf727e6d4aadca59e8fab7810 100644 (file)
@@ -2,12 +2,13 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { SnackbarAction, snackbarActions } from "./snackbar-actions";
+import { SnackbarAction, snackbarActions, SnackbarKind } from "./snackbar-actions";
 
 export interface SnackbarState {
     message: string;
     open: boolean;
     hideDuration: number;
+    kind: SnackbarKind;
 }
 
 const DEFAULT_HIDE_DURATION = 3000;
@@ -15,7 +16,8 @@ const DEFAULT_HIDE_DURATION = 3000;
 const initialState: SnackbarState = {
     message: "",
     open: false,
-    hideDuration: DEFAULT_HIDE_DURATION
+    hideDuration: DEFAULT_HIDE_DURATION,
+    kind: SnackbarKind.INFO
 };
 
 export const snackbarReducer = (state = initialState, action: SnackbarAction) => {
index 535777e1bd4fd0765f1ac88914e592165f969258..341e803cf66cbdcbaf1634d72b9a93f2d9300ef8 100644 (file)
@@ -7,13 +7,24 @@ import { connect } from "react-redux";
 import { RootState } from "~/store/store";
 import MaterialSnackbar, { SnackbarProps } from "@material-ui/core/Snackbar";
 import { Dispatch } from "redux";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
+import IconButton from '@material-ui/core/IconButton';
+import SnackbarContent from '@material-ui/core/SnackbarContent';
+import WarningIcon from '@material-ui/icons/Warning';
+import CheckCircleIcon from '@material-ui/icons/CheckCircle';
+import ErrorIcon from '@material-ui/icons/Error';
+import InfoIcon from '@material-ui/icons/Info';
+import CloseIcon from '@material-ui/icons/Close';
+import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from "~/common/custom-theme";
+import { amber, green } from "@material-ui/core/colors";
+import * as classNames from 'classnames';
 
 const mapStateToProps = (state: RootState): SnackbarProps => ({
-    anchorOrigin: { vertical: "bottom", horizontal: "center" },
+    anchorOrigin: { vertical: "bottom", horizontal: "right" },
     open: state.snackbar.open,
     message: <span>{state.snackbar.message}</span>,
-    autoHideDuration: state.snackbar.hideDuration
+    autoHideDuration: state.snackbar.hideDuration,
 });
 
 const mapDispatchToProps = (dispatch: Dispatch): Pick<SnackbarProps, "onClose"> => ({
@@ -24,4 +35,93 @@ const mapDispatchToProps = (dispatch: Dispatch): Pick<SnackbarProps, "onClose">
     }
 });
 
-export const Snackbar = connect(mapStateToProps, mapDispatchToProps)(MaterialSnackbar);
+const ArvadosSnackbar = (props: any) => <MaterialSnackbar {...props}>
+    <ArvadosSnackbarContent {...props}/>
+</MaterialSnackbar>;
+
+type CssRules = "success" | "error" | "info" | "warning" | "icon" | "iconVariant" | "message";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    success: {
+        backgroundColor: green[600],
+    },
+    error: {
+        backgroundColor: theme.palette.error.dark,
+    },
+    info: {
+        backgroundColor: theme.palette.primary.dark,
+    },
+    warning: {
+        backgroundColor: amber[700],
+    },
+    icon: {
+        fontSize: 20,
+    },
+    iconVariant: {
+        opacity: 0.9,
+        marginRight: theme.spacing.unit,
+    },
+    message: {
+        display: 'flex',
+        alignItems: 'center',
+    },
+});
+
+interface ArvadosSnackbarProps {
+    kind: SnackbarKind;
+}
+
+const ArvadosSnackbarContent = (props: SnackbarProps & ArvadosSnackbarProps & WithStyles<CssRules>) => {
+    const { classes, className, message, onClose, kind, ...other } = props;
+
+    let Icon = InfoIcon;
+    let cssClass;
+    switch (kind) {
+        case SnackbarKind.INFO:
+            Icon = InfoIcon;
+            cssClass = classes.info;
+            break;
+        case SnackbarKind.WARNING:
+            Icon = WarningIcon;
+            cssClass = classes.warning;
+            break;
+        case SnackbarKind.SUCCESS:
+            Icon = CheckCircleIcon;
+            cssClass = classes.success;
+            break;
+        case SnackbarKind.ERROR:
+            Icon = ErrorIcon;
+            cssClass = classes.error;
+            break;
+    }
+
+    return (
+        <SnackbarContent
+            className={classNames(cssClass, className)}
+            aria-describedby="client-snackbar"
+            message={
+                <span id="client-snackbar" className={classes.message}>
+                    <Icon className={classNames(classes.icon, classes.iconVariant)}/>
+                    {message}
+                </span>
+            }
+            action={
+                <IconButton
+                    key="close"
+                    aria-label="Close"
+                    color="inherit"
+                    onClick={e => {
+                        if (onClose) {
+                            onClose(e, '');
+                        }
+                    }}>
+                    <CloseIcon className={classes.icon}/>
+                </IconButton>
+            }
+        />
+    );
+};
+
+export const Snackbar = connect(mapStateToProps, mapDispatchToProps)(
+    withStyles(styles)(ArvadosSnackbar)
+);