15088: Handles browser navigation during link account ops
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Mon, 13 May 2019 20:34:30 +0000 (16:34 -0400)
committerEric Biagiotti <ebiagiotti@veritasgenetics.com>
Mon, 13 May 2019 20:34:30 +0000 (16:34 -0400)
- Inactive page link account button brings the user to the initial link account page

Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com>

src/index.tsx
src/routes/routes.ts
src/services/link-account-service/link-account-service.ts
src/store/auth/auth-action.ts
src/store/link-account-panel/link-account-panel-actions.ts
src/store/link-account-panel/link-account-panel-reducer.ts
src/views/inactive-panel/inactive-panel.tsx
src/views/link-account-panel/link-account-panel-root.tsx
src/views/main-panel/main-panel-root.tsx
src/views/main-panel/main-panel.tsx
src/views/workbench/workbench.tsx

index 9f9b27ca912809f4730435382214a8d5ced02eeb..014627a9b2ea06e089e2e0bf7e902b51e29ce4f0 100644 (file)
@@ -64,6 +64,9 @@ import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions
 import { collectionAdminActionSet } from '~/views-components/context-menu/action-sets/collection-admin-action-set';
 import { processResourceAdminActionSet } from '~/views-components/context-menu/action-sets/process-resource-admin-action-set';
 import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
+import { ACCOUNT_LINK_STATUS_KEY } from '~/services/link-account-service/link-account-service';
+import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
+import { matchTokenRoute } from '~/routes/routes';
 
 console.log(`Starting arvados [${getBuildInfo()}]`);
 
@@ -108,6 +111,13 @@ fetchConfig()
         });
         const store = configureStore(history, services);
 
+        // Cancel any link account ops in progess unless the user has
+        // just logged in or there has been a successful link operation
+        const data = sessionStorage.getItem(ACCOUNT_LINK_STATUS_KEY);
+        if (!matchTokenRoute(history.location.pathname) && data === null) {
+            store.dispatch<any>(cancelLinking());
+        }
+
         store.subscribe(initListener(history, store, services, config));
         store.dispatch(initAuth(config));
         store.dispatch(setBuildInfo());
index 02835fcce843c6fd894d072197542b6123abd9ec..ba7e2a45aff16946ad61b8a2316eba6e84dd632e 100644 (file)
@@ -122,6 +122,9 @@ export const matchLinkAccountRoute = (route: string) =>
 export const matchKeepServicesRoute = (route: string) =>
     matchPath(route, { path: Routes.KEEP_SERVICES });
 
+export const matchTokenRoute = (route: string) =>
+    matchPath(route, { path: Routes.TOKEN });
+
 export const matchUsersRoute = (route: string) =>
     matchPath(route, { path: Routes.USERS });
 
index ebae69ac37da2216d3f91aa1a2abc9017837ce77..42fae3654a224f74f58fe8e86bbf3e95fd7ad7c4 100644 (file)
@@ -16,15 +16,15 @@ export class LinkAccountService {
         protected serverApi: AxiosInstance,
         protected actions: ApiActions) { }
 
