Replace typesafe-actions with unionize, add fetching user details action
authorDaniel Kos <daniel.kos@contractors.roche.com>
Wed, 6 Jun 2018 06:14:11 +0000 (08:14 +0200)
committerDaniel Kos <daniel.kos@contractors.roche.com>
Wed, 6 Jun 2018 06:15:05 +0000 (08:15 +0200)
Feature #13563

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

16 files changed:
package.json
src/common/actions.ts [new file with mode: 0644]
src/common/server-api.ts
src/components/api-token/api-token.tsx
src/components/tree/tree.tsx
src/index.tsx
src/models/user.ts
src/services/auth-service/auth-service.ts
src/store/auth-action.ts
src/store/auth-reducer.ts
src/store/project-action.ts
src/store/project-reducer.ts
src/store/root-reducer.ts
src/store/store.ts
src/views/workbench/workbench.tsx
yarn.lock

index 9f4197cabf8ee81cf894a2dc11f1e7f400a54053..9a31c4e7fddfa5169574cf2cf0f41bdf85393211 100644 (file)
@@ -14,8 +14,8 @@
     "react-router-redux": "5.0.0-alpha.9",
     "react-scripts-ts": "2.16.0",
     "redux": "4.0.0",
-    "redux-devtools": "3.4.1",
-    "typesafe-actions": "2.0.4"
+    "redux-thunk": "2.3.0",
+    "unionize": "2.1.2"
   },
   "scripts": {
     "start": "react-scripts-ts start",
@@ -34,7 +34,8 @@
     "@types/react-router-dom": "4.2.7",
     "@types/react-router-redux": "5.0.15",
     "@types/redux-devtools": "3.0.44",
-    "typescript": "2.9.1"
+    "typescript": "2.9.1",
+    "redux-devtools": "3.4.1"
   },
   "moduleNameMapper": {
     "^~/(.*)$": "<rootDir>/src/$1"
diff --git a/src/common/actions.ts b/src/common/actions.ts
new file mode 100644 (file)
index 0000000..6a5f410
--- /dev/null
@@ -0,0 +1,3 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
index 9078b73db39fba63417da93edbefe572b7304261..2e676dde5c4a5ae76aa5ed4a71319d5eb1740351 100644 (file)
@@ -4,17 +4,16 @@
 
 import Axios, { AxiosInstance } from "axios";
 
-export const API_HOST = 'https://qr1hi.arvadosapi.com/arvados/v1';
+export const API_HOST = 'https://qr1hi.arvadosapi.com';
 
 export const serverApi: AxiosInstance = Axios.create({
-    baseURL: API_HOST
+    baseURL: API_HOST + '/arvados/v1'
 });
 
 export function setServerApiAuthorizationHeader(token: string) {
     serverApi.defaults.headers.common = {
         'Authorization': `OAuth2 ${token}`
-    };
-}
+    };}
 
 export function removeServerApiAuthorizationHeader() {
     delete serverApi.defaults.headers.common.Authorization;
index 91ef4e9f2c18d60475a1f5f2e34d085cfa54a3cf..bc0caf56dff4e976a2952e185c3cf766f607001a 100644 (file)
@@ -1,13 +1,16 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
 import { Redirect, RouteProps } from "react-router";
 import * as React from "react";
-import { connect } from "react-redux";
-import authActions from "../../store/auth-action";
+import { connect, DispatchProp } from "react-redux";
+import authActions, { getUserDetails } from "../../store/auth-action";
 
 interface ApiTokenProps {
-    saveApiToken: (token: string) => void;
 }
 
-class ApiToken extends React.Component<ApiTokenProps & RouteProps, {}> {
+class ApiToken extends React.Component<ApiTokenProps & RouteProps & DispatchProp<any>, {}> {
     static getUrlParameter(search: string, name: string) {
         const safeName = name.replace(/[\[]/, '\\[').replace(/[\]]/, '\\]');
         const regex = new RegExp('[\\?&]' + safeName + '=([^&#]*)');
@@ -18,13 +21,12 @@ class ApiToken extends React.Component<ApiTokenProps & RouteProps, {}> {
     componentDidMount() {
         const search = this.props.location ? this.props.location.search : "";
         const apiToken = ApiToken.getUrlParameter(search, 'api_token');
-        this.props.saveApiToken(apiToken);
+        this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken));
+        this.props.dispatch(getUserDetails());
     }
     render() {
         return <Redirect to="/"/>
     }
 }
 
-export default connect<ApiTokenProps>(null, {
-    saveApiToken: authActions.saveApiToken
-})(ApiToken);
+export default connect()(ApiToken);
index 0c99db8a50f5a4c081aba21cd81df709267ca5d8..12369d57e12b1415624839a8244dbe95bb7442d6 100644 (file)
@@ -15,7 +15,7 @@ interface TreeProps<T> {
 class Tree<T> extends React.Component<TreeProps<T>, {}> {
     render() {
         return <List>
-            {this.props.items.map((it: T, idx: number) =>
+            {this.props.items && this.props.items.map((it: T, idx: number) =>
                 <ListItem key={`item/${idx}`} button>
                     {this.props.render(it)}
                 </ListItem>
index 8935470323ee0bb388e687c535ef9a61b2c44852..ae5395f5de9a2b1d1e832d796229330bba2d010b 100644 (file)
@@ -25,6 +25,7 @@ const store = configureStore({
         location: null
     },
     auth: {
+        user: undefined
     }
 }, history);
 
index 514402d87e32f4245efc050d7ee142b77b8c2717..f1780d5db0cc96414c9c6435f17c10554a62ef31 100644 (file)
@@ -4,6 +4,6 @@
 
 export interface User {
     email: string;
-    apiToken: string;
-    apiHost: string;
+    firstName: string;
+    lastName: string;
 }
index 672a619410ba9c57274ab55605243e5af68c5ec0..cedf3c6215222d1db0f82d1209e1d0f239d22a73 100644 (file)
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import Axios from "axios";
-import { API_HOST, serverApi } from "../../common/server-api";
+import { API_HOST } from "../../common/server-api";
 
 const API_TOKEN_KEY = 'api_token';
 
index 02fdcac20215cb0c57181db6fc29e07f0f2b39fb..719cb81be7469893c2bb907196fdaca78dd782f7 100644 (file)
@@ -2,14 +2,40 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ActionType, createStandardAction } from "typesafe-actions";
+import { serverApi } from "../common/server-api";
+import { ofType, default as unionize, UnionOf } from "unionize";
+import { Dispatch } from "redux";
 
-const actions = {
-    saveApiToken: createStandardAction('@@auth/saveApiToken')<string>(),
-    getUserTokenDetails: createStandardAction('@@auth/userTokenDetails')(),
-    login: createStandardAction('@@auth/login')(),
-    logout: createStandardAction('@@auth/logout')()
-};
+export interface UserDetailsResponse {
+    email: string;
+    first_name: string;
+    last_name: string;
+    is_admin: boolean;
+}
+
+const actions = unionize({
+    SAVE_API_TOKEN: ofType<string>(),
+    LOGIN: {},
+    LOGOUT: {},
+    USER_DETAILS_REQUEST: {},
+    USER_DETAILS_SUCCESS: ofType<UserDetailsResponse>()
+}, {
+    tag: 'type',
+    value: 'payload'
+});
 
-export type AuthAction = ActionType<typeof actions>;
+export type AuthAction = UnionOf<typeof actions>;
 export default actions;
+
+export const getUserDetails = () => (dispatch: Dispatch) => {
+    dispatch(actions.USER_DETAILS_REQUEST());
+    serverApi
+        .get<UserDetailsResponse>('/users/current')
+        .then(resp => {
+            dispatch(actions.USER_DETAILS_SUCCESS(resp.data));
+        })
+        // .catch(err => {
+        // });
+};
+
+
index 8e9eb4f7ba3a05169e60fe76fb2db222e7f02f6a..0d1f730e0db1ad7eff335b6072d923980b5a3ef0 100644 (file)
@@ -2,35 +2,42 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { getType } from "typesafe-actions";
-import actions, { AuthAction } from "./auth-action";
+import actions, { AuthAction, UserDetailsResponse } from "./auth-action";
 import { User } from "../models/user";
 import { authService } from "../services/services";
-import { removeServerApiAuthorizationHeader, serverApi, setServerApiAuthorizationHeader } from "../common/server-api";
+import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../common/server-api";
 
-type AuthState = User | {};
+export interface AuthState {
+    user?: User;
+    apiToken?: string;
+};
 
 const authReducer = (state: AuthState = {}, action: AuthAction) => {
-    switch (action.type) {
-        case getType(actions.saveApiToken): {
-            authService.saveApiToken(action.payload);
-            setServerApiAuthorizationHeader(action.payload);
-            serverApi.get('/users/current');
-            return {...state, apiToken: action.payload};
-        }
-        case getType(actions.login): {
+    return actions.match(action, {
+        SAVE_API_TOKEN: (token: string) => {
+            authService.saveApiToken(token);
+            setServerApiAuthorizationHeader(token);
+            return {...state, apiToken: token};
+        },
+        LOGIN: () => {
             authService.login();
             return state;
-        }
-        case getType(actions.logout): {
+        },
+        LOGOUT: () => {
             authService.removeApiToken();
             removeServerApiAuthorizationHeader();
             authService.logout();
-            return {...state, apiToken: null };
-        }
-        default:
-            return state;
-    }
+            return {...state, apiToken: undefined};
+        },
+        USER_DETAILS_SUCCESS: (ud: UserDetailsResponse) => {
+            return {...state, user: {
+                email: ud.email,
+                firstName: ud.first_name,
+                lastName: ud.last_name
+            }}
+        },
+        default: () => state
+    });
 };
 
 export default authReducer;
index 568de2b8e22422080488a779250cfd24ada034ec..904a34d4f09498319bd78289c9be308bac2c8d5e 100644 (file)
@@ -2,13 +2,16 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { ActionType, createStandardAction } from "typesafe-actions";
 import { Project } from "../models/project";
+import { default as unionize, ofType, UnionOf } from "unionize";
 
-const actions = {
-    createProject: createStandardAction('@@project/create')<Project>(),
-    removeProject: createStandardAction('@@project/remove')<string>()
-};
+const actions = unionize({
+    CREATE_PROJECT: ofType<Project>(),
+    REMOVE_PROJECT: ofType<string>()
+}, {
+    tag: 'type',
+    value: 'payload'
+});
 
-export type ProjectAction = ActionType<typeof actions>;
+export type ProjectAction = UnionOf<typeof actions>;
 export default actions;
index 1614ed9a29eeff2c9d8be455d4da12f4f78635cd..a321265c619e26c22564216c2c7eeb16add908bc 100644 (file)
@@ -2,20 +2,17 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { getType } from "typesafe-actions";
 import { Project } from "../models/project";
 import actions, { ProjectAction } from "./project-action";
 
-type ProjectState = Project[];
+export type ProjectState = Project[];
 
 const projectsReducer = (state: ProjectState = [], action: ProjectAction) => {
-    switch (action.type) {
-        case getType(actions.createProject): {
-            return [...state, action.payload];
-        }
-        default:
-            return state;
-    }
+    return actions.match(action, {
+        CREATE_PROJECT: (project) => [...state, project],
+        REMOVE_PROJECT: () => state,
+        default: () => state
+    });
 };
 
 export default projectsReducer;
index b514ecf6e9fc0fcebd757b04c2bc98c1adc35ec9..380bd5349dd83d59322d9f4bdae58be29e9bc3f9 100644 (file)
@@ -3,10 +3,15 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { combineReducers } from "redux";
-import { StateType } from "typesafe-actions";
-import { routerReducer } from "react-router-redux";
-import authReducer from "./auth-reducer";
-import projectsReducer from "./project-reducer";
+import { routerReducer, RouterState } from "react-router-redux";
+import authReducer, { AuthState } from "./auth-reducer";
+import projectsReducer, { ProjectState } from "./project-reducer";
+
+export interface RootState {
+    auth: AuthState,
+    projects: ProjectState,
+    router: RouterState
+}
 
 const rootReducer = combineReducers({
     auth: authReducer,
@@ -14,6 +19,4 @@ const rootReducer = combineReducers({
     router: routerReducer
 });
 
-export type RootState = StateType<typeof rootReducer>;
-
 export default rootReducer;
index 975debe816ce4944bf67aa338b5d80a6c6c4f178..d3646e69155b8384fe756effcf976d4b2b59d8c9 100644 (file)
@@ -5,6 +5,7 @@
 import { createStore, applyMiddleware, compose, Middleware } from 'redux';
 import { default as rootReducer, RootState } from "./root-reducer";
 import { routerMiddleware } from "react-router-redux";
+import thunkMiddleware from 'redux-thunk';
 import { History } from "history";
 
 const composeEnhancers =
@@ -14,7 +15,8 @@ const composeEnhancers =
 
 export default function configureStore(initialState: RootState, history: History) {
     const middlewares: Middleware[] = [
-        routerMiddleware(history)
+        routerMiddleware(history),
+        thunkMiddleware
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, initialState!, enhancer);
index 3e22385224b5c0c70610d1f923b01e196f1aa8d0..8f0562bbb918ab988e583354be6ccae32c42dad2 100644 (file)
@@ -9,7 +9,7 @@ import Drawer from '@material-ui/core/Drawer';
 import AppBar from '@material-ui/core/AppBar';
 import Toolbar from '@material-ui/core/Toolbar';
 import Typography from '@material-ui/core/Typography';
-import { connect } from "react-redux";
+import { connect, DispatchProp } from "react-redux";
 import Tree from "../../components/tree/tree";
 import { Project } from "../../models/project";
 import { RootState } from "../../store/root-reducer";
@@ -23,6 +23,7 @@ import IconButton from "@material-ui/core/IconButton/IconButton";
 import Menu from "@material-ui/core/Menu/Menu";
 import MenuItem from "@material-ui/core/MenuItem/MenuItem";
 import { AccountCircle } from "@material-ui/icons";
+import { AnyAction } from "redux";
 
 const drawerWidth = 240;
 
@@ -61,11 +62,9 @@ interface WorkbenchDataProps {
 }
 
 interface WorkbenchActionProps {
-    login?: () => void;
-    logout?: () => void;
 }
 
-type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & WithStyles<CssRules>;
+type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles<CssRules>;
 
 interface WorkbenchState {
     anchorEl: any;
@@ -80,12 +79,12 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
     }
 
     login = () => {
-        this.props.login!();
+        this.props.dispatch(authActions.LOGIN() as AnyAction);
     };
 
     logout = () => {
         this.handleClose();
-        this.props.logout!();
+        this.props.dispatch(authActions.LOGOUT() as AnyAction);
     };
 
     handleOpenMenu = (event: React.MouseEvent<any>) => {
@@ -166,10 +165,7 @@ class Workbench extends React.Component<WorkbenchProps, WorkbenchState> {
 export default connect<WorkbenchDataProps>(
     (state: RootState) => ({
         projects: state.projects
-    }), {
-        login: authActions.login,
-        logout: authActions.logout
-    }
+    })
 )(
     withStyles(styles)(Workbench)
 );
index 8b715297901a6de1f44a18b2ade9f8b9c5c1639b..eace7b6352ac8996fe125e3ad832cc256db04acb 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -6140,6 +6140,10 @@ redux-devtools@3.4.1:
     prop-types "^15.5.7"
     redux-devtools-instrument "^1.0.1"
 
+redux-thunk@^2.3.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/redux-thunk/-/redux-thunk-2.3.0.tgz#51c2c19a185ed5187aaa9a2d08b666d0d6467622"
+
 redux@4.0.0, "redux@>= 3.7.2", redux@^4.0.0:
   version "4.0.0"
   resolved "https://registry.yarnpkg.com/redux/-/redux-4.0.0.tgz#aa698a92b729315d22b34a0553d7e6533555cc03"
@@ -7248,10 +7252,6 @@ typedarray@^0.0.6:
   version "0.0.6"
   resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
 
-typesafe-actions@2.0.4:
-  version "2.0.4"
-  resolved "https://registry.yarnpkg.com/typesafe-actions/-/typesafe-actions-2.0.4.tgz#31c8f8df3566d549eb52edb64a75997e970c17d0"
-
 typescript@2.9.1:
   version "2.9.1"
   resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.1.tgz#fdb19d2c67a15d11995fd15640e373e09ab09961"
@@ -7324,6 +7324,10 @@ union-value@^1.0.0:
     is-extendable "^0.1.1"
     set-value "^0.4.3"
 
+unionize@^2.1.2:
+  version "2.1.2"
+  resolved "https://registry.yarnpkg.com/unionize/-/unionize-2.1.2.tgz#2513b148de515bec93f045d1685bd88eab62b608"
+
 uniq@^1.0.1:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/uniq/-/uniq-1.0.1.tgz#b31c5ae8254844a3a8281541ce2b04b865a734ff"