From f239b9e88677d82a48b6b565ab2fd407d1171729 Mon Sep 17 00:00:00 2001 From: Daniel Kos Date: Mon, 30 Jul 2018 00:47:41 +0200 Subject: [PATCH] Introduce service repository Feature #13901 Arvados-DCO-1.1-Signed-off-by: Daniel Kos --- src/index.tsx | 11 +++--- src/services/services.ts | 28 ++++++++++++--- src/store/auth/auth-action.ts | 7 ++-- src/store/auth/auth-reducer.test.ts | 20 ++++++----- src/store/auth/auth-reducer.ts | 20 +++++------ .../favorite-panel-middleware.ts | 6 ++-- src/store/favorites/favorites-actions.ts | 14 ++++---- .../project-panel/project-panel-middleware.ts | 6 ++-- src/store/project/project-action.ts | 10 +++--- src/store/store.ts | 34 +++++++++---------- src/views-components/api-token/api-token.tsx | 5 +-- src/views/workbench/workbench.test.tsx | 3 +- src/views/workbench/workbench.tsx | 10 ++++-- 13 files changed, 103 insertions(+), 71 deletions(-) diff --git a/src/index.tsx b/src/index.tsx index 77d5763b..4b2e3359 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -13,7 +13,7 @@ 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 { authService } from "./services/services"; +import { createServices } from "./services/services"; import { getProjectList } from "./store/project/project-action"; import { MuiThemeProvider } from '@material-ui/core/styles'; import { CustomTheme } from './common/custom-theme'; @@ -36,10 +36,13 @@ fetchConfig() setBaseUrl(config.API_HOST); const history = createBrowserHistory(); - const store = configureStore(history); + const services = createServices(); + const store = configureStore(history, services); store.dispatch(authActions.INIT()); - store.dispatch(getProjectList(authService.getUuid())); + store.dispatch(getProjectList(services.authService.getUuid())); + + const Token = (props: any) => ; const App = () => @@ -47,7 +50,7 @@ fetchConfig()
- +
diff --git a/src/services/services.ts b/src/services/services.ts index a08ed3cb..55ab8365 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -9,8 +9,26 @@ import { ProjectService } from "./project-service/project-service"; import { LinkService } from "./link-service/link-service"; import { FavoriteService } from "./favorite-service/favorite-service"; -export const authService = new AuthService(authClient, apiClient); -export const groupsService = new GroupsService(apiClient); -export const projectService = new ProjectService(apiClient); -export const linkService = new LinkService(apiClient); -export const favoriteService = new FavoriteService(linkService, groupsService); +export interface ServiceRepository { + authService: AuthService; + groupsService: GroupsService; + projectService: ProjectService; + linkService: LinkService; + favoriteService: FavoriteService; +} + +export const createServices = (): ServiceRepository => { + 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); + + return { + authService, + groupsService, + projectService, + linkService, + favoriteService + }; +}; diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts index e9930a02..8b268cce 100644 --- a/src/store/auth/auth-action.ts +++ b/src/store/auth/auth-action.ts @@ -4,8 +4,9 @@ import { ofType, default as unionize, UnionOf } from "unionize"; import { Dispatch } from "redux"; -import { authService } from "../../services/services"; import { User } from "../../models/user"; +import { RootState } from "../store"; +import { ServiceRepository } from "../../services/services"; export const authActions = unionize({ SAVE_API_TOKEN: ofType(), @@ -19,9 +20,9 @@ export const authActions = unionize({ value: 'payload' }); -export const getUserDetails = () => (dispatch: Dispatch): Promise => { +export const getUserDetails = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise => { dispatch(authActions.USER_DETAILS_REQUEST()); - return authService.getUserDetails().then(details => { + return services.authService.getUserDetails().then(details => { dispatch(authActions.USER_DETAILS_SUCCESS(details)); return details; }); diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts index 778b500d..f686db5e 100644 --- a/src/store/auth/auth-reducer.test.ts +++ b/src/store/auth/auth-reducer.test.ts @@ -2,8 +2,8 @@ // // SPDX-License-Identifier: AGPL-3.0 -import { authReducer } from "./auth-reducer"; -import { authActions } from "./auth-action"; +import { authReducer, AuthState } from "./auth-reducer"; +import { AuthAction, authActions } from "./auth-action"; import { API_TOKEN_KEY, USER_EMAIL_KEY, @@ -14,15 +14,19 @@ import { } from "../../services/auth-service/auth-service"; import 'jest-localstorage-mock'; +import { createServices } from "../../services/services"; describe('auth-reducer', () => { + let reducer: (state: AuthState | undefined, action: AuthAction) => any; + beforeAll(() => { localStorage.clear(); + reducer = authReducer(createServices()); }); it('should return default state on initialisation', () => { const initialState = undefined; - const state = authReducer(initialState, authActions.INIT()); + const state = reducer(initialState, authActions.INIT()); expect(state).toEqual({ apiToken: undefined, user: undefined @@ -39,7 +43,7 @@ describe('auth-reducer', () => { localStorage.setItem(USER_UUID_KEY, "uuid"); localStorage.setItem(USER_OWNER_UUID_KEY, "ownerUuid"); - const state = authReducer(initialState, authActions.INIT()); + const state = reducer(initialState, authActions.INIT()); expect(state).toEqual({ apiToken: "token", user: { @@ -55,7 +59,7 @@ describe('auth-reducer', () => { it('should store token in local storage', () => { const initialState = undefined; - const state = authReducer(initialState, authActions.SAVE_API_TOKEN("token")); + const state = reducer(initialState, authActions.SAVE_API_TOKEN("token")); expect(state).toEqual({ apiToken: "token", user: undefined @@ -75,7 +79,7 @@ describe('auth-reducer', () => { ownerUuid: "ownerUuid" }; - const state = authReducer(initialState, authActions.USER_DETAILS_SUCCESS(user)); + const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user)); expect(state).toEqual({ apiToken: undefined, user: { @@ -93,7 +97,7 @@ describe('auth-reducer', () => { it('should fire external url to login', () => { const initialState = undefined; window.location.assign = jest.fn(); - authReducer(initialState, authActions.LOGIN()); + reducer(initialState, authActions.LOGIN()); expect(window.location.assign).toBeCalledWith( `/login?return_to=${window.location.protocol}//${window.location.host}/token` ); @@ -102,7 +106,7 @@ describe('auth-reducer', () => { it('should fire external url to logout', () => { const initialState = undefined; window.location.assign = jest.fn(); - authReducer(initialState, authActions.LOGOUT()); + 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 366385d5..e3f968a8 100644 --- a/src/store/auth/auth-reducer.ts +++ b/src/store/auth/auth-reducer.ts @@ -4,7 +4,7 @@ import { authActions, AuthAction } from "./auth-action"; import { User } from "../../models/user"; -import { authService } from "../../services/services"; +import { ServiceRepository } from "../../services/services"; import { removeServerApiAuthorizationHeader, setServerApiAuthorizationHeader } from "../../common/api/server-api"; export interface AuthState { @@ -12,34 +12,34 @@ export interface AuthState { apiToken?: string; } -export const authReducer = (state: AuthState = {}, action: AuthAction) => { +export const authReducer = (services: ServiceRepository) => (state: AuthState = {}, action: AuthAction) => { return authActions.match(action, { SAVE_API_TOKEN: (token: string) => { - authService.saveApiToken(token); + services.authService.saveApiToken(token); setServerApiAuthorizationHeader(token); return {...state, apiToken: token}; }, INIT: () => { - const user = authService.getUser(); - const token = authService.getApiToken(); + const user = services.authService.getUser(); + const token = services.authService.getApiToken(); if (token) { setServerApiAuthorizationHeader(token); } return {user, apiToken: token}; }, LOGIN: () => { - authService.login(); + services.authService.login(); return state; }, LOGOUT: () => { - authService.removeApiToken(); - authService.removeUser(); + services.authService.removeApiToken(); + services.authService.removeUser(); removeServerApiAuthorizationHeader(); - authService.logout(); + services.authService.logout(); return {...state, apiToken: undefined}; }, USER_DETAILS_SUCCESS: (user: User) => { - authService.saveUser(user); + services.authService.saveUser(user); return {...state, user}; }, default: () => state diff --git a/src/store/favorite-panel/favorite-panel-middleware.ts b/src/store/favorite-panel/favorite-panel-middleware.ts index 548a1178..62e93c61 100644 --- a/src/store/favorite-panel/favorite-panel-middleware.ts +++ b/src/store/favorite-panel/favorite-panel-middleware.ts @@ -4,7 +4,6 @@ import { Middleware } from "redux"; import { dataExplorerActions } from "../data-explorer/data-explorer-action"; -import { favoriteService } from "../../services/services"; import { RootState } from "../store"; import { getDataExplorer } from "../data-explorer/data-explorer-reducer"; import { FilterBuilder } from "../../common/api/filter-builder"; @@ -22,8 +21,9 @@ import { OrderBuilder } from "../../common/api/order-builder"; import { SortDirection } from "../../components/data-table/data-column"; import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service"; import { FavoriteOrderBuilder } from "../../services/favorite-service/favorite-order-builder"; +import { ServiceRepository } from "../../services/services"; -export const favoritePanelMiddleware: Middleware = store => next => { +export const favoritePanelMiddleware = (services: ServiceRepository): Middleware => store => next => { next(dataExplorerActions.SET_COLUMNS({ id: FAVORITE_PANEL_ID, columns })); return action => { @@ -62,7 +62,7 @@ export const favoritePanelMiddleware: Middleware = store => next => { const typeFilters = getColumnFilters(columns, FavoritePanelColumnNames.TYPE); const order = FavoriteOrderBuilder.create(); if (typeFilters.length > 0) { - favoriteService + services.favoriteService .list(state.projects.currentItemId, { limit: dataExplorer.rowsPerPage, offset: dataExplorer.page * dataExplorer.rowsPerPage, diff --git a/src/store/favorites/favorites-actions.ts b/src/store/favorites/favorites-actions.ts index eb4f6490..38229dff 100644 --- a/src/store/favorites/favorites-actions.ts +++ b/src/store/favorites/favorites-actions.ts @@ -4,10 +4,10 @@ import { unionize, ofType, UnionOf } from "unionize"; import { Dispatch } from "redux"; -import { favoriteService } from "../../services/services"; import { RootState } from "../store"; import { checkFavorite } from "./favorites-reducer"; import { snackbarActions } from "../snackbar/snackbar-actions"; +import { ServiceRepository } from "../../services/services"; export const favoritesActions = unionize({ TOGGLE_FAVORITE: ofType<{ resourceUuid: string }>(), @@ -18,14 +18,14 @@ export const favoritesActions = unionize({ export type FavoritesAction = UnionOf; export const toggleFavorite = (resource: { uuid: string; name: string }) => - (dispatch: Dispatch, getState: () => RootState): Promise => { + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise => { 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); const promise: any = isFavorite - ? favoriteService.delete({ userUuid, resourceUuid: resource.uuid }) - : favoriteService.create({ userUuid, resource }); + ? services.favoriteService.delete({ userUuid, resourceUuid: resource.uuid }) + : services.favoriteService.create({ userUuid, resource }); return promise .then(() => { @@ -41,12 +41,12 @@ export const toggleFavorite = (resource: { uuid: string; name: string }) => }; export const checkPresenceInFavorites = (resourceUuids: string[]) => - (dispatch: Dispatch, getState: () => RootState) => { + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const userUuid = getState().auth.user!.uuid; dispatch(favoritesActions.CHECK_PRESENCE_IN_FAVORITES(resourceUuids)); - favoriteService + services.favoriteService .checkPresenceInFavorites(userUuid, resourceUuids) - .then(results => { + .then((results: any) => { dispatch(favoritesActions.UPDATE_FAVORITES(results)); }); }; diff --git a/src/store/project-panel/project-panel-middleware.ts b/src/store/project-panel/project-panel-middleware.ts index b7ab03ce..e984e068 100644 --- a/src/store/project-panel/project-panel-middleware.ts +++ b/src/store/project-panel/project-panel-middleware.ts @@ -5,7 +5,6 @@ import { Middleware } from "redux"; import { dataExplorerActions } from "../data-explorer/data-explorer-action"; import { PROJECT_PANEL_ID, columns, ProjectPanelFilter, ProjectPanelColumnNames } from "../../views/project-panel/project-panel"; -import { groupsService } from "../../services/services"; import { RootState } from "../store"; import { getDataExplorer } from "../data-explorer/data-explorer-reducer"; import { resourceToDataItem, ProjectPanelItem } from "../../views/project-panel/project-panel-item"; @@ -16,8 +15,9 @@ import { OrderBuilder } from "../../common/api/order-builder"; import { GroupContentsResource, GroupContentsResourcePrefix } from "../../services/groups-service/groups-service"; import { SortDirection } from "../../components/data-table/data-column"; import { checkPresenceInFavorites } from "../favorites/favorites-actions"; +import { ServiceRepository } from "../../services/services"; -export const projectPanelMiddleware: Middleware = store => next => { +export const projectPanelMiddleware = (services: ServiceRepository): Middleware => store => next => { next(dataExplorerActions.SET_COLUMNS({ id: PROJECT_PANEL_ID, columns })); return action => { @@ -57,7 +57,7 @@ export const projectPanelMiddleware: Middleware = store => next => { const sortColumn = dataExplorer.columns.find(({ sortDirection }) => Boolean(sortDirection && sortDirection !== "none")); const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC ? SortDirection.ASC : SortDirection.DESC; if (typeFilters.length > 0) { - groupsService + services.groupsService .contents(state.projects.currentItemId, { limit: dataExplorer.rowsPerPage, offset: dataExplorer.page * dataExplorer.rowsPerPage, diff --git a/src/store/project/project-action.ts b/src/store/project/project-action.ts index cf384561..2f9963bf 100644 --- a/src/store/project/project-action.ts +++ b/src/store/project/project-action.ts @@ -4,11 +4,11 @@ import { default as unionize, ofType, UnionOf } from "unionize"; import { ProjectResource } from "../../models/project"; -import { projectService } from "../../services/services"; import { Dispatch } from "redux"; import { FilterBuilder } from "../../common/api/filter-builder"; import { RootState } from "../store"; import { checkPresenceInFavorites } from "../favorites/favorites-actions"; +import { ServiceRepository } from "../../services/services"; export const projectActions = unionize({ OPEN_PROJECT_CREATOR: ofType<{ ownerUuid: string }>(), @@ -26,9 +26,9 @@ export const projectActions = unionize({ value: 'payload' }); -export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState) => { +export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { dispatch(projectActions.PROJECTS_REQUEST(parentUuid)); - return projectService.list({ + return services.projectService.list({ filters: FilterBuilder .create() .addEqual("ownerUuid", parentUuid) @@ -40,11 +40,11 @@ export const getProjectList = (parentUuid: string = '') => (dispatch: Dispatch, }; export const createProject = (project: Partial) => - (dispatch: Dispatch, getState: () => RootState) => { + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { const { ownerUuid } = getState().projects.creator; const projectData = { ownerUuid, ...project }; dispatch(projectActions.CREATE_PROJECT(projectData)); - return projectService + return services.projectService .create(projectData) .then(project => dispatch(projectActions.CREATE_PROJECT_SUCCESS(project))); }; diff --git a/src/store/store.ts b/src/store/store.ts index ae077442..cf07d6df 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -18,6 +18,7 @@ import { favoritePanelMiddleware } from "./favorite-panel/favorite-panel-middlew import { reducer as formReducer } from 'redux-form'; import { FavoritesState, favoritesReducer } from './favorites/favorites-reducer'; import { snackbarReducer, SnackbarState } from './snackbar/snackbar-reducer'; +import { ServiceRepository } from "../services/services"; const composeEnhancers = (process.env.NODE_ENV === 'development' && @@ -36,26 +37,25 @@ export interface RootState { snackbar: SnackbarState; } -const rootReducer = combineReducers({ - auth: authReducer, - projects: projectsReducer, - router: routerReducer, - dataExplorer: dataExplorerReducer, - sidePanel: sidePanelReducer, - detailsPanel: detailsPanelReducer, - contextMenu: contextMenuReducer, - form: formReducer, - favorites: favoritesReducer, - snackbar: snackbarReducer, -}); +export function configureStore(history: History, services: ServiceRepository) { + const rootReducer = combineReducers({ + auth: authReducer(services), + projects: projectsReducer, + router: routerReducer, + dataExplorer: dataExplorerReducer, + sidePanel: sidePanelReducer, + detailsPanel: detailsPanelReducer, + contextMenu: contextMenuReducer, + form: formReducer, + favorites: favoritesReducer, + snackbar: snackbarReducer, + }); - -export function configureStore(history: History) { const middlewares: Middleware[] = [ routerMiddleware(history), - thunkMiddleware, - projectPanelMiddleware, - favoritePanelMiddleware + thunkMiddleware.withExtraArgument(services), + projectPanelMiddleware(services), + favoritePanelMiddleware(services) ]; const enhancer = composeEnhancers(applyMiddleware(...middlewares)); return createStore(rootReducer, enhancer); diff --git a/src/views-components/api-token/api-token.tsx b/src/views-components/api-token/api-token.tsx index 1d017ccd..51e3ad1f 100644 --- a/src/views-components/api-token/api-token.tsx +++ b/src/views-components/api-token/api-token.tsx @@ -6,11 +6,12 @@ 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 { authService } from "../../services/services"; import { getProjectList } from "../../store/project/project-action"; import { getUrlParameter } from "../../common/url"; +import { AuthService } from "../../services/auth-service/auth-service"; interface ApiTokenProps { + authService: AuthService; } export const ApiToken = connect()( @@ -20,7 +21,7 @@ export const ApiToken = connect()( const apiToken = getUrlParameter(search, 'api_token'); this.props.dispatch(authActions.SAVE_API_TOKEN(apiToken)); this.props.dispatch(getUserDetails()).then(() => { - const rootUuid = authService.getRootUuid(); + 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 538b8e78..48ea2de9 100644 --- a/src/views/workbench/workbench.test.tsx +++ b/src/views/workbench/workbench.test.tsx @@ -11,12 +11,13 @@ import createBrowserHistory from "history/createBrowserHistory"; import { ConnectedRouter } from "react-router-redux"; import { MuiThemeProvider } from '@material-ui/core/styles'; import { CustomTheme } from '../../common/custom-theme'; +import { createServices } from "../../services/services"; const history = createBrowserHistory(); it('renders without crashing', () => { const div = document.createElement('div'); - const store = configureStore(createBrowserHistory()); + const store = configureStore(createBrowserHistory(), createServices()); ReactDOM.render( diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 3637528d..b0fe4b3e 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -24,7 +24,6 @@ import { ProjectPanel } from "../project-panel/project-panel"; import { DetailsPanel } from '../../views-components/details-panel/details-panel'; import { ArvadosTheme } from '../../common/custom-theme'; import { CreateProjectDialog } from "../../views-components/create-project-dialog/create-project-dialog"; -import { authService } from '../../services/services'; import { detailsPanelActions, loadDetails } from "../../store/details-panel/details-panel-action"; import { contextMenuActions } from "../../store/context-menu/context-menu-actions"; @@ -36,6 +35,7 @@ import { FavoritePanel, FAVORITE_PANEL_ID } from "../favorite-panel/favorite-pan import { CurrentTokenDialog } from '../../views-components/current-token-dialog/current-token-dialog'; import { dataExplorerActions } from '../../store/data-explorer/data-explorer-action'; import { Snackbar } from '../../views-components/snackbar/snackbar'; +import { AuthService } from "../../services/auth-service/auth-service"; const drawerWidth = 240; const appBarHeight = 100; @@ -86,10 +86,14 @@ interface WorkbenchDataProps { sidePanelItems: SidePanelItem[]; } +interface WorkbenchServiceProps { + authService: AuthService; +} + interface WorkbenchActionProps { } -type WorkbenchProps = WorkbenchDataProps & WorkbenchActionProps & DispatchProp & WithStyles; +type WorkbenchProps = WorkbenchDataProps & WorkbenchServiceProps & WorkbenchActionProps & DispatchProp & WithStyles; interface NavBreadcrumb extends Breadcrumb { itemId: string; @@ -188,7 +192,7 @@ export const Workbench = withStyles(styles)( toggleActive={this.toggleSidePanelActive} sidePanelItems={this.props.sidePanelItems} onContextMenu={(event) => this.openContextMenu(event, { - uuid: authService.getUuid() || "", + uuid: this.props.authService.getUuid() || "", name: "", kind: ContextMenuKind.ROOT_PROJECT })}> -- 2.30.2