[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