refs #14186 Merge branch 'origin/14186-progress-indicator-store'
authorDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 18 Sep 2018 11:55:53 +0000 (13:55 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Tue, 18 Sep 2018 11:56:16 +0000 (13:56 +0200)
Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos@contractors.roche.com>

61 files changed:
package.json
src/common/custom-theme.ts
src/components/data-explorer/data-explorer.tsx
src/components/data-table/data-table.tsx
src/index.tsx
src/services/api/api-actions.ts [new file with mode: 0644]
src/services/api/url-builder.test.ts [new file with mode: 0644]
src/services/api/url-builder.ts
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/favorite-service/favorite-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/collections/collection-copy-actions.ts
src/store/collections/collection-create-actions.ts
src/store/collections/collection-move-actions.ts
src/store/collections/collection-partial-copy-actions.ts
src/store/collections/collection-update-actions.ts
src/store/collections/collection-upload-actions.ts
src/store/data-explorer/data-explorer-reducer.ts
src/store/favorite-panel/favorite-panel-middleware-service.ts
src/store/favorites/favorites-actions.ts
src/store/processes/process.ts
src/store/progress-indicator/progress-indicator-actions.ts [new file with mode: 0644]
src/store/progress-indicator/progress-indicator-reducer.ts [new file with mode: 0644]
src/store/progress-indicator/with-progress.ts [new file with mode: 0644]
src/store/project-panel/project-panel-middleware-service.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/snackbar/snackbar-actions.ts
src/store/snackbar/snackbar-reducer.ts
src/store/store.ts
src/store/trash-panel/trash-panel-middleware-service.ts
src/store/trash/trash-actions.ts
src/views-components/data-explorer/data-explorer.tsx
src/views-components/main-app-bar/main-app-bar.tsx
src/views-components/progress/content-progress.tsx [new file with mode: 0644]
src/views-components/progress/side-panel-progress.tsx [new file with mode: 0644]
src/views-components/progress/workbench-progress.tsx [new file with mode: 0644]
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views-components/side-panel/side-panel.tsx
src/views-components/snackbar/snackbar.tsx
src/views/favorite-panel/favorite-panel.tsx
src/views/process-panel/subprocesses-card.tsx
src/views/project-panel/project-panel.tsx
src/views/trash-panel/trash-panel.tsx
src/views/workbench/workbench.tsx
tslint.json
yarn.lock

index 623e11707f1c79bb07a5947c7a1cb43427007655..620ff5a6d03a6d7d616ab9d32b0ec2f47c0530ac 100644 (file)
@@ -25,7 +25,8 @@
     "react-transition-group": "2.4.0",
     "redux": "4.0.0",
     "redux-thunk": "2.3.0",
-    "unionize": "2.1.2"
+    "unionize": "2.1.2",
+    "uuid": "3.3.2"
   },
   "scripts": {
     "start": "react-scripts-ts start",
@@ -48,6 +49,7 @@
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
     "@types/redux-form": "7.4.5",
+    "@types/uuid": "3.4.4",
     "axios-mock-adapter": "1.15.0",
     "enzyme": "3.4.4",
     "enzyme-adapter-react-16": "1.2.0",
index 3d56f78b5c80c8adf285526e9dd1439fbe3937c9..ff0eb5e34ce7449f672464485b5879cc98f5d382 100644 (file)
@@ -27,8 +27,6 @@ interface Colors {
     yellow700: string;
     red900: string;
     blue500: string;
-    grey500: string;
-    grey700: string;
 }
 
 const arvadosPurple = '#361336';
@@ -46,8 +44,6 @@ export const themeOptions: ArvadosThemeOptions = {
             yellow700: yellow["700"],
             red900: red['900'],
             blue500: blue['500'],
-            grey500,
-            grey700
         }
     },
     overrides: {
index d63d1ccce519b69b825594ec6a05d4f7f5339397..59f4dbebb47832ff4ec5a827c44eb1a434db61ac 100644 (file)
@@ -33,6 +33,7 @@ interface DataExplorerDataProps<T> {
     page: number;
     contextMenuColumn: boolean;
     dataTableDefaultView?: React.ReactNode;
+    working?: boolean;
 }
 
 interface DataExplorerActionProps<T> {
@@ -60,7 +61,7 @@ export const DataExplorer = withStyles(styles)(
         }
         render() {
             const {
-                columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
+                columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
                 dataTableDefaultView
@@ -87,6 +88,7 @@ export const DataExplorer = withStyles(styles)(
                     onFiltersChange={onFiltersChange}
                     onSortToggle={onSortToggle}
                     extractKey={extractKey}
+                    working={working}
                     defaultView={dataTableDefaultView}
                 />
                 <Toolbar>
index 65531a5b5543d1e95a0fb49f22514b96d53f49d8..25d81c62fa7dc89a8135c8a718c5dc02800d22a6 100644 (file)
@@ -19,6 +19,7 @@ export interface DataTableDataProps<T> {
     onSortToggle: (column: DataColumn<T>) => void;
     onFiltersChange: (filters: DataTableFilterItem[], column: DataColumn<T>) => void;
     extractKey?: (item: T) => React.Key;
+    working?: boolean;
     defaultView?: React.ReactNode;
 }
 
@@ -63,7 +64,7 @@ export const DataTable = withStyles(styles)(
                             {items.map(this.renderBodyRow)}
                         </TableBody>
                     </Table>
-                    {items.length === 0 && this.renderNoItemsPlaceholder()}
+                    {items.length === 0 && this.props.working !== undefined && !this.props.working && this.renderNoItemsPlaceholder()}
                 </div>
             </div>;
         }
@@ -71,7 +72,7 @@ export const DataTable = withStyles(styles)(
         renderNoItemsPlaceholder = () => {
             return this.props.defaultView
                 ? this.props.defaultView
-                : <DataTableDefaultView />;
+                : <DataTableDefaultView/>;
         }
 
         renderHeadCell = (column: DataColumn<T>, index: number) => {
index a76a86ac7d70213a6dad83a030f3f0e523164ac0..0d026f2389f2713372607cc718e681758e769725 100644 (file)
@@ -37,6 +37,8 @@ import { Config } from '~/common/config';
 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, SnackbarKind } 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);
@@ -61,7 +63,15 @@ addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
 fetchConfig()
     .then(({ config, apiHost }) => {
         const history = createBrowserHistory();
-        const services = createServices(config);
+        const services = createServices(config, {
+            progressFn: (id, working) => {
+                store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
+            },
+            errorFn: (id, error) => {
+                console.error("Backend error:", error);
+                store.dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Backend error", kind: SnackbarKind.ERROR }));
+            }
+        });
         const store = configureStore(history, services);
 
         store.subscribe(initListener(history, store, services, config));
@@ -87,8 +97,6 @@ fetchConfig()
             <App />,
             document.getElementById('root') as HTMLElement
         );
-
-
     });
 
 const initListener = (history: History, store: RootStore, services: ServiceRepository, config: Config) => {
diff --git a/src/services/api/api-actions.ts b/src/services/api/api-actions.ts
new file mode 100644 (file)
index 0000000..f986786
--- /dev/null
@@ -0,0 +1,11 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type ProgressFn = (id: string, working: boolean) => void;
+export type ErrorFn = (id: string, error: any) => void;
+
+export interface ApiActions {
+    progressFn: ProgressFn;
+    errorFn: ErrorFn;
+}
diff --git a/src/services/api/url-builder.test.ts b/src/services/api/url-builder.test.ts
new file mode 100644 (file)
index 0000000..2b48940
--- /dev/null
@@ -0,0 +1,36 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { OrderBuilder } from "./order-builder";
+import { joinUrls } from "~/services/api/url-builder";
+
+describe("UrlBuilder", () => {
+    it("should join urls properly 1", () => {
+        expect(joinUrls('http://localhost:3000', '/main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 2", () => {
+        expect(joinUrls('http://localhost:3000/', '/main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 3", () => {
+        expect(joinUrls('http://localhost:3000//', '/main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 4", () => {
+        expect(joinUrls('http://localhost:3000', '//main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 5", () => {
+        expect(joinUrls('http://localhost:3000///', 'main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 6", () => {
+        expect(joinUrls('http://localhost:3000///', '//main')).toEqual('http://localhost:3000/main');
+    });
+    it("should join urls properly 7", () => {
+        expect(joinUrls(undefined, '//main')).toEqual('/main');
+    });
+    it("should join urls properly 8", () => {
+        expect(joinUrls(undefined, 'main')).toEqual('/main');
+    });
+    it("should join urls properly 9", () => {
+        expect(joinUrls('http://localhost:3000///', undefined)).toEqual('http://localhost:3000');
+    });
+});
index 0587c837371dbe0ef242885f0bce6a4a5c2e9c4b..32039a50c23f2a12e1c2c7fdbfe690c4cceecee6 100644 (file)
@@ -24,3 +24,24 @@ export class UrlBuilder {
         return this.url + this.query;
     }
 }
+
+export function joinUrls(url0?: string, url1?: string) {
+    let u0 = "";
+    if (url0) {
+        let idx0 = url0.length - 1;
+        while (url0[idx0] === '/') { --idx0; }
+        u0 = url0.substr(0, idx0 + 1);
+    }
+    let u1 = "";
+    if (url1) {
+        let idx1 = 0;
+        while (url1[idx1] === '/') { ++idx1; }
+        u1 = url1.substr(idx1);
+    }
+    let url = u0;
+    if (u1.length > 0) {
+        url += '/';
+    }
+    url += u1;
+    return url;
+}
index 57915f70578f04be4afd19ef8d6de2543b1cdf3b..50760bb4d8493b5384b1564ca6e936c00001b40e 100644 (file)
@@ -4,6 +4,8 @@
 
 import { User } from "~/models/user";
 import { AxiosInstance } from "axios";
+import { ApiActions, ProgressFn } from "~/services/api/api-actions";
+import * as uuid from "uuid/v4";
 
 export const API_TOKEN_KEY = 'apiToken';
 export const USER_EMAIL_KEY = 'userEmail';
@@ -25,7 +27,8 @@ export class AuthService {
 
     constructor(
         protected apiClient: AxiosInstance,
-        protected baseUrl: string) { }
+        protected baseUrl: string,
+        protected actions: ApiActions) { }
 
     public saveApiToken(token: string) {
         localStorage.setItem(API_TOKEN_KEY, token);
@@ -86,15 +89,25 @@ export class AuthService {
     }
 
     public getUserDetails = (): Promise<User> => {
+        const reqId = uuid();
+        this.actions.progressFn(reqId, true);
         return this.apiClient
             .get<UserDetailsResponse>('/users/current')
-            .then(resp => ({
-                email: resp.data.email,
-                firstName: resp.data.first_name,
-                lastName: resp.data.last_name,
-                uuid: resp.data.uuid,
-                ownerUuid: resp.data.owner_uuid
-            }));
+            .then(resp => {
+                this.actions.progressFn(reqId, false);
+                return {
+                    email: resp.data.email,
+                    firstName: resp.data.first_name,
+                    lastName: resp.data.last_name,
+                    uuid: resp.data.uuid,
+                    ownerUuid: resp.data.owner_uuid
+                };
+            })
+            .catch(e => {
+                this.actions.progressFn(reqId, false);
+                this.actions.errorFn(reqId, e);
+                throw e;
+            });
     }
 
     public getRootUuid() {
index 6e6f2a97d439748a3fb96d9741cff45aa965e073..28de14f51595fcd067bacdd0a08c940dac8a6726 100644 (file)
@@ -11,12 +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 { 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) {
-        super(serverApi, "collections");
+    constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
+        super(serverApi, "collections", actions);
     }
 
     async files(uuid: string) {
index d67d5dbf403ab66ea59c6267dce0d0fa90dacd9c..5a3bae25fdf005d71245ef821b6cea7693d03a8d 100644 (file)
@@ -6,11 +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 { ApiActions } from "~/services/api/api-actions";
 
-export const mockResourceService = <R extends Resource, C extends CommonResourceService<R>>(Service: new (client: AxiosInstance) => C) => {
+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, actions: ApiActions) => C) => {
     const axiosInstance = axios.create();
     const axiosMock = new MockAdapter(axiosInstance);
-    const service = new Service(axiosInstance);
+    const service = new Service(axiosInstance, actions);
     Object.keys(service).map(key => service[key] = jest.fn());
     return service;
 };
@@ -28,14 +35,14 @@ describe("CommonResourceService", () => {
             .onPost("/resource/")
             .reply(200, { owner_uuid: "ownerUuidValue" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        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");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         await commonResourceService.create({ ownerUuid: "ownerUuidValue" });
         expect(axiosInstance.post).toHaveBeenCalledWith("/resource/", {owner_uuid: "ownerUuidValue"});
     });
@@ -45,7 +52,7 @@ describe("CommonResourceService", () => {
             .onDelete("/resource/uuid")
             .reply(200, { deleted_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.delete("uuid");
         expect(resource).toEqual({ deletedAt: "now" });
     });
@@ -55,7 +62,7 @@ describe("CommonResourceService", () => {
             .onGet("/resource/uuid")
             .reply(200, { modified_at: "now" });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.get("uuid");
         expect(resource).toEqual({ modifiedAt: "now" });
     });
@@ -73,7 +80,7 @@ describe("CommonResourceService", () => {
                 items_available: 20
             });
 
-        const commonResourceService = new CommonResourceService(axiosInstance, "resource");
+        const commonResourceService = new CommonResourceService(axiosInstance, "resource", actions);
         const resource = await commonResourceService.list({ limit: 10, offset: 1 });
         expect(resource).toEqual({
             kind: "kind",
index 09e034f5f8b6022e762c902b5d7d4e0cb99411a4..f6810c0453b183a1db0847fe127a8dd607004d41 100644 (file)
@@ -5,6 +5,8 @@
 import * as _ from "lodash";
 import { AxiosInstance, AxiosPromise } from "axios";
 import { Resource } from "src/models/resource";
+import * as uuid from "uuid/v4";
+import { ApiActions } from "~/services/api/api-actions";
 
 export interface ListArguments {
     limit?: number;
@@ -39,7 +41,7 @@ export enum CommonResourceServiceError {
 
 export class CommonResourceService<T extends Resource> {
 
-    static mapResponseKeys = (response: { data: any }): Promise<any> =>
+    static mapResponseKeys = (response: { data: any }) =>
         CommonResourceService.mapKeys(_.camelCase)(response.data)
 
     static mapKeys = (mapFn: (key: string) => string) =>
@@ -60,36 +62,55 @@ export class CommonResourceService<T extends Resource> {
             }
         }
 
-    static defaultResponse<R>(promise: AxiosPromise<R>): Promise<R> {
+    static defaultResponse<R>(promise: AxiosPromise<R>, actions: ApiActions): Promise<R> {
+        const reqId = uuid();
+        actions.progressFn(reqId, true);
         return promise
+            .then(data => {
+                actions.progressFn(reqId, false);
+                return data;
+            })
             .then(CommonResourceService.mapResponseKeys)
-            .catch(({ response }) => Promise.reject<Errors>(CommonResourceService.mapResponseKeys(response)));
+            .catch(({ response }) => {
+                actions.progressFn(reqId, false);
+                const errors = CommonResourceService.mapResponseKeys(response) as Errors;
+                actions.errorFn(reqId, errors);
+                throw errors;
+            });
     }
 
     protected serverApi: AxiosInstance;
     protected resourceType: string;
+    protected actions: ApiActions;
 
-    constructor(serverApi: AxiosInstance, resourceType: string) {
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
         this.serverApi = serverApi;
         this.resourceType = '/' + resourceType + '/';
+        this.actions = actions;
     }
 
     create(data?: Partial<T> | any) {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
+                .post<T>(this.resourceType, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
+            this.actions
+        );
     }
 
     delete(uuid: string): Promise<T> {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .delete(this.resourceType + uuid));
+                .delete(this.resourceType + uuid),
+            this.actions
+        );
     }
 
     get(uuid: string) {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .get<T>(this.resourceType + uuid));
+                .get<T>(this.resourceType + uuid),
+            this.actions
+        );
     }
 
     list(args: ListArguments = {}): Promise<ListResults<T>> {
@@ -103,14 +124,17 @@ export class CommonResourceService<T extends Resource> {
             this.serverApi
                 .get(this.resourceType, {
                     params: CommonResourceService.mapKeys(_.snakeCase)(params)
-                }));
+                }),
+            this.actions
+        );
     }
 
     update(uuid: string, data: Partial<T>) {
         return CommonResourceService.defaultResponse(
             this.serverApi
-                .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)));
-
+                .put<T>(this.resourceType + uuid, data && CommonResourceService.mapKeys(_.snakeCase)(data)),
+            this.actions
+        );
     }
 }
 
index 23e7366e9f69537aa0905e1a982c02943d3fb8bd..633b2fbd89cdf09041e4c93ee001916599763d62 100644 (file)
@@ -6,27 +6,32 @@ import * as _ from "lodash";
 import { AxiosInstance } from "axios";
 import { TrashableResource } from "src/models/resource";
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
 
-    constructor(serverApi: AxiosInstance, resourceType: string) {
-        super(serverApi, resourceType);
+    constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions) {
+        super(serverApi, resourceType, actions);
     }
 
     trash(uuid: string): Promise<T> {
-        return this.serverApi
-            .post(this.resourceType + `${uuid}/trash`)
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .post(this.resourceType + `${uuid}/trash`),
+            this.actions
+        );
     }
 
     untrash(uuid: string): Promise<T> {
         const params = {
             ensure_unique_name: true
         };
-        return this.serverApi
-            .post(this.resourceType + `${uuid}/untrash`, {
-                params: CommonResourceService.mapKeys(_.snakeCase)(params)
-            })
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .post(this.resourceType + `${uuid}/untrash`, {
+                    params: CommonResourceService.mapKeys(_.snakeCase)(params)
+                }),
+            this.actions
+        );
     }
 }
index 01805ff903ee4396da0ae64dc283045da2119c98..e035ed5328fbecfef416212daa1183cb5d51b748 100644 (file)
@@ -4,10 +4,11 @@
 
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
-import { ContainerRequestResource } from '../../models/container-request';
+import { ContainerRequestResource } from '~/models/container-request';
+import { ApiActions } from "~/services/api/api-actions";
 
 export class ContainerRequestService extends CommonResourceService<ContainerRequestResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "container_requests");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "container_requests", actions);
     }
 }
index 0ace1f60af6a1e72fbf674727493209796a5f360..86b3d2dc8ca97c1a87bf6d0f14ef94235582569a 100644 (file)
@@ -4,10 +4,11 @@
 
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { AxiosInstance } from "axios";
-import { ContainerResource } from '../../models/container';
+import { ContainerResource } from '~/models/container';
+import { ApiActions } from "~/services/api/api-actions";
 
 export class ContainerService extends CommonResourceService<ContainerResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "containers");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "containers", actions);
     }
 }
index 4601054315fab0a196c1fa4de6c12056558e4a57..92b0713dbcc6ee2ce8b0eb0939d4cb928710676b 100644 (file)
@@ -19,7 +19,7 @@ export interface FavoriteListArguments {
 export class FavoriteService {
     constructor(
         private linkService: LinkService,
-        private groupsService: GroupsService
+        private groupsService: GroupsService,
     ) {}
 
     create(data: { userUuid: string; resource: { uuid: string; name: string } }) {
index e1157f4b177e5ca18c9764c9bb249cf1467d7074..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);
+        const groupsService = new GroupsService(axios, actions);
         const resource = await groupsService.contents("1", { limit: 10, offset: 1 });
         expect(resource).toEqual({
             kind: "kind",
index 299e6808546b224cb2e1b39f18bb3aeb73061b40..e705b6e5377541f5eaa245d2cf7afbc0b1c40dcf 100644 (file)
@@ -10,7 +10,8 @@ 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 { GroupResource } from '~/models/group';
+import { ApiActions } from "~/services/api/api-actions";
+import { GroupResource } from "~/models/group";
 
 export interface ContentsArguments {
     limit?: number;
@@ -32,8 +33,8 @@ export type GroupContentsResource =
 
 export class GroupsService<T extends GroupResource = GroupResource> extends TrashableResourceService<T> {
 
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "groups");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "groups", actions);
     }
 
     contents(uuid: string, args: ContentsArguments = {}): Promise<ListResults<GroupContentsResource>> {
@@ -43,17 +44,21 @@ export class GroupsService<T extends GroupResource = GroupResource> extends Tras
             filters: filters ? `[${filters}]` : undefined,
             order: order ? order : undefined
         };
-        return this.serverApi
-            .get(this.resourceType + `${uuid}/contents`, {
-                params: CommonResourceService.mapKeys(_.snakeCase)(params)
-            })
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .get(this.resourceType + `${uuid}/contents`, {
+                    params: CommonResourceService.mapKeys(_.snakeCase)(params)
+                }),
+            this.actions
+        );
     }
 
     shared(params: SharedArguments = {}): Promise<ListResults<GroupContentsResource>> {
-        return this.serverApi
-            .get(this.resourceType + 'shared', { params })
-            .then(CommonResourceService.mapResponseKeys);
+        return CommonResourceService.defaultResponse(
+            this.serverApi
+                .get(this.resourceType + 'shared', { params }),
+            this.actions
+        );
     }
 }
 
index 77d06d933d37ba334e5205a70adde87d127d83d4..17ee522e4dea00d7c6ecea813a3111a43f64fb4a 100644 (file)
@@ -5,9 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";\r
 import { AxiosInstance } from "axios";\r
 import { KeepResource } from "~/models/keep";\r
+import { ApiActions } from "~/services/api/api-actions";\r
 \r
 export class KeepService extends CommonResourceService<KeepResource> {\r
-    constructor(serverApi: AxiosInstance) {\r
-        super(serverApi, "keep_services");\r
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {\r
+        super(serverApi, "keep_services", actions);\r
     }\r
 }\r
index c77def5f8c2eb461b8533ded52b6ae522cdc03e4..2701279e7c5cee5dfabc6f8bf2d52babea51fc06 100644 (file)
@@ -5,9 +5,10 @@
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { LinkResource } from "~/models/link";
 import { AxiosInstance } from "axios";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class LinkService extends CommonResourceService<LinkResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "links");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "links", actions);
     }
 }
