[ARVADOS-WORKBENCH2] updated: 1.1.4-439-g24ddc06

Git user git at public.curoverse.com
Thu Aug 2 02:38:30 EDT 2018


Summary of changes:
 src/common/api/server-api.ts                       | 15 ++---
 src/index.tsx                                      |  7 +--
 src/services/auth-service/auth-service.ts          |  1 -
 src/services/services.ts                           | 21 ++++++-
 src/store/auth/auth-action.ts                      | 40 +++++++++++--
 .../{auth-reducer.test.ts => auth-actions.test.ts} | 67 +++++----------------
 src/store/auth/auth-reducer.test.ts                | 68 ++++------------------
 src/store/auth/auth-reducer.ts                     | 17 +-----
 src/store/details-panel/details-panel-action.ts    | 15 ++---
 src/store/store.ts                                 |  6 +-
 src/views-components/api-token/api-token.tsx       |  4 +-
 src/views/workbench/workbench.test.tsx             |  2 +-
 src/views/workbench/workbench.tsx                  |  6 +-
 13 files changed, 106 insertions(+), 163 deletions(-)
 copy src/store/auth/{auth-reducer.test.ts => auth-actions.test.ts} (52%)

       via  24ddc06cb04b124d337e91c190230bfad83e490b (commit)
      from  3ec7057e490b3d19b15663dd312328f637fa1d3b (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit 24ddc06cb04b124d337e91c190230bfad83e490b
Author: Daniel Kos <daniel.kos at contractors.roche.com>
Date:   Thu Aug 2 08:38:24 2018 +0200

    Make auth reducer side effects free
    
    Feature #13901
    
    Arvados-DCO-1.1-Signed-off-by: Daniel Kos <daniel.kos at contractors.roche.com>

diff --git a/src/common/api/server-api.ts b/src/common/api/server-api.ts
index 75eabd9..a700f27 100644
--- a/src/common/api/server-api.ts
+++ b/src/common/api/server-api.ts
@@ -2,23 +2,18 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import Axios, { AxiosInstance } from "axios";
+import { AxiosInstance } from "axios";
 
-export const API_HOST = process.env.REACT_APP_ARVADOS_API_HOST;
-
-export const authClient: AxiosInstance = Axios.create();
-export const apiClient: AxiosInstance = Axios.create();
-
-export function setServerApiAuthorizationHeader(token: string) {
-    [authClient, apiClient].forEach(client => {
+export function setServerApiAuthorizationHeader(clients: AxiosInstance[], token: string) {
+    clients.forEach(client => {
         client.defaults.headers.common = {
             Authorization: `OAuth2 ${token}`
         };
     });
 }
 
-export function removeServerApiAuthorizationHeader() {
-    [authClient, apiClient].forEach(client => {
+export function removeServerApiAuthorizationHeader(clients: AxiosInstance[]) {
+    clients.forEach(client => {
         delete client.defaults.headers.common.Authorization;
     });
 }
diff --git a/src/index.tsx b/src/index.tsx
index bf2816d..49c4213 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -12,7 +12,7 @@ import createBrowserHistory from "history/createBrowserHistory";
 import { configureStore } from "./store/store";
 import { ConnectedRouter } from "react-router-redux";
 import { ApiToken } from "./views-components/api-token/api-token";
-import { authActions } from "./store/auth/auth-action";
+import { initAuth } from "./store/auth/auth-action";
 import { createServices } from "./services/services";
 import { getProjectList } from "./store/project/project-action";
 import { MuiThemeProvider } from '@material-ui/core/styles';
@@ -31,13 +31,12 @@ addMenuActionSet(ContextMenuKind.FAVORITE, favoriteActionSet);
 
 fetchConfig()
     .then(config => {
-
         const history = createBrowserHistory();
         const services = createServices(config.API_HOST);
         const store = configureStore(history, services);
 
-        store.dispatch(authActions.INIT());
-        store.dispatch<any>(getProjectList(services.authService.getUuid()));
+        store.dispatch(initAuth());
+        store.dispatch(getProjectList(services.authService.getUuid()));
 
         const Token = (props: any) => <ApiToken authService={services.authService} {...props}/>;
 
diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts
index 551d435..273208c 100644
--- a/src/services/auth-service/auth-service.ts
+++ b/src/services/auth-service/auth-service.ts
@@ -2,7 +2,6 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { API_HOST } from "../../common/api/server-api";
 import { User } from "../../models/user";
 import { AxiosInstance } from "../../../node_modules/axios";
 
diff --git a/src/services/services.ts b/src/services/services.ts
index 350128f..0d19cee 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -4,34 +4,49 @@
 
 import { AuthService } from "./auth-service/auth-service";
 import { GroupsService } from "./groups-service/groups-service";
-import { authClient, apiClient } from "../common/api/server-api";
 import { ProjectService } from "./project-service/project-service";
 import { LinkService } from "./link-service/link-service";
 import { FavoriteService } from "./favorite-service/favorite-service";
+import { AxiosInstance } from "axios";
+import { CommonResourceService } from "../common/api/common-resource-service";
+import { CollectionResource } from "../models/collection";
+import { Resource } from "../models/resource";
+import Axios from "axios";
 
 export interface ServiceRepository {
+    apiClient: AxiosInstance;
+    authClient: AxiosInstance;
+
     authService: AuthService;
     groupsService: GroupsService;
     projectService: ProjectService;
     linkService: LinkService;
     favoriteService: FavoriteService;
+    collectionService: CommonResourceService<Resource>;
 }
 
 export const createServices = (baseUrl: string): ServiceRepository => {
+    const authClient = Axios.create();
+    const apiClient = Axios.create();
+
     authClient.defaults.baseURL = baseUrl;
-    apiClient.defaults.baseURL = baseUrl + "/arvados/v1";
+    apiClient.defaults.baseURL = `${baseUrl}/arvados/v1`;
 
     const authService = new AuthService(authClient, apiClient);
     const groupsService = new GroupsService(apiClient);
     const projectService = new ProjectService(apiClient);
     const linkService = new LinkService(apiClient);
     const favoriteService = new FavoriteService(linkService, groupsService);
+    const collectionService = new CommonResourceService<CollectionResource>(apiClient, "collections");
 
     return {
+        apiClient,
+        authClient,
         authService,
         groupsService,
         projectService,
         linkService,
-        favoriteService
+        favoriteService,
+        collectionService
     };
 };
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index 8b268cc..839134c 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -7,12 +7,13 @@ import { Dispatch } from "redux";
 import { User } from "../../models/user";
 import { RootState } from "../store";
 import { ServiceRepository } from "../../services/services";
+import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api";
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
     LOGIN: {},
     LOGOUT: {},
-    INIT: {},
+    INIT: ofType<{ user: User, token: string }>(),
     USER_DETAILS_REQUEST: {},
     USER_DETAILS_SUCCESS: ofType<User>()
 }, {
@@ -20,11 +21,42 @@ export const authActions = unionize({
     value: 'payload'
 });
 
+export const initAuth = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    const user = services.authService.getUser();
+    const token = services.authService.getApiToken();
+    if (token) {
+        setServerApiAuthorizationHeader([services.authClient, services.apiClient], token);
+    }
+    if (token && user) {
+        dispatch(authActions.INIT({ user, token }));
+    }
+};
+
+export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.saveApiToken(token);
+    setServerApiAuthorizationHeader([services.authClient, services.apiClient], token);
+    dispatch(authActions.SAVE_API_TOKEN(token));
+};
+
+export const login = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.login();
+    dispatch(authActions.LOGIN());
+};
+
+export const logout = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.removeApiToken();
+    services.authService.removeUser();
+    removeServerApiAuthorizationHeader([services.authClient, services.apiClient]);
+    services.authService.logout();
+    dispatch(authActions.LOGOUT());
+};
+
 export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<User> => {
     dispatch(authActions.USER_DETAILS_REQUEST());
-    return services.authService.getUserDetails().then(details => {
-        dispatch(authActions.USER_DETAILS_SUCCESS(details));
-        return details;
+    return services.authService.getUserDetails().then(user => {
+        services.authService.saveUser(user);
+        dispatch(authActions.USER_DETAILS_SUCCESS(user));
+        return user;
     });
 };
 
diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-actions.test.ts
similarity index 52%
copy from src/store/auth/auth-reducer.test.ts
copy to src/store/auth/auth-actions.test.ts
index a741928..dc399cf 100644
--- a/src/store/auth/auth-reducer.test.ts
+++ b/src/store/auth/auth-actions.test.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { authReducer, AuthState } from "./auth-reducer";
-import { AuthAction, authActions } from "./auth-action";
+import { AuthAction, authActions, initAuth } from "./auth-action";
 import {
     API_TOKEN_KEY,
     USER_EMAIL_KEY,
@@ -15,26 +15,20 @@ import {
 
 import 'jest-localstorage-mock';
 import { createServices } from "../../services/services";
+import { configureStore, RootStore } from "../store";
+import createBrowserHistory from "../../../node_modules/@types/history/createBrowserHistory";
 
-describe('auth-reducer', () => {
+describe('auth-actions', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
+    let store: RootStore;
 
-    beforeAll(() => {
+    beforeEach(() => {
+        store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
         localStorage.clear();
         reducer = authReducer(createServices("/arvados/v1"));
     });
 
-    it('should return default state on initialisation', () => {
-        const initialState = undefined;
-        const state = reducer(initialState, authActions.INIT());
-        expect(state).toEqual({
-            apiToken: undefined,
-            user: undefined
-        });
-    });
-
-    it('should read user and api token from local storage on init if they are there', () => {
-        const initialState = undefined;
+    it('should initialise state with user and api token from local storage', () => {
 
         localStorage.setItem(API_TOKEN_KEY, "token");
         localStorage.setItem(USER_EMAIL_KEY, "test at test.com");
@@ -43,57 +37,21 @@ describe('auth-reducer', () => {
         localStorage.setItem(USER_UUID_KEY, "uuid");
         localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
 
-        const state = reducer(initialState, authActions.INIT());
-        expect(state).toEqual({
-            apiToken: "token",
-            user: {
-                email: "test at test.com",
-                firstName: "John",
-                lastName: "Doe",
-                uuid: "uuid",
-                ownerUuid: "ownerUuid"
-            }
-        });
-    });
-
-    it('should store token in local storage', () => {
-        const initialState = undefined;
+        store.dispatch(initAuth());
 
-        const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
-        expect(state).toEqual({
+        expect(store.getState().auth).toEqual({
             apiToken: "token",
-            user: undefined
-        });
-
-        expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
-    });
-
-    it('should set user details on success fetch', () => {
-        const initialState = undefined;
-
-        const user = {
-            email: "test at test.com",
-            firstName: "John",
-            lastName: "Doe",
-            uuid: "uuid",
-            ownerUuid: "ownerUuid"
-        };
-
-        const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
-        expect(state).toEqual({
-            apiToken: undefined,
             user: {
                 email: "test at test.com",
                 firstName: "John",
                 lastName: "Doe",
                 uuid: "uuid",
-                ownerUuid: "ownerUuid",
+                ownerUuid: "ownerUuid"
             }
         });
-
-        expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
     });
 
+    /*
     it('should fire external url to login', () => {
         const initialState = undefined;
         window.location.assign = jest.fn();
@@ -111,4 +69,5 @@ describe('auth-reducer', () => {
             `/logout?return_to=${location.protocol}//${location.host}`
         );
     });
+    */
 });
diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts
index a741928..0e05263 100644
--- a/src/store/auth/auth-reducer.test.ts
+++ b/src/store/auth/auth-reducer.test.ts
@@ -4,14 +4,6 @@
 
 import { authReducer, AuthState } from "./auth-reducer";
 import { AuthAction, authActions } from "./auth-action";
-import {
-    API_TOKEN_KEY,
-    USER_EMAIL_KEY,
-    USER_FIRST_NAME_KEY,
-    USER_LAST_NAME_KEY,
-    USER_OWNER_UUID_KEY,
-    USER_UUID_KEY
-} from "../../services/auth-service/auth-service";
 
 import 'jest-localstorage-mock';
 import { createServices } from "../../services/services";
@@ -24,39 +16,23 @@ describe('auth-reducer', () => {
         reducer = authReducer(createServices("/arvados/v1"));
     });
 
-    it('should return default state on initialisation', () => {
+    it('should correctly initialise state', () => {
         const initialState = undefined;
-        const state = reducer(initialState, authActions.INIT());
-        expect(state).toEqual({
-            apiToken: undefined,
-            user: undefined
-        });
-    });
-
-    it('should read user and api token from local storage on init if they are there', () => {
-        const initialState = undefined;
-
-        localStorage.setItem(API_TOKEN_KEY, "token");
-        localStorage.setItem(USER_EMAIL_KEY, "test at test.com");
-        localStorage.setItem(USER_FIRST_NAME_KEY, "John");
-        localStorage.setItem(USER_LAST_NAME_KEY, "Doe");
-        localStorage.setItem(USER_UUID_KEY, "uuid");
-        localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid");
-
-        const state = reducer(initialState, authActions.INIT());
+        const user = {
+            email: "test at test.com",
+            firstName: "John",
+            lastName: "Doe",
+            uuid: "uuid",
+            ownerUuid: "ownerUuid"
+        };
+        const state = reducer(initialState, authActions.INIT({user, token: "token"}));
         expect(state).toEqual({
             apiToken: "token",
-            user: {
-                email: "test at test.com",
-                firstName: "John",
-                lastName: "Doe",
-                uuid: "uuid",
-                ownerUuid: "ownerUuid"
-            }
+            user
         });
     });
 
-    it('should store token in local storage', () => {
+    it('should save api token', () => {
         const initialState = undefined;
 
         const state = reducer(initialState, authActions.SAVE_API_TOKEN("token"));
@@ -64,8 +40,6 @@ describe('auth-reducer', () => {
             apiToken: "token",
             user: undefined
         });
-
-        expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
     });
 
     it('should set user details on success fetch', () => {
@@ -90,25 +64,5 @@ describe('auth-reducer', () => {
                 ownerUuid: "ownerUuid",
             }
         });
-
-        expect(localStorage.getItem(API_TOKEN_KEY)).toBe("token");
-    });
-
-    it('should fire external url to login', () => {
-        const initialState = undefined;
-        window.location.assign = jest.fn();
-        reducer(initialState, authActions.LOGIN());
-        expect(window.location.assign).toBeCalledWith(
-            `/login?return_to=${window.location.protocol}//${window.location.host}/token`
-        );
-    });
-
-    it('should fire external url to logout', () => {
-        const initialState = undefined;
-        window.location.assign = jest.fn();
-        reducer(initialState, authActions.LOGOUT());
-        expect(window.location.assign).toBeCalledWith(
-            `/logout?return_to=${location.protocol}//${location.host}`
-        );
     });
 });
diff --git a/src/store/auth/auth-reducer.ts b/src/store/auth/auth-reducer.ts
index e3f968a..ac9ecbe 100644
--- a/src/store/auth/auth-reducer.ts
+++ b/src/store/auth/auth-reducer.ts
@@ -15,31 +15,18 @@ export interface AuthState {
 export const authReducer = (services: ServiceRepository) => (state: AuthState = {}, action: AuthAction) => {
     return authActions.match(action, {
         SAVE_API_TOKEN: (token: string) => {
-            services.authService.saveApiToken(token);
-            setServerApiAuthorizationHeader(token);
             return {...state, apiToken: token};
         },
-        INIT: () => {
-            const user = services.authService.getUser();
-            const token = services.authService.getApiToken();
-            if (token) {
-                setServerApiAuthorizationHeader(token);
-            }
-            return {user, apiToken: token};
+        INIT: ({ user, token }) => {
+            return { user, apiToken: token };
         },
         LOGIN: () => {
-            services.authService.login();
             return state;
         },
         LOGOUT: () => {
-            services.authService.removeApiToken();
-            services.authService.removeUser();
-            removeServerApiAuthorizationHeader();
-            services.authService.logout();
             return {...state, apiToken: undefined};
         },
         USER_DETAILS_SUCCESS: (user: User) => {
-            services.authService.saveUser(user);
             return {...state, user};
         },
         default: () => state
diff --git a/src/store/details-panel/details-panel-action.ts b/src/store/details-panel/details-panel-action.ts
index 03212b9..cb5a709 100644
--- a/src/store/details-panel/details-panel-action.ts
+++ b/src/store/details-panel/details-panel-action.ts
@@ -5,8 +5,9 @@
 import { unionize, ofType, UnionOf } from "unionize";
 import { CommonResourceService } from "../../common/api/common-resource-service";
 import { Dispatch } from "redux";
-import { apiClient } from "../../common/api/server-api";
 import { Resource, ResourceKind } from "../../models/resource";
+import { RootState } from "../store";
+import { ServiceRepository } from "../../services/services";
 
 export const detailsPanelActions = unionize({
     TOGGLE_DETAILS_PANEL: ofType<{}>(),
@@ -17,23 +18,23 @@ export const detailsPanelActions = unionize({
 export type DetailsPanelAction = UnionOf<typeof detailsPanelActions>;
 
 export const loadDetails = (uuid: string, kind: ResourceKind) =>
-    (dispatch: Dispatch) => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(detailsPanelActions.LOAD_DETAILS({ uuid, kind }));
-        getService(kind)
+        getService(services, kind)
             .get(uuid)
             .then(project => {
                 dispatch(detailsPanelActions.LOAD_DETAILS_SUCCESS({ item: project }));
             });
     };
 
-const getService = (kind: ResourceKind) => {
+const getService = (services: ServiceRepository, kind: ResourceKind) => {
     switch (kind) {
         case ResourceKind.PROJECT:
-            return new CommonResourceService(apiClient, "groups");
+            return services.projectService;
         case ResourceKind.COLLECTION:
-            return new CommonResourceService(apiClient, "collections");
+            return services.collectionService;
         default:
-            return new CommonResourceService(apiClient, "");
+            return services.projectService;
     }
 };
 
diff --git a/src/store/store.ts b/src/store/store.ts
index cf07d6d..2154b3b 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { createStore, applyMiddleware, compose, Middleware, combineReducers } from 'redux';
+import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
 import { routerMiddleware, routerReducer, RouterState } from "react-router-redux";
 import thunkMiddleware from 'redux-thunk';
 import { History } from "history";
@@ -37,7 +37,9 @@ export interface RootState {
     snackbar: SnackbarState;
 }
 
-export function configureStore(history: History, services: ServiceRepository) {
+export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
+
+export function configureStore(history: History, services: ServiceRepository): RootStore {
     const rootReducer = combineReducers({
         auth: authReducer(services),
         projects: projectsReducer,
diff --git a/src/views-components/api-token/api-token.tsx b/src/views-components/api-token/api-token.tsx
index 51e3ad1..0ae41c6 100644
--- a/src/views-components/api-token/api-token.tsx
+++ b/src/views-components/api-token/api-token.tsx
@@ -5,7 +5,7 @@
 import { Redirect, RouteProps } from "react-router";
 import * as React from "react";
 import { connect, DispatchProp } from "react-redux";
-import { authActions, getUserDetails } from "../../store/auth/auth-action";
+import { getUserDetails, saveApiToken } from "../../store/auth/auth-action";
 import { getProjectList } from "../../store/project/project-action";
 import { getUrlParameter } from "../../common/url";
 import { AuthService } from "../../services/auth-service/auth-service";
@@ -19,7 +19,7 @@ export const ApiToken = connect()(
         componentDidMount() {
             const search = this.props.location ? this.props.location.search : "";
             const apiToken = getUrlParameter(search, 'api_token');
-            this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
+            this.props.dispatch(saveApiToken(apiToken));
             this.props.dispatch<any>(getUserDetails()).then(() => {
                 const rootUuid = this.props.authService.getRootUuid();
                 this.props.dispatch(getProjectList(rootUuid));
diff --git a/src/views/workbench/workbench.test.tsx b/src/views/workbench/workbench.test.tsx
index 48ea2de..8e0b353 100644
--- a/src/views/workbench/workbench.test.tsx
+++ b/src/views/workbench/workbench.test.tsx
@@ -17,7 +17,7 @@ const history = createBrowserHistory();
 
 it('renders without crashing', () => {
     const div = document.createElement('div');
-    const store = configureStore(createBrowserHistory(), createServices());
+    const store = configureStore(createBrowserHistory(), createServices("/arvados/v1"));
     ReactDOM.render(
         <MuiThemeProvider theme={CustomTheme}>
             <Provider store={store}>
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index b0fe4b3..aa27adb 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -7,7 +7,7 @@ import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/st
 import Drawer from '@material-ui/core/Drawer';
 import { connect, DispatchProp } from "react-redux";
 import { Route, Switch, RouteComponentProps } from "react-router";
-import { authActions } from "../../store/auth/auth-action";
+import { authActions, login, logout } from "../../store/auth/auth-action";
 import { User } from "../../models/user";
 import { RootState } from "../../store/store";
 import { MainAppBar, MainAppBarActionProps, MainAppBarMenuItem } from '../../views-components/main-app-bar/main-app-bar';
@@ -139,7 +139,7 @@ export const Workbench = withStyles(styles)(
                         },
                         {
                             label: "Logout",
-                            action: () => this.props.dispatch(authActions.LOGOUT())
+                            action: () => this.props.dispatch<any>(logout())
                         },
                         {
                             label: "My account",
@@ -155,7 +155,7 @@ export const Workbench = withStyles(styles)(
                     anonymousMenu: [
                         {
                             label: "Sign in",
-                            action: () => this.props.dispatch(authActions.LOGIN())
+                            action: () => this.props.dispatch<any>(login())
                         }
                     ]
                 }

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list