-    public saveToSession(account: AccountToLink) {
+    public saveAccountToLink(account: AccountToLink) {
         sessionStorage.setItem(USER_LINK_ACCOUNT_KEY, JSON.stringify(account));
     }
 
-    public removeFromSession() {
+    public removeAccountToLink() {
         sessionStorage.removeItem(USER_LINK_ACCOUNT_KEY);
     }
 
-    public getFromSession() {
+    public getAccountToLink() {
         const data = sessionStorage.getItem(USER_LINK_ACCOUNT_KEY);
         return data ? JSON.parse(data) as AccountToLink : undefined;
     }
index 1198ff8dba40569693937a6a8f13b74dd4c5a76a..8be02ed02ab426dae71000ee1aca3295510f15e4 100644 (file)
@@ -80,7 +80,7 @@ export const login = (uuidPrefix: string, homeCluster: string) => (dispatch: Dis
 
 export const logout = (deleteLinkData: boolean = false) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     if (deleteLinkData) {
-        services.linkAccountService.removeFromSession();
+        services.linkAccountService.removeAccountToLink();
     }
     services.authService.removeApiToken();
     services.authService.removeUser();
index 61dc657eb21f998104a13467a4e42d819138819d..5552882449b0d376aa3c502ca74d4c66a650c87d 100644 (file)
@@ -14,6 +14,8 @@ import { UserResource } from "~/models/user";
 import { GroupResource } from "~/models/group";
 import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
 import { login, logout } from "~/store/auth/auth-action";
+import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
+import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
 
 export const linkAccountPanelActions = unionize({
     LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
@@ -69,13 +71,6 @@ export const checkForLinkStatus = () =>
         }
     };
 
-export const finishLinking = (status: LinkAccountStatus) =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        services.linkAccountService.removeFromSession();
-        services.linkAccountService.saveLinkOpStatus(status);
-        location.reload();
-    };
-
 export const switchUser = (user: UserResource, token: string) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(saveUser(user));
@@ -97,12 +92,11 @@ export const linkFailed = () =>
             }
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
         }
-        dispatch(finishLinking(LinkAccountStatus.FAILED));
+        services.linkAccountService.removeAccountToLink();
     };
 
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-
         // First check if an account link operation has completed
         dispatch(checkForLinkStatus());
 
@@ -112,20 +106,11 @@ export const loadLinkAccountPanel = () =>
         const curToken = getState().auth.apiToken;
         if (curUser && curToken) {
             const curUserResource = await services.userService.get(curUser.uuid);
-            const linkAccountData = services.linkAccountService.getFromSession();
+            const linkAccountData = services.linkAccountService.getAccountToLink();
 
             // If there is link account session data, then the user has logged in a second time
             if (linkAccountData) {
 
-                // If the window is refreshed after the second login, cancel the linking
-                if (window.performance) {
-                    if (performance.navigation.type === PerformanceNavigation.TYPE_BACK_FORWARD ||
-                        performance.navigation.type === PerformanceNavigation.TYPE_RELOAD) {
-                        dispatch(cancelLinking());
-                        return;
-                    }
-                }
-
                 // Use the token of the user we are getting data for. This avoids any admin/non-admin permissions
                 // issues since a user will always be able to query the api server for their own user data.
                 dispatch(saveApiToken(linkAccountData.token));
@@ -155,7 +140,8 @@ export const loadLinkAccountPanel = () =>
                     // This should never really happen, but just in case, switch to the user that
                     // originated the linking operation (i.e. the user saved in session data)
                     dispatch(switchUser(savedUserResource, linkAccountData.token));
-                    dispatch(finishLinking(LinkAccountStatus.FAILED));
+                    services.linkAccountService.removeAccountToLink();
+                    dispatch(linkAccountPanelActions.LINK_INIT({targetUser:savedUserResource}));
                 }
 
                 dispatch(switchUser(params.targetUser, params.targetUserToken));
@@ -183,7 +169,7 @@ export const loadLinkAccountPanel = () =>
 export const startLinking = (t: LinkAccountType) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
-        services.linkAccountService.saveToSession(accountToLink);
+        services.linkAccountService.saveAccountToLink(accountToLink);
         const auth = getState().auth;
         dispatch(logout());
         dispatch(login(auth.localCluster, auth.remoteHosts[auth.homeCluster]));
@@ -191,15 +177,16 @@ export const startLinking = (t: LinkAccountType) =>
 
 export const getAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        return services.linkAccountService.getFromSession();
+        return services.linkAccountService.getAccountToLink();
     };
 
 export const cancelLinking = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         let user: UserResource | undefined;
         try {
+            dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
             // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
-            const linkAccountData = services.linkAccountService.getFromSession();
+            const linkAccountData = services.linkAccountService.getAccountToLink();
             if (linkAccountData) {
                 dispatch(saveApiToken(linkAccountData.token));
                 user = await services.userService.get(linkAccountData.userUuid);
@@ -207,7 +194,9 @@ export const cancelLinking = () =>
             }
         }
         finally {
-            dispatch(finishLinking(LinkAccountStatus.CANCELLED));
+            services.linkAccountService.removeAccountToLink();
+            dispatch(linkAccountPanelActions.LINK_INIT({targetUser:user}));
+            dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
         }
     };
 