index 8f6c66c8a2ddbf24f9e02fb0fe84874036b23f6c..3a049a60b8b48f341b0c459961b8429effb7f0f0 100644 (file)
@@ -5,9 +5,10 @@
 import { AxiosInstance } from "axios";
 import { LogResource } from '~/models/log';
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class LogService extends CommonResourceService<LogResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "logs");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "logs", actions);
     }
 }
index 11c2f61f3d87f5d9f7190726fc22d658d078f73d..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);
+        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);
+        const projectService = new ProjectService(axiosInstance, actions);
         const resource = await projectService.list();
         expect(axiosInstance.get).toHaveBeenCalledWith("/groups/", {
             params: {
index 53721dd301b64399d77775c3ec09b65240a6c564..9c764b0910a2b2338347951b56f427cfcfe9cfa3 100644 (file)
@@ -12,36 +12,37 @@ import { CollectionService } from "./collection-service/collection-service";
 import { TagService } from "./tag-service/tag-service";
 import { CollectionFilesService } from "./collection-files-service/collection-files-service";
 import { KeepService } from "./keep-service/keep-service";
-import { WebDAV } from "../common/webdav";
-import { Config } from "../common/config";
+import { WebDAV } from "~/common/webdav";
+import { Config } from "~/common/config";
 import { UserService } from './user-service/user-service';
 import { AncestorService } from "~/services/ancestors-service/ancestors-service";
 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) => {
+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);
-    const containerService = new ContainerService(apiClient);
-    const groupsService = new GroupsService(apiClient);
-    const keepService = new KeepService(apiClient);
-    const linkService = new LinkService(apiClient);
-    const logService = new LogService(apiClient);
-    const projectService = new ProjectService(apiClient);
-    const userService = new UserService(apiClient);
-    
+    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);
-    const collectionService = new CollectionService(apiClient, webdavClient, authService);
+    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);
@@ -77,4 +78,4 @@ export const getResourceService = (kind?: ResourceKind) => (serviceRepository: S
         default:
             return undefined;
     }
