From 7e58fd10a7abc133b32a5084796c2b061e03ecd8 Mon Sep 17 00:00:00 2001 From: Daniel Kos Date: Wed, 6 Jun 2018 08:14:11 +0200 Subject: [PATCH] Replace typesafe-actions with unionize, add fetching user details action Feature #13563 Arvados-DCO-1.1-Signed-off-by: Daniel Kos : --- package.json | 7 ++-- src/common/actions.ts | 3 ++ src/common/server-api.ts | 7 ++-- src/components/api-token/api-token.tsx | 18 +++++---- src/components/tree/tree.tsx | 2 +- src/index.tsx | 1 + src/models/user.ts | 4 +- src/services/auth-service/auth-service.ts | 2 +- src/store/auth-action.ts | 42 +++++++++++++++++---- src/store/auth-reducer.ts | 45 +++++++++++++---------- src/store/project-action.ts | 15 +++++--- src/store/project-reducer.ts | 15 +++----- src/store/root-reducer.ts | 15 +++++--- src/store/store.ts | 4 +- src/views/workbench/workbench.tsx | 16 +++----- yarn.lock | 12 ++++-- 16 files changed, 126 insertions(+), 82 deletions(-) create mode 100644 src/common/actions.ts diff --git a/package.json b/package.json index 9f4197ca..9a31c4e7 100644 --- a/package.json +++ b/package.json @@ -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": { "^~/(.*)$": "/src/$1" diff --git a/src/common/actions.ts b/src/common/actions.ts new file mode 100644 index 00000000..6a5f4107 --- /dev/null +++ b/src/common/actions.ts @@ -0,0 +1,3 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 diff --git a/src/common/server-api.ts b/src/common/server-api.ts index 9078b73d..2e676dde 100644 --- a/src/common/server-api.ts +++ b/src/common/server-api.ts @@ -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; diff --git a/src/components/api-token/api-token.tsx b/src/components/api-token/api-token.tsx index 91ef4e9f..bc0caf56 100644 --- a/src/components/api-token/api-token.tsx +++ b/src/components/api-token/api-token.tsx @@ -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 { +class ApiToken extends React.Component, {}> { 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 { 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 } } -export default connect(null, { - saveApiToken: authActions.saveApiToken -})(ApiToken); +export default connect()(ApiToken); diff --git a/src/components/tree/tree.tsx b/src/components/tree/tree.tsx index 0c99db8a..12369d57 100644 --- a/src/components/tree/tree.tsx +++ b/src/components/tree/tree.tsx @@ -15,7 +15,7 @@ interface TreeProps { class Tree extends React.Component, {}> { render() { return - {this.props.items.map((it: T, idx: number) => + {this.props.items && this.props.items.map((it: T, idx: number) => {this.props.render(it)} diff --git a/src/index.tsx b/src/index.tsx index 89354703..ae5395f5 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -25,6 +25,7 @@ const store = configureStore({ location: null }, auth: { + user: undefined } }, history); diff --git a/src/models/user.ts b/src/models/user.ts index 514402d8..f1780d5d 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -4,6 +4,6 @@ export interface User { email: string; - apiToken: string; - apiHost: string; + firstName: string; + lastName: string; } diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts index 672a6194..cedf3c62 100644 --- a/src/services/auth-service/auth-service.ts +++ b/src/services/auth-service/auth-service.ts @@ -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'; diff --git a/src/store/auth-action.ts b/src/store/auth-action.ts index 02fdcac2..719cb81b 100644 --- a/src/store/auth-action.ts +++ b/src/store/auth-action.ts @@ -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')(), - 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(), + LOGIN: {}, + LOGOUT: {}, + USER_DETAILS_REQUEST: {}, + USER_DETAILS_SUCCESS: ofType() +}, { + tag: 'type', + value: 'payload' +}); -export type AuthAction = ActionType; +export type AuthAction = UnionOf; export default actions; + +export const getUserDetails = () => (dispatch: Dispatch) => { + dispatch(actions.USER_DETAILS_REQUEST()); + serverApi + .get('/users/current') + .then(resp => { + dispatch(actions.USER_DETAILS_SUCCESS(resp.data)); + }) + // .catch(err => { + // }); +}; + + diff --git a/src/store/auth-reducer.ts b/src/store/auth-reducer.ts index 8e9eb4f7..0d1f730e 100644 --- a/src/store/auth-reducer.ts +++ b/src/store/auth-reducer.ts @@ -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; diff --git a/src/store/project-action.ts b/src/store/project-action.ts index 568de2b8..904a34d4 100644 --- a/src/store/project-action.ts +++ b/src/store/project-action.ts @@ -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')(), - removeProject: createStandardAction('@@project/remove')() -}; +const actions = unionize({ + CREATE_PROJECT: ofType(), + REMOVE_PROJECT: ofType() +}, { + tag: 'type', + value: 'payload' +}); -export type ProjectAction = ActionType; +export type ProjectAction = UnionOf; export default actions; diff --git a/src/store/project-reducer.ts b/src/store/project-reducer.ts index 1614ed9a..a321265c 100644 --- a/src/store/project-reducer.ts +++ b/src/store/project-reducer.ts @@ -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; diff --git a/src/store/root-reducer.ts b/src/store/root-reducer.ts index b514ecf6..380bd534 100644 --- a/src/store/root-reducer.ts +++ b/src/store/root-reducer.ts @@ -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; - export default rootReducer; diff --git a/src/store/store.ts b/src/store/store.ts index 975debe8..d3646e69 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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); diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 3e223852..8f0562bb 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -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; +type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles; interface WorkbenchState { anchorEl: any; @@ -80,12 +79,12 @@ class Workbench extends React.Component { } 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) => { @@ -166,10 +165,7 @@ class Workbench extends React.Component { export default connect( (state: RootState) => ({ projects: state.projects - }), { - login: authActions.login, - logout: authActions.logout - } + }) )( withStyles(styles)(Workbench) ); diff --git a/yarn.lock b/yarn.lock index 8b715297..eace7b63 100644 --- 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" -- 2.30.2