@@ -236,6 +225,9 @@ export const linkAccount = () =>
                 dispatch(saveApiToken(linkState.userToLinkToken));
                 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
+                services.linkAccountService.removeAccountToLink();
+                services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.SUCCESS);
+                location.reload();
             }
             catch(e) {
                 // If the link operation fails, delete the previously made project
@@ -248,8 +240,5 @@ export const linkAccount = () =>
                 }
                 throw e;
             }
-            finally {
-                dispatch(finishLinking(LinkAccountStatus.SUCCESS));
-            }
         }
     };
\ No newline at end of file
index 80878c3471dec02457b380962928672306a7fec8..0e61abdd2f55bf094cfe96716cd223fdafd92a65 100644 (file)
@@ -6,6 +6,7 @@ import { linkAccountPanelActions, LinkAccountPanelAction } from "~/store/link-ac
 import { UserResource } from "~/models/user";
 
 export enum LinkAccountPanelStatus {
+    NONE,
     INITIAL,
     HAS_SESSION_DATA,
     LINKING,
@@ -41,7 +42,7 @@ const initialState = {
     targetUserToken: undefined,
     userToLink: undefined,
     userToLinkToken: undefined,
-    status: LinkAccountPanelStatus.INITIAL,
+    status: LinkAccountPanelStatus.NONE,
     error: LinkAccountPanelError.NONE
 };
 
index 5f045f690fedba8f11e249be5fb4ec9d5a7fb99d..8d53a21ecaa060fd659d6c92ac7d9e4f83e6b692 100644 (file)
@@ -5,11 +5,10 @@
 import * as React from 'react';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { startLinking } from '~/store/link-account-panel/link-account-panel-actions';
 import { Grid, Typography, Button } from '@material-ui/core';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { LinkAccountType } from '~/models/link-account';
+import { navigateToLinkAccount } from '~/store/navigation/navigation-action';
 
 
 type CssRules = 'root' | 'ontop' | 'title';
@@ -43,7 +42,9 @@ export interface InactivePanelActionProps {
 }
 
 const mapDispatchToProps = (dispatch: Dispatch): InactivePanelActionProps => ({
-    startLinking: () => dispatch<any>(startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT))
+    startLinking: () => {
+        dispatch<any>(navigateToLinkAccount);
+    }
 });
 
 type InactivePanelProps =  WithStyles<CssRules> & InactivePanelActionProps;
index 2439cc92fda00b9b897e8df17b30a1890e395145..19c4b97af069edbd74b48462702beeac54f352f3 100644 (file)
@@ -67,7 +67,7 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                 </Grid>
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
+                        <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
                             Add another login to this account
                         </Button>
                     </Grid>
index 51287fa8c00c4445a573e837232262a435580b63..43bc7fbc158ba6c891ff3d87b60dd0f8d5deca74 100644 (file)
@@ -30,23 +30,24 @@ export interface MainPanelRootDataProps {
     buildInfo: string;
     uuidPrefix: string;
     isNotLinking: boolean;
+    isLinkingPath: boolean;
 }
 
 type MainPanelRootProps = MainPanelRootDataProps & WithStyles<CssRules>;
 
 export const MainPanelRoot = withStyles(styles)(
-    ({ classes, loading, working, user, buildInfo, uuidPrefix, isNotLinking }: MainPanelRootProps) =>
+    ({ classes, loading, working, user, buildInfo, uuidPrefix, isNotLinking, isLinkingPath }: MainPanelRootProps) =>
         loading
             ? <WorkbenchLoadingScreen />
-            : <> { isNotLinking ? <>
-               <MainAppBar
+            : <>
+               { isNotLinking && <MainAppBar
                     user={user}
                     buildInfo={buildInfo}
                     uuidPrefix={uuidPrefix}>
                     {working ? <LinearProgress color="secondary" /> : null}
-                </MainAppBar>
+               </MainAppBar> }
                 <Grid container direction="column" className={classes.root}>
-                    { user ? (user.isActive ? <WorkbenchPanel /> : <InactivePanel />) : <LoginPanel />}
-               </Grid>
-            </> : user ? <LinkAccountPanel/> : <LoginPanel /> } </>
+                    { user ? (user.isActive || (!user.isActive && isLinkingPath) ? <WorkbenchPanel isNotLinking={isNotLinking} isUserActive={user.isActive} /> : <InactivePanel />) : <LoginPanel /> }
+                </Grid>
+            </>
 );