-};
\ No newline at end of file
+};
index 31cc4bbbbce8820b357dcab978a2efc2f8fb381f..a69203dc5bece0c4c3e1f29aceba92c3de998849 100644 (file)
@@ -5,9 +5,10 @@
 import { AxiosInstance } from "axios";
 import { CommonResourceService } from "~/services/common-service/common-resource-service";
 import { UserResource } from "~/models/user";
+import { ApiActions } from "~/services/api/api-actions";
 
 export class UserService extends CommonResourceService<UserResource> {
-    constructor(serverApi: AxiosInstance) {
-        super(serverApi, "users");
+    constructor(serverApi: AxiosInstance, actions: ApiActions) {
+        super(serverApi, "users", actions);
     }
 }
index 4ac48a0be2afa021ab220f553847eccde259871d..a1cd7f4f776776831956deae615b0ae0ed30878c 100644 (file)
@@ -18,15 +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 actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     beforeEach(() => {
-        store = configureStore(createBrowserHistory(), createServices(mockConfig({})));
+        store = configureStore(createBrowserHistory(), createServices(mockConfig({}), actions));
         localStorage.clear();
-        reducer = authReducer(createServices(mockConfig({})));
+        reducer = authReducer(createServices(mockConfig({}), actions));
     });
 
     it('should initialise state with user and api token from local storage', () => {
index 2b1920a61db9fc47e32fb543341a2010ccd59d63..1202bacb125b5e1afc61908cb64f9953946b41f6 100644 (file)
@@ -8,13 +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 actions: ApiActions = {
+        progressFn: (id: string, working: boolean) => {},
+        errorFn: (id: string, message: string) => {}
+    };
 
     beforeAll(() => {
         localStorage.clear();
-        reducer = authReducer(createServices(mockConfig({})));
+        reducer = authReducer(createServices(mockConfig({}), actions));
     });
 
     it('should correctly initialise state', () => {
index 09d4e04e7659aa13a54e137ec7b5571feae980ab..058d2dd457147495920945498fb79d29a40b09c9 100644 (file)
@@ -10,6 +10,7 @@ import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
 import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
 
@@ -25,11 +26,13 @@ export const copyCollection = (resource: CopyFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_COPY_FORM_NAME));
             const collection = await services.collectionService.get(resource.uuid);
             const uuidKey = 'uuid';
             delete collection[uuidKey];
             await services.collectionService.create({ ...collection, ownerUuid: resource.ownerUuid, name: resource.name });
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
             return collection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
@@ -39,6 +42,7 @@ export const copyCollection = (resource: CopyFormDialogData) =>
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
                 throw new Error('Could not copy the collection.');
             }
-            return ;
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
+            return;
         }
     };
index 254d6a8aa6850cb318057f5bbbccd228e397653e..7f21887db6d8bd84d40af48522ddd5d44724d743 100644 (file)
@@ -10,6 +10,7 @@ import { ServiceRepository } from '~/services/services';
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
 import { uploadCollectionFiles } from './collection-upload-actions';
 import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export interface CollectionCreateFormDialogData {
     ownerUuid: string;
@@ -30,16 +31,19 @@ export const createCollection = (data: CollectionCreateFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_CREATE_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_CREATE_FORM_NAME));
             const newCollection = await services.collectionService.create(data);
             await dispatch<any>(uploadCollectionFiles(newCollection.uuid));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_CREATE_FORM_NAME }));
             dispatch(reset(COLLECTION_CREATE_FORM_NAME));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
             return newCollection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_CREATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
             }
-            return ;
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_CREATE_FORM_NAME));
+            return;
         }
     };
index 420bcd01f1d90ed3d505414291d65b8f0bb89193..9bdc5523793a789a2df2b8329abe2b188117be5c 100644 (file)
@@ -8,10 +8,11 @@ import { startSubmit, stopSubmit, initialize } from 'redux-form';
 import { ServiceRepository } from '~/services/services';
 import { RootState } from '~/store/store';
 import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { projectPanelActions } from '~/store/project-panel/project-panel-action';
 import { MoveToFormDialogData } from '~/store/move-to-dialog/move-to-dialog';
 import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree-picker-actions';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
 
@@ -26,11 +27,17 @@ export const moveCollection = (resource: MoveToFormDialogData) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_MOVE_FORM_NAME));
             const collection = await services.collectionService.get(resource.uuid);
             await services.collectionService.update(resource.uuid, { ...collection, ownerUuid: resource.ownerUuid });
             dispatch(projectPanelActions.REQUEST_ITEMS());
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been moved', hideDuration: 2000 }));
+            dispatch(snackbarActions.OPEN_SNACKBAR({
+                message: 'Collection has been moved',
+                hideDuration: 2000,
+                kind: SnackbarKind.SUCCESS
+            }));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
             return collection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
@@ -40,6 +47,7 @@ export const moveCollection = (resource: MoveToFormDialogData) =>
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the collection.', hideDuration: 2000 }));
             }
-            return ;
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
+            return;
         }
     };
index dedf75e16cda4d972720b4a685d2fecefe04d2d1..4dac9c7d7e5ce55d4246c885dcb7a707b54e7957 100644 (file)
@@ -9,8 +9,9 @@ import { resetPickerProjectTree } from '~/store/project-tree-picker/project-tree
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { ServiceRepository } from '~/services/services';
 import { filterCollectionFilesBySelection } from '../collection-panel/collection-panel-files/collection-panel-files-state';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { getCommonResourceServiceError, CommonResourceServiceError } from '~/services/common-service/common-resource-service';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const COLLECTION_PARTIAL_COPY_FORM_NAME = 'COLLECTION_PARTIAL_COPY_DIALOG';
 
@@ -42,6 +43,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
         const currentCollection = state.collectionPanel.item;
         if (currentCollection) {
             try {
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
                 const collection = await services.collectionService.get(currentCollection.uuid);
                 const collectionCopy = {
                     ...collection,
@@ -54,7 +56,12 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
                 const paths = filterCollectionFilesBySelection(state.collectionPanelFiles, false).map(file => file.id);
                 await services.collectionService.deleteFiles(newCollection.uuid, paths);
                 dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'New collection created.', hideDuration: 2000 }));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'New collection created.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
             } catch (e) {
                 const error = getCommonResourceServiceError(e);
                 if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
@@ -66,6 +73,7 @@ export const copyCollectionPartial = ({ name, description, projectUuid }: Collec
                     dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_FORM_NAME }));
                     dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Collection has been copied but may contain incorrect files.', hideDuration: 2000 }));
                 }
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_PARTIAL_COPY_FORM_NAME));
             }
         }
     };
index bf05d4ddc5a3a02075e42c402d22f22090061f1f..9c859234f3a3911e755e9840fa6b554a6e7d83a0 100644 (file)
@@ -11,6 +11,7 @@ import { getCommonResourceServiceError, CommonResourceServiceError } from "~/ser
 import { ServiceRepository } from "~/services/services";
 import { CollectionResource } from '~/models/collection';
 import { ContextMenuResource } from "~/store/context-menu/context-menu-actions";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export interface CollectionUpdateFormDialogData {
     uuid: string;
@@ -31,15 +32,18 @@ export const updateCollection = (collection: Partial<CollectionResource>) =>
         const uuid = collection.uuid || '';
         dispatch(startSubmit(COLLECTION_UPDATE_FORM_NAME));
         try {
+            dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPDATE_FORM_NAME));
             const updatedCollection = await services.collectionService.update(uuid, collection);
             dispatch(collectionPanelActions.LOAD_COLLECTION_SUCCESS({ item: updatedCollection as CollectionResource }));
             dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_UPDATE_FORM_NAME }));
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             return updatedCollection;
         } catch (e) {
             const error = getCommonResourceServiceError(e);
             if (error === CommonResourceServiceError.UNIQUE_VIOLATION) {
                 dispatch(stopSubmit(COLLECTION_UPDATE_FORM_NAME, { name: 'Collection with the same name already exists.' }));
             }
+            dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPDATE_FORM_NAME));
             return;
         }
     };
index 4a5aff35009becbbdb6e018b4735fce4618fb842..ef241a7c33f03e2ddfcac24fe6cc9de67b417eb7 100644 (file)
@@ -7,9 +7,10 @@ import { RootState } from '~/store/store';
 import { ServiceRepository } from '~/services/services';
 import { dialogActions } from '~/store/dialog/dialog-actions';
 import { loadCollectionFiles } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
 import { fileUploaderActions } from '~/store/file-uploader/file-uploader-actions';
 import { reset, startSubmit } from 'redux-form';
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const uploadCollectionFiles = (collectionUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
@@ -31,11 +32,21 @@ export const submitCollectionFiles = () =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const currentCollection = getState().collectionPanel.item;
         if (currentCollection) {
-            dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
-            await dispatch<any>(uploadCollectionFiles(currentCollection.uuid));
-            dispatch<any>(loadCollectionFiles(currentCollection.uuid));
-            dispatch(closeUploadCollectionFilesDialog());
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Data has been uploaded.', hideDuration: 2000 }));
+            try {
+                dispatch(progressIndicatorActions.START_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+                dispatch(startSubmit(COLLECTION_UPLOAD_FILES_DIALOG));
+                await dispatch<any>(uploadCollectionFiles(currentCollection.uuid));
+                dispatch<any>(loadCollectionFiles(currentCollection.uuid));
+                dispatch(closeUploadCollectionFilesDialog());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'Data has been uploaded.',
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+            } catch (e) {
+                dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_UPLOAD_FILES_DIALOG));
+            }
         }
     };
 
@@ -43,4 +54,4 @@ export const closeUploadCollectionFilesDialog = () => dialogActions.CLOSE_DIALOG
 
 const handleUploadProgress = (dispatch: Dispatch) => (fileId: number, loaded: number, total: number, currentTime: number) => {
     dispatch(fileUploaderActions.SET_UPLOAD_PROGRESS({ fileId, loaded, total, currentTime }));
-};
\ No newline at end of file
+};
index cc800244abc1d0d9c1673bd03743d1eff7024f14..d059d37af4639110170f848cf1badf4be411154a 100644 (file)
@@ -15,6 +15,7 @@ export interface DataExplorer {
     rowsPerPage: number;
     rowsPerPageOptions: number[];
     searchValue: string;
+    working?: boolean;
 }
 
 export const initialDataExplorer: DataExplorer = {
index c385309f71f28d3295e23b718089fab3e334b7f2..acdc12b4d3c6b90e112bb5fc1e384f274fbd9f6f 100644 (file)
@@ -16,7 +16,8 @@ import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
 import { LinkResource } from "~/models/link";
 import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
 import { resourcesActions } from "~/store/resources/resources-actions";
-import { snackbarActions } from '~/store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
 import { getDataExplorer } from "~/store/data-explorer/data-explorer-reducer";
 import { loadMissingProcessesInformation } from "~/store/project-panel/project-panel-middleware-service";
 
@@ -30,7 +31,6 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
         if (!dataExplorer) {
             api.dispatch(favoritesPanelDataExplorerIsNotSet());
         } else {
-
             const columns = dataExplorer.columns as DataColumns<string, FavoritePanelFilter>;
             const sortColumn = dataExplorer.columns.find(c => c.sortDirection !== SortDirection.NONE);
             const typeFilters = this.getColumnFilters(columns, FavoritePanelColumnNames.TYPE);
@@ -50,6 +50,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                     .addOrder(direction, "name", GroupContentsResourcePrefix.PROJECT);
             }
             try {
+                api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
                 const response = await this.services.favoriteService
                     .list(this.services.authService.getUuid()!, {
                         limit: dataExplorer.rowsPerPage,
@@ -61,6 +62,7 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                             .addILike("name", dataExplorer.searchValue)
                             .getFilters()
                     });
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(response.items));
                 await api.dispatch<any>(loadMissingProcessesInformation(response.items));
                 api.dispatch(favoritePanelActions.SET_ITEMS({
@@ -71,12 +73,14 @@ export class FavoritePanelMiddlewareService extends DataExplorerMiddlewareServic
                 }));
                 api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
             } catch (e) {
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(favoritePanelActions.SET_ITEMS({
                     items: [],
                     itemsAvailable: 0,
                     page: 0,
                     rowsPerPage: dataExplorer.rowsPerPage
                 }));
+                api.dispatch(couldNotFetchFavoritesContents());
             }
         }
     }
@@ -86,3 +90,9 @@ const favoritesPanelDataExplorerIsNotSet = () =>
     snackbarActions.OPEN_SNACKBAR({
         message: 'Favorites panel is not ready.'
     });
+
+const couldNotFetchFavoritesContents = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        message: 'Could not fetch favorites contents.',
+        kind: SnackbarKind.ERROR
+    });
index e5a8e591d20d1527b0137fffc3a4c35c8cd4b1ff..5a3001fbc0d352f6992421d9b7fa5c6354b0cfc1 100644 (file)
@@ -6,8 +6,9 @@ import { unionize, ofType, UnionOf } from "~/common/unionize";
 import { Dispatch } from "redux";
 import { RootState } from "../store";
 import { checkFavorite } from "./favorites-reducer";
-import { snackbarActions } from "../snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "../snackbar/snackbar-actions";
 import { ServiceRepository } from "~/services/services";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export const favoritesActions = unionize({
     TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(),
@@ -19,10 +20,16 @@ export type FavoritesAction = UnionOf<typeof favoritesActions>;
 
 export const toggleFavorite = (resource: { uuid: string; name: string }) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
+        dispatch(progressIndicatorActions.START_WORKING("toggleFavorite"));
         const userUuid = getState().auth.user!.uuid;
         dispatch(favoritesActions.TOGGLE_FAVORITE({ resourceUuid: resource.uuid }));
-        dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Working..." }));
         const isFavorite = checkFavorite(resource.uuid, getState().favorites);
+        dispatch(snackbarActions.OPEN_SNACKBAR({
+            message: isFavorite
+                ? "Removing from favorites..."
+                : "Adding to favorites..."
+        }));
+
         const promise: any = isFavorite
             ? services.favoriteService.delete({ userUuid, resourceUuid: resource.uuid })
             : services.favoriteService.create({ userUuid, resource });
@@ -35,8 +42,14 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) =>
                     message: isFavorite
                         ? "Removed from favorites"
                         : "Added to favorites",
-                    hideDuration: 2000
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
                 }));
+                dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+            })
+            .catch((e: any) => {
+                dispatch(progressIndicatorActions.STOP_WORKING("toggleFavorite"));
+                throw e;
             });
     };
 
index c9e62f943455a2ebaae9b8d8b45c16ff8084a62f..ab8093b856c4c259b1bab8992e98293444f7ac78 100644 (file)
@@ -61,7 +61,7 @@ export const getProcessRuntime = ({ container }: Process) =>
         ? getTimeDiff(container.finishedAt || '', container.startedAt || '')
         : 0;
 
-export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme) => {
+export const getProcessStatusColor = (status: string, { customs, palette }: ArvadosTheme) => {
     switch (status) {
         case ProcessStatus.RUNNING:
             return customs.colors.blue500;
@@ -71,7 +71,7 @@ export const getProcessStatusColor = (status: string, { customs }: ArvadosTheme)
         case ProcessStatus.FAILED:
             return customs.colors.red900;
         default:
-            return customs.colors.grey500;
+            return palette.grey["500"];
     }
 };
 
diff --git a/src/store/progress-indicator/progress-indicator-actions.ts b/src/store/progress-indicator/progress-indicator-actions.ts
new file mode 100644 (file)
index 0000000..34a43d8
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+
+export const progressIndicatorActions = unionize({
+    START_WORKING: ofType<string>(),
+    STOP_WORKING: ofType<string>(),
+    PERSIST_STOP_WORKING: ofType<string>(),
+    TOGGLE_WORKING: ofType<{ id: string, working: boolean }>()
+});
+
+export type ProgressIndicatorAction = UnionOf<typeof progressIndicatorActions>;
diff --git a/src/store/progress-indicator/progress-indicator-reducer.ts b/src/store/progress-indicator/progress-indicator-reducer.ts
new file mode 100644 (file)
index 0000000..849906b
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ProgressIndicatorAction, progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+
+export type ProgressIndicatorState = { id: string, working: boolean }[];
+
+const initialState: ProgressIndicatorState = [];
+
+export const progressIndicatorReducer = (state: ProgressIndicatorState = initialState, action: ProgressIndicatorAction) => {
+    const startWorking = (id: string) => state.find(p => p.id === id) ? state : state.concat({ id, working: true });
+    const stopWorking = (id: string) => state.filter(p => p.id !== id);
+
+    return progressIndicatorActions.match(action, {
+        START_WORKING: id => startWorking(id),
+        STOP_WORKING: id => stopWorking(id),
+        PERSIST_STOP_WORKING: id => state.map(p => ({
+            ...p,
+            working: p.id === id ? false : p.working
+        })),
+        TOGGLE_WORKING: ({ id, working }) => working ? startWorking(id) : stopWorking(id),
+        default: () => state,
+    });
+};
+
+export function isSystemWorking(state: ProgressIndicatorState): boolean {
+    return state.length > 0 && state.reduce((working, p) => working ? true : p.working, false);
+}
diff --git a/src/store/progress-indicator/with-progress.ts b/src/store/progress-indicator/with-progress.ts
new file mode 100644 (file)
index 0000000..24f7e32
--- /dev/null
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { connect } from 'react-redux';
+import { RootState } from '~/store/store';
+
+export type WithProgressStateProps = {
+    working: boolean;
+};
+
+export const withProgress = (id: string) =>
+    (component: React.ComponentType<WithProgressStateProps>) =>
+        connect(mapStateToProps(id))(component);
+
+export const mapStateToProps = (id: string) => (state: RootState): WithProgressStateProps => {
+    const progress = state.progressIndicator.find(p => p.id === id);
+    return { working: progress ? progress.working : false };
+};
index 519943c1f547a1b7c13c18f880dca8368dc4a635..09e76ae28b99ef30697228df98f97b78531559f7 100644 (file)
@@ -17,7 +17,8 @@ import { Dispatch, MiddlewareAPI } from "redux";
 import { ProjectResource } from "~/models/project";
 import { updateResources } from "~/store/resources/resources-actions";
 import { getProperty } from "~/store/properties/properties";
-import { snackbarActions } from '../snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '../snackbar/snackbar-actions';
+import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
 import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
 import { ListResults } from '~/services/common-service/common-resource-service';
 import { loadContainers } from '../processes/processes-actions';
@@ -38,12 +39,21 @@ export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService
             api.dispatch(projectPanelDataExplorerIsNotSet());
         } else {
             try {
+                api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
                 const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer));
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch<any>(updateFavorites(response.items.map(item => item.uuid)));
                 api.dispatch(updateResources(response.items));
                 await api.dispatch<any>(loadMissingProcessesInformation(response.items));
                 api.dispatch(setItems(response));
             } catch (e) {
+                api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+                api.dispatch(projectPanelActions.SET_ITEMS({
+                    items: [],
+                    itemsAvailable: 0,
+                    page: 0,
+                    rowsPerPage: dataExplorer.rowsPerPage
+                }));
                 api.dispatch(couldNotFetchProjectContents());
             }
         }
@@ -116,7 +126,8 @@ const projectPanelCurrentUuidIsNotSet = () =>
 
 const couldNotFetchProjectContents = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch project contents.'
+        message: 'Could not fetch project contents.',
+        kind: SnackbarKind.ERROR
     });
 
 const projectPanelDataExplorerIsNotSet = () =>
index 073de22c4ea4b9fa30aae1b15fc2311dfc706fe0..23c5ea2217972d07765cc720b0e3253f4ac50b2a 100644 (file)
@@ -13,6 +13,7 @@ import { getTreePicker, TreePicker } from '../tree-picker/tree-picker';
 import { TreeItemStatus } from "~/components/tree/tree";
 import { getNodeAncestors, getNodeValue, getNodeAncestorsIds, getNode } from '~/models/tree';
 import { ProjectResource } from '~/models/project';
+import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
 
 export enum SidePanelTreeCategory {
     PROJECTS = 'Projects',
index 55d9f3a8651b86afecc516aa48dc1af0abc412e1..d6d7128e85547cb313f283127b039abbacd340fd 100644 (file)
@@ -4,9 +4,23 @@
 
 import { unionize, ofType, UnionOf } from "~/common/unionize";
 
+export interface SnackbarMessage {
+    message: string;
+    hideDuration: number;
+    kind: SnackbarKind;
+}
+
+export enum SnackbarKind {
+    SUCCESS = 1,
+    ERROR = 2,
+    INFO = 3,
+    WARNING = 4
+}
+
 export const snackbarActions = unionize({
-    OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number}>(),
-    CLOSE_SNACKBAR: ofType<{}>()
+    OPEN_SNACKBAR: ofType<{message: string; hideDuration?: number, kind?: SnackbarKind}>(),
+    CLOSE_SNACKBAR: ofType<{}>(),
+    SHIFT_MESSAGES: ofType<{}>()
 });
 
 export type SnackbarAction = UnionOf<typeof snackbarActions>;
index fc2f4a1964e27627ff5d02e63150713bbe78eb3b..73c566fc8cb95945e7b9b95abea9dfa4e00e6684 100644 (file)
@@ -2,26 +2,43 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { SnackbarAction, snackbarActions } from "./snackbar-actions";
+import { SnackbarAction, snackbarActions, SnackbarKind, SnackbarMessage } from "./snackbar-actions";
 
 export interface SnackbarState {
-    message: string;
+    messages: SnackbarMessage[];
     open: boolean;
-    hideDuration: number;
 }
 
 const DEFAULT_HIDE_DURATION = 3000;
 
 const initialState: SnackbarState = {
-    message: "",
-    open: false,
-    hideDuration: DEFAULT_HIDE_DURATION
+    messages: [],
+    open: false
 };
 
 export const snackbarReducer = (state = initialState, action: SnackbarAction) => {
     return snackbarActions.match(action, {
-        OPEN_SNACKBAR: data => ({ ...initialState, ...data, open: true }),
-        CLOSE_SNACKBAR: () => initialState,
+        OPEN_SNACKBAR: data => {
+            return {
+                open: true,
+                messages: state.messages.concat({
+                    message: data.message,
+                    hideDuration: data.hideDuration ? data.hideDuration : DEFAULT_HIDE_DURATION,
+                    kind: data.kind ? data.kind : SnackbarKind.INFO
+                })
+            };
+        },
+        CLOSE_SNACKBAR: () => ({
+            ...state,
+            open: false
+        }),
+        SHIFT_MESSAGES: () => {
+            const messages = state.messages.filter((m, idx) => idx > 0);
+            return {
+                open: messages.length > 0,
+                messages
+            };
+        },
         default: () => state,
     });
 };
index 43ab2310f754c91d63a58f25fd7ef3bfcbe7af90..012b747425b72e714472a5b2a0cfe89d03dc2546 100644 (file)
@@ -34,6 +34,7 @@ import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel
 import { processPanelReducer } from '~/store/process-panel/process-panel-reducer';
 import { SHARED_WITH_ME_PANEL_ID } from '~/store/shared-with-me-panel/shared-with-me-panel-actions';
 import { SharedWithMeMiddlewareService } from './shared-with-me-panel/shared-with-me-middleware-service';
+import { progressIndicatorReducer } from './progress-indicator/progress-indicator-reducer';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -89,5 +90,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     snackbar: snackbarReducer,
     treePicker: treePickerReducer,
     fileUploader: fileUploaderReducer,
-    processPanel: processPanelReducer
+    processPanel: processPanelReducer,
+    progressIndicator: progressIndicatorReducer
 });
index 6e8fa542478766368968e0912fc636da5f4c9b6b..90838b207a559292ac538fbe62a02571233cbd30 100644 (file)
@@ -19,8 +19,9 @@ import { TrashPanelColumnNames, TrashPanelFilter } from "~/views/trash-panel/tra
 import { ProjectResource } from "~/models/project";
 import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
 import { updateFavorites } from "~/store/favorites/favorites-actions";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { updateResources } from "~/store/resources/resources-actions";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 
 export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -47,6 +48,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
         }
 
         try {
+            api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
             const userUuid = this.services.authService.getUuid()!;
             const listResults = await this.services.groupsService
                 .contents(userUuid, {
@@ -61,6 +63,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
                     recursive: true,
                     includeTrash: true
                 });
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
 
             const items = listResults.items.map(it => it.uuid);
 
@@ -71,6 +74,13 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
             api.dispatch<any>(updateFavorites(items));
             api.dispatch(updateResources(listResults.items));
         } catch (e) {
+            api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+            api.dispatch(trashPanelActions.SET_ITEMS({
+                items: [],
+                itemsAvailable: 0,
+                page: 0,
+                rowsPerPage: dataExplorer.rowsPerPage
+            }));
             api.dispatch(couldNotFetchTrashContents());
         }
     }
@@ -78,5 +88,7 @@ export class TrashPanelMiddlewareService extends DataExplorerMiddlewareService {
 
 const couldNotFetchTrashContents = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch trash contents.'
+        message: 'Could not fetch trash contents.',
+        kind: SnackbarKind.ERROR
     });
+
index cd6df55670d322044c5d386a3c597cc19f0db3e0..5cf952eb1fa98a5a53857faf2f9da6224039fcfb 100644 (file)
@@ -5,53 +5,69 @@
 import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
-import { snackbarActions } from "~/store/snackbar/snackbar-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { trashPanelActions } from "~/store/trash-panel/trash-panel-action";
 import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "~/store/side-panel-tree/side-panel-tree-actions";
 import { projectPanelActions } from "~/store/project-panel/project-panel-action";
-import { ResourceKind, TrashableResource } from "~/models/resource";
+import { ResourceKind } from "~/models/resource";
 
 export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
-        if (isTrashed) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
-            await services.groupsService.untrash(uuid);
-            dispatch<any>(activateSidePanelTreeItem(uuid));
-            dispatch(trashPanelActions.REQUEST_ITEMS());
-            dispatch(snackbarActions.CLOSE_SNACKBAR());
+        try {
+            if (isTrashed) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+                await services.groupsService.untrash(uuid);
+                dispatch<any>(activateSidePanelTreeItem(uuid));
+                dispatch(trashPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Restored from trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            } else {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+                await services.groupsService.trash(uuid);
+                dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Added to trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            }
+        } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Restored from trash",
-                hideDuration: 2000
-            }));
-        } else {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
-            await services.groupsService.trash(uuid);
-            dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
-            dispatch(snackbarActions.CLOSE_SNACKBAR());
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Added to trash",
-                hideDuration: 2000
+                message: "Could not move project to trash",
+                kind: SnackbarKind.ERROR
             }));
         }
     };
 
 export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
-        if (isTrashed) {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
-            await services.collectionService.untrash(uuid);
-            dispatch(trashPanelActions.REQUEST_ITEMS());
-            dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Restored from trash",
-                hideDuration: 2000
-            }));
-        } else {
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
-            await services.collectionService.trash(uuid);
-            dispatch(projectPanelActions.REQUEST_ITEMS());
+        try {
+            if (isTrashed) {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Restoring from trash..." }));
+                await services.collectionService.untrash(uuid);
+                dispatch(trashPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Restored from trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            } else {
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Moving to trash..." }));
+                await services.collectionService.trash(uuid);
+                dispatch(projectPanelActions.REQUEST_ITEMS());
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: "Added to trash",
+                    hideDuration: 2000,
+                    kind: SnackbarKind.SUCCESS
+                }));
+            }
+        } catch (e) {
             dispatch(snackbarActions.OPEN_SNACKBAR({
-                message: "Added to trash",
-                hideDuration: 2000
+                message: "Could not move collection to trash",
+                kind: SnackbarKind.ERROR
             }));
         }
     };
index 16dd59933411f5d394b31a58d5262fbff0418cda..74c3e64aaacf1139d5a90cff0c5ada69f40b14b2 100644 (file)
@@ -21,7 +21,9 @@ interface Props {
 }
 
 const mapStateToProps = (state: RootState, { id }: Props) => {
-    return getDataExplorer(state.dataExplorer, id);
+    const progress = state.progressIndicator.find(p => p.id === id);
+    const working = progress && progress.working;
+    return { ...getDataExplorer(state.dataExplorer, id), working };
 };
 
 const mapDispatchToProps = () => {
index ec2a511a1e89faf2e0261911dbe8e5b679d1882d..93cf4968e99e5bd1475258fa23b5f3ed35fe8003 100644 (file)
@@ -13,6 +13,7 @@ import { NotificationsMenu } from "~/views-components/main-app-bar/notifications
 import { AccountMenu } from "~/views-components/main-app-bar/account-menu";
 import { AnonymousMenu } from "~/views-components/main-app-bar/anonymous-menu";
 import { HelpMenu } from './help-menu';
+import { ReactNode } from "react";
 
 type CssRules = 'toolbar' | 'link';
 
@@ -31,6 +32,7 @@ interface MainAppBarDataProps {
     searchDebounce?: number;
     user?: User;
     buildInfo?: string;
+    children?: ReactNode;
 }
 
 export interface MainAppBarActionProps {
@@ -41,7 +43,7 @@ export type MainAppBarProps = MainAppBarDataProps & MainAppBarActionProps & With
 
 export const MainAppBar = withStyles(styles)(
     (props: MainAppBarProps) => {
-        return <AppBar position="static">
+        return <AppBar position="absolute">
             <Toolbar className={props.classes.toolbar}>
                 <Grid container justify="space-between">
                     <Grid container item xs={3} direction="column" justify="center">
@@ -80,6 +82,7 @@ export const MainAppBar = withStyles(styles)(
                     </Grid>
                 </Grid>
             </Toolbar>
+            {props.children}
         </AppBar>;
     }
 );
diff --git a/src/views-components/progress/content-progress.tsx b/src/views-components/progress/content-progress.tsx
new file mode 100644 (file)
index 0000000..fa2cad5
--- /dev/null
@@ -0,0 +1,13 @@
+// // Copyright (C) The Arvados Authors. All rights reserved.
+// //
+// // SPDX-License-Identifier: AGPL-3.0
+//
+// import * as React from 'react';
+// import { CircularProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+//
+// export const ContentProgress = withProgress(ProgressIndicatorData.CONTENT_PROGRESS)((props: WithProgressStateProps) =>
+//     props.started ? <CircularProgress /> : null
+// );
diff --git a/src/views-components/progress/side-panel-progress.tsx b/src/views-components/progress/side-panel-progress.tsx
new file mode 100644 (file)
index 0000000..2d832a5
--- /dev/null
@@ -0,0 +1,13 @@
+// // Copyright (C) The Arvados Authors. All rights reserved.
+// //
+// // SPDX-License-Identifier: AGPL-3.0
+//
+// import * as React from 'react';
+// import { CircularProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+//
+// export const SidePanelProgress = withProgress(ProgressIndicatorData.SIDE_PANEL_PROGRESS)((props: WithProgressStateProps) =>
+//     props.started ? <span style={{ display: 'flex', justifyContent: 'center', marginTop: "40px" }}><CircularProgress /></span> : null
+// );
diff --git a/src/views-components/progress/workbench-progress.tsx b/src/views-components/progress/workbench-progress.tsx
new file mode 100644 (file)
index 0000000..1fdd57c
--- /dev/null
@@ -0,0 +1,14 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+// import * as React from 'react';
+// import { LinearProgress } from '@material-ui/core';
+// import { withProgress } from '~/store/progress-indicator/with-progress';
+// import { WithProgressStateProps } from '~/store/progress-indicator/with-progress';
+// import { ProgressIndicatorData } from '~/store/progress-indicator/progress-indicator-reducer';
+
+// export const WorkbenchProgress = withProgress(ProgressIndicatorData.WORKBENCH_PROGRESS)(
+//     (props: WithProgressStateProps) =>
+//         props.started ? <LinearProgress color="secondary" /> : null
+// );
index d0b00d6fb50c4576a9e8190311ec056c826c32bc..4d4760fac37abf583bebde4342c26dc4f570cf75 100644 (file)
@@ -16,6 +16,7 @@ import { openSidePanelContextMenu } from '~/store/context-menu/context-menu-acti
 
 export interface SidePanelTreeProps {
     onItemActivation: (id: string) => void;
+    sidePanelProgress?: boolean;
 }
 
 type SidePanelTreeActionProps = Pick<TreePickerProps, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen'>;
index fffe3344c9ce66dd94c3a8d79933f83047aaf7a9..739e9eac1139ee62b3b38eeb3b8988cea5cc3bb2 100644 (file)
@@ -11,6 +11,7 @@ import { connect } from 'react-redux';
 import { navigateFromSidePanel } from '../../store/side-panel/side-panel-action';
 import { Grid } from '@material-ui/core';
 import { SidePanelButton } from '~/views-components/side-panel-button/side-panel-button';
+import { RootState } from '~/store/store';
 
 const DRAWER_WITDH = 240;
 
@@ -32,11 +33,14 @@ const mapDispatchToProps = (dispatch: Dispatch): SidePanelTreeProps => ({
     }
 });
 
+const mapStateToProps = (state: RootState) => ({
+});
+
 export const SidePanel = compose(
     withStyles(styles),
-    connect(undefined, mapDispatchToProps)
+    connect(mapStateToProps, mapDispatchToProps)
 )(({ classes, ...props }: WithStyles<CssRules> & SidePanelTreeProps) =>
     <Grid item xs>
         <SidePanelButton />
         <SidePanelTree {...props} />
-    </Grid>);
\ No newline at end of file
+    </Grid>);
index 535777e1bd4fd0765f1ac88914e592165f969258..7449e1e2f82027afeb870a834031dc584df6390d 100644 (file)
@@ -7,21 +7,129 @@ 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" },
-    open: state.snackbar.open,
-    message: <span>{state.snackbar.message}</span>,
-    autoHideDuration: state.snackbar.hideDuration
-});
+const mapStateToProps = (state: RootState): SnackbarProps & ArvadosSnackbarProps => {
+    const messages = state.snackbar.messages;
+    return {
+        anchorOrigin: { vertical: "bottom", horizontal: "right" },
+        open: state.snackbar.open,
+        message: <span>{messages.length > 0 ? messages[0].message : ""}</span>,
+        autoHideDuration: messages.length > 0 ? messages[0].hideDuration : 0,
+        kind: messages.length > 0 ? messages[0].kind : SnackbarKind.INFO
+    };
+};
 
-const mapDispatchToProps = (dispatch: Dispatch): Pick<SnackbarProps, "onClose"> => ({
+const mapDispatchToProps = (dispatch: Dispatch) => ({
     onClose: (event: any, reason: string) => {
         if (reason !== "clickaway") {
             dispatch(snackbarActions.CLOSE_SNACKBAR());
         }
+    },
+    onExited: () => {
+        dispatch(snackbarActions.SHIFT_MESSAGES());
     }
 });
 
-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 } = props;
+
+    let Icon = InfoIcon;
+    let cssClass = classes.info;
+
+    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)
+);
index 73849562dad12f34c4153188f0d9e6cd5ce8180c..4ba967c0529a37b5142f13bc6648e1b3e3b14a01 100644 (file)
@@ -164,23 +164,18 @@ export const FavoritePanel = withStyles(styles)(
     connect(mapStateToProps, mapDispatchToProps)(
         class extends React.Component<FavoritePanelProps> {
             render() {
-                return this.hasAnyFavorites()
-                    ? <DataExplorer
-                        id={FAVORITE_PANEL_ID}
-                        onRowClick={this.props.onItemClick}
-                        onRowDoubleClick={this.props.onItemDoubleClick}
-                        onContextMenu={this.props.onContextMenu}
-                        contextMenuColumn={true}
-                        dataTableDefaultView={<DataTableDefaultView icon={FavoriteIcon}/>} />
-                    : <PanelDefaultView
-                        icon={FavoriteIcon}
-                        messages={['Your favorites list is empty.']} />;
-            }
-
-            hasAnyFavorites = () => {
-                return Object
-                    .keys(this.props.favorites)
-                    .find(uuid => this.props.favorites[uuid]);
+                return <DataExplorer
+                    id={FAVORITE_PANEL_ID}
+                    onRowClick={this.props.onItemClick}
+                    onRowDoubleClick={this.props.onItemDoubleClick}
+                    onContextMenu={this.props.onContextMenu}
+                    contextMenuColumn={true}
+                    dataTableDefaultView={
+                        <DataTableDefaultView
+                            icon={FavoriteIcon}
+                            messages={['Your favorites list is empty.']}
+                            />
+                    } />;
             }
         }
     )
index 0607c4711e2238ea26dcefff22663ca66568d258..9cff1e981630ed6a72e8a2868bb8c70c413b22cd 100644 (file)
@@ -16,7 +16,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         height: '100%'
     },
     title: {
-        color: theme.customs.colors.grey700
+        color: theme.palette.grey["700"]
     },
     gridFilter: {
         height: '20px',
index 25579396167eef30d75abbe2d80ca71937754202..2b2be2e8905ec5d111faf8c121da72dddba94371 100644 (file)
@@ -23,9 +23,23 @@ import { ProjectResource } from '~/models/project';
 import { navigateTo } from '~/store/navigation/navigation-action';
 import { getProperty } from '~/store/properties/properties';
 import { PROJECT_PANEL_CURRENT_UUID } from '~/store/project-panel/project-panel-action';
-import { filterResources } from '~/store/resources/resources';
-import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
+import { StyleRulesCallback, WithStyles } from "@material-ui/core";
+import { ArvadosTheme } from "~/common/custom-theme";
+import withStyles from "@material-ui/core/styles/withStyles";
+
+type CssRules = 'root' | "button";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        position: 'relative',
+        width: '100%',
+        height: '100%'
+    },
+    button: {
+        marginLeft: theme.spacing.unit
+    },
+});
 
 export enum ProjectPanelColumnNames {
     NAME = "Name",
@@ -110,30 +124,33 @@ interface ProjectPanelDataProps {
     resources: ResourcesState;
 }
 
-type ProjectPanelProps = ProjectPanelDataProps & DispatchProp & RouteComponentProps<{ id: string }>;
+type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
+    & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
 
-export const ProjectPanel = connect((state: RootState) => ({
+export const ProjectPanel = withStyles(styles)(
+    connect((state: RootState) => ({
         currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
         resources: state.resources
     }))(
         class extends React.Component<ProjectPanelProps> {
             render() {
-                return this.hasAnyItems()
-                    ? <DataExplorer
+                const { classes } = this.props;
+                return <div className={classes.root}>
+                    <DataExplorer
                         id={PROJECT_PANEL_ID}
                         onRowClick={this.handleRowClick}
                         onRowDoubleClick={this.handleRowDoubleClick}
                         onContextMenu={this.handleContextMenu}
                         contextMenuColumn={true}
-                        dataTableDefaultView={<DataTableDefaultView icon={ProjectIcon}/>} />
-                    : <PanelDefaultView
-                        icon={ProjectIcon}
-                        messages={['Your project is empty.', 'Please create a project or create a collection and upload a data.']} />;
-            }
-
-            hasAnyItems = () => {
-                const resources = filterResources(this.isCurrentItemChild)(this.props.resources);
-                return resources.length > 0;
+                        dataTableDefaultView={
+                            <DataTableDefaultView
+                                icon={ProjectIcon}
+                                messages={[
+                                    'Your project is empty.',
+                                    'Please create a project or create a collection and upload a data.'
+                                ]}/>
+                        }/>
+                </div>;
             }
 
             isCurrentItemChild = (resource: Resource) => {
@@ -164,4 +181,5 @@ export const ProjectPanel = connect((state: RootState) => ({
             }
 
         }
-    );
+    )
+);
index e627f4f26bcddbd32117934f93c08fd9fa80128f..92febd8acf55467f960eb234ac1ecee99c6ea28a 100644 (file)
@@ -155,23 +155,17 @@ export const TrashPanel = withStyles(styles)(
     }))(
         class extends React.Component<TrashPanelProps> {
             render() {
-                return this.hasAnyTrashedResources()
-                    ? <DataExplorer
-                        id={TRASH_PANEL_ID}
-                        onRowClick={this.handleRowClick}
-                        onRowDoubleClick={this.handleRowDoubleClick}
-                        onContextMenu={this.handleContextMenu}
-                        contextMenuColumn={false}
-                        dataTableDefaultView={<DataTableDefaultView icon={TrashIcon} />} />
-                    : <PanelDefaultView
-                        icon={TrashIcon}
-                        messages={['Your trash list is empty.']} />;
-            }
-
-            hasAnyTrashedResources = () => {
-                // TODO: implement check if there is anything in the trash,
-                //       without taking pagination into the account
-                return true;
+                return <DataExplorer
+                    id={TRASH_PANEL_ID}
+                    onRowClick={this.handleRowClick}
+                    onRowDoubleClick={this.handleRowDoubleClick}
+                    onContextMenu={this.handleContextMenu}
+                    contextMenuColumn={false}
+                    dataTableDefaultView={
+                        <DataTableDefaultView
+                            icon={TrashIcon}
+                            messages={['Your trash list is empty.']}/>
+                    } />;
             }
 
             handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
index 1d7d47d09ee89f8085312cf2b0a1b804f823e07c..ad1a266881993b8f5e38eb568ac0f8f2c175ef3d 100644 (file)
@@ -41,10 +41,11 @@ import { FilesUploadCollectionDialog } from '~/views-components/dialog-forms/fil
 import { PartialCopyCollectionDialog } from '~/views-components/dialog-forms/partial-copy-collection-dialog';
 import { TrashPanel } from "~/views/trash-panel/trash-panel";
 import { MainContentBar } from '~/views-components/main-content-bar/main-content-bar';
-import { Grid } from '@material-ui/core';
+import { Grid, LinearProgress } from '@material-ui/core';
 import { SharedWithMePanel } from '../shared-with-me-panel/shared-with-me-panel';
-import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
 import SplitterLayout from 'react-splitter-layout';
+import { ProcessCommandDialog } from '~/views-components/process-command-dialog/process-command-dialog';
+import { isSystemWorking } from "~/store/progress-indicator/progress-indicator-reducer";
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content' | 'appBar';
 
@@ -52,7 +53,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         overflow: 'hidden',
         width: '100vw',
-        height: '100vh'
+        height: '100vh',
+        paddingTop: theme.spacing.unit * 8
     },
     container: {
         position: 'relative'
@@ -83,6 +85,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 interface WorkbenchDataProps {
     user?: User;
     currentToken?: string;
+    working: boolean;
 }
 
 interface WorkbenchGeneralProps {
@@ -101,6 +104,7 @@ export const Workbench = withStyles(styles)(
         (state: RootState) => ({
             user: state.auth.user,
             currentToken: state.auth.apiToken,
+            working: isSystemWorking(state.progressIndicator)
         })
     )(
         class extends React.Component<WorkbenchProps, WorkbenchState> {
@@ -110,14 +114,14 @@ export const Workbench = withStyles(styles)(
             render() {
                 const { classes } = this.props;
                 return <>
+                    <MainAppBar
+                        searchText={this.state.searchText}
+                        user={this.props.user}
+                        onSearch={this.onSearch}
+                        buildInfo={this.props.buildInfo}>
+                        {this.props.working ? <LinearProgress color="secondary" /> : null}
+                    </MainAppBar>
                     <Grid container direction="column" className={classes.root}>
-                        <Grid className={classes.appBar}>
-                            <MainAppBar
-                                searchText={this.state.searchText}
-                                user={this.props.user}
-                                onSearch={this.onSearch}
-                                buildInfo={this.props.buildInfo} />
-                        </Grid>
                         {this.props.user &&
                             <Grid container item xs alignItems="stretch" wrap="nowrap">
                                 <Grid container item className={classes.container}>
index 85b43690d37e54ae7d2d4c3dd1f80dffb527abd9..f9b81ca95bf0e83a0f2d7b19866624d9eef5887d 100644 (file)
@@ -14,7 +14,8 @@
     "no-shadowed-variable": false,
     "semicolon": true,
     "array-type": false,
-    "interface-over-type-literal": false
+    "interface-over-type-literal": false,
+    "no-empty": false
   },
   "linterOptions": {
     "exclude": [
index 3765778d616a8698c561eb92a8e8e9339c3f1482..30e94bdefb4c9be9d37fc394908c15653130d55f 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
     "@types/react" "*"
     redux "^3.6.0 || ^4.0.0"
 
+"@types/uuid@3.4.4":
+  version "3.4.4"
+  resolved "https://registry.yarnpkg.com/@types/uuid/-/uuid-3.4.4.tgz#7af69360fa65ef0decb41fd150bf4ca5c0cefdf5"
+  dependencies:
+    "@types/node" "*"
+
 abab@^1.0.4:
   version "1.0.4"
   resolved "https://registry.yarnpkg.com/abab/-/abab-1.0.4.tgz#5faad9c2c07f60dd76770f71cf025b62a63cfd4e"
@@ -7758,14 +7764,14 @@ utils-merge@1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/utils-merge/-/utils-merge-1.0.1.tgz#9f95710f50a267947b2ccc124741c1028427e713"
 
+uuid@3.3.2, uuid@^3.1.0:
+  version "3.3.2"
+  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
+
 uuid@^2.0.2:
   version "2.0.3"
   resolved "https://registry.yarnpkg.com/uuid/-/uuid-2.0.3.tgz#67e2e863797215530dff318e5bf9dcebfd47b21a"
 
-uuid@^3.1.0:
-  version "3.3.2"
-  resolved "https://registry.yarnpkg.com/uuid/-/uuid-3.3.2.tgz#1b4af4955eb3077c501c23872fc6513811587131"
-
 validate-npm-package-license@^3.0.1:
   version "3.0.3"
   resolved "https://registry.yarnpkg.com/validate-npm-package-license/-/validate-npm-package-license-3.0.3.tgz#81643bcbef1bdfecd4623793dc4648948ba98338"