index 178db25c222878339d456913d0f647e6916a5659..5bf03da3986afbe73c8999c43cd119f438652785 100644 (file)
@@ -8,6 +8,7 @@ import { MainPanelRoot, MainPanelRootDataProps } from '~/views/main-panel/main-p
 import { isSystemWorking } from '~/store/progress-indicator/progress-indicator-reducer';
 import { isWorkbenchLoading } from '~/store/workbench/workbench-actions';
 import { LinkAccountPanelStatus } from '~/store/link-account-panel/link-account-panel-reducer';
+import { matchLinkAccountRoute } from '~/routes/routes';
 
 const mapStateToProps = (state: RootState): MainPanelRootDataProps => {
     return {
@@ -16,7 +17,8 @@ const mapStateToProps = (state: RootState): MainPanelRootDataProps => {
         loading: isWorkbenchLoading(state),
         buildInfo: state.appInfo.buildInfo,
         uuidPrefix: state.auth.localCluster,
-        isNotLinking: state.linkAccountPanel.status === LinkAccountPanelStatus.INITIAL
+        isNotLinking: state.linkAccountPanel.status === LinkAccountPanelStatus.NONE || state.linkAccountPanel.status === LinkAccountPanelStatus.INITIAL,
+        isLinkingPath:  state.router.location ? matchLinkAccountRoute(state.router.location.pathname) !== null : false
     };
 };
 
index 07b056554e65a7fca999d590ae61b6eb204bd619..e852150c2721001476ee0eb07399a0505bd02770 100644 (file)
@@ -3,6 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
+import { connect } from 'react-redux';
 import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 import { Route, Switch } from "react-router";
 import { ProjectPanel } from "~/views/project-panel/project-panel";
@@ -124,7 +125,12 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     }
 });
 
-type WorkbenchPanelProps = WithStyles<CssRules>;
+interface WorkbenchDataProps {
+    isUserActive: boolean;
+    isNotLinking: boolean;
+}
+
+type WorkbenchPanelProps = WithStyles<CssRules> & WorkbenchDataProps;
 
 const defaultSplitterSize = 90;
 
@@ -136,21 +142,21 @@ const getSplitterInitialSize = () => {
 const saveSplitterSize = (size: number) => localStorage.setItem('splitterSize', size.toString());
 
 export const WorkbenchPanel =
-    withStyles(styles)(({ classes }: WorkbenchPanelProps) =>
-        <Grid container item xs className={classes.root}>
-            <Grid container item xs className={classes.container}>
-                <SplitterLayout customClassName={classes.splitter} percentage={true}
+    withStyles(styles)((props: WorkbenchPanelProps) =>
+        <Grid container item xs className={props.classes.root}>
+            <Grid container item xs className={props.classes.container}>
+                <SplitterLayout customClassName={props.classes.splitter} percentage={true}
                     primaryIndex={0} primaryMinSize={10}
                     secondaryInitialSize={getSplitterInitialSize()} secondaryMinSize={40}
                     onSecondaryPaneSizeChange={saveSplitterSize}>
-                    <Grid container item xs component='aside' direction='column' className={classes.asidePanel}>
+                    { props.isUserActive && props.isNotLinking && <Grid container item xs component='aside' direction='column' className={props.classes.asidePanel}>
                         <SidePanel />
-                    </Grid>
-                    <Grid container item xs component="main" direction="column" className={classes.contentWrapper}>
+                    </Grid> }
+                    <Grid container item xs component="main" direction="column" className={props.classes.contentWrapper}>
                         <Grid item xs>
-                            <MainContentBar />
+                            { props.isNotLinking && <MainContentBar /> }
                         </Grid>
-                        <Grid item xs className={classes.content}>
+                        <Grid item xs className={props.classes.content}>
                             <Switch>
                                 <Route path={Routes.PROJECTS} component={ProjectPanel} />
                                 <Route path={Routes.COLLECTIONS} component={CollectionPanel} />