[ARVADOS-WORKBENCH2] updated: 1.3.1-524-gb58c1d9f

Git user git at public.curoverse.com
Wed May 22 21:38:27 UTC 2019


Summary of changes:
 src/common/formatters.ts                           |  10 +-
 src/models/group.ts                                |   1 +
 src/models/link-account.ts                         |  22 ++
 src/models/test-utils.ts                           |   1 +
 src/routes/route-change-handlers.ts                |   3 +
 src/routes/routes.ts                               |  10 +
 .../link-account-service/link-account-service.ts   |  57 +++++
 src/services/services.ts                           |   3 +
 src/store/auth/auth-action.test.ts                 |   5 +-
 src/store/auth/auth-action.ts                      |  31 ++-
 src/store/auth/auth-reducer.ts                     |   5 +-
 .../link-account-panel-actions.ts                  | 280 +++++++++++++++++++++
 .../link-account-panel-reducer.test.ts             |  79 ++++++
 .../link-account-panel-reducer.ts                  |  86 +++++++
 src/store/navigation/navigation-action.ts          |   2 +
 src/store/store.ts                                 |   4 +-
 src/store/workbench/workbench-actions.ts           |  12 +-
 src/views-components/api-token/api-token.tsx       |  10 +-
 src/views-components/main-app-bar/account-menu.tsx |   6 +-
 src/views/inactive-panel/inactive-panel.tsx        |  79 +++---
 .../link-account-panel/link-account-panel-root.tsx | 182 ++++++++++++++
 .../link-account-panel/link-account-panel.tsx      |  37 +++
 src/views/main-panel/main-panel-root.tsx           |  11 +-
 src/views/main-panel/main-panel.tsx                |   6 +-
 src/views/workbench/workbench.tsx                  |  28 ++-
 25 files changed, 902 insertions(+), 68 deletions(-)
 create mode 100644 src/models/link-account.ts
 create mode 100644 src/services/link-account-service/link-account-service.ts
 create mode 100644 src/store/link-account-panel/link-account-panel-actions.ts
 create mode 100644 src/store/link-account-panel/link-account-panel-reducer.test.ts
 create mode 100644 src/store/link-account-panel/link-account-panel-reducer.ts
 create mode 100644 src/views/link-account-panel/link-account-panel-root.tsx
 create mode 100644 src/views/link-account-panel/link-account-panel.tsx

       via  b58c1d9f7ce635aeab54c02faa516593305967e3 (commit)
       via  23199ed951991534b1c582dce5b609f758f50a68 (commit)
       via  547ea9aed5331a2d7a9ffd64807c48294721a686 (commit)
       via  081ace17e2bbed7176960006baa57e20f13b7bdb (commit)
       via  6cbd20e54c830bbd70b6c314c53e95cf06c4e4ed (commit)
       via  010156ef2c2c6977b9ad67fc613b369d8b3ae94f (commit)
       via  b0c7c2e4ae762e5b25a180c5166b1cb8b3290260 (commit)
       via  950b520777c389353222c717ee3ac18e80a468f2 (commit)
       via  a69cc610cc0827653d5fbfac1477b832ff5efaba (commit)
       via  122fba47e0b0629ebb449c5206ba400021bc6de1 (commit)
       via  eb9611ea28acee0a8fdef772ef8d1b31ac689e4c (commit)
       via  842eaeb05a5df83493e9448bc720760d9cc288cc (commit)
       via  6e1ba15c736bacce7dc187f24a33216bb9ad37f3 (commit)
       via  06f1c8b752ff3f2b788773eac42438335bb86a91 (commit)
       via  3bc058f0bcd746912230e29683509d692fa8203e (commit)
       via  3b53b656e65fdabc32b3bc748074eb35e9df98eb (commit)
       via  967f2bb4cc911c0f3dae6246ac81e19c177751ba (commit)
       via  c801917eb9aeabf5104e5512dd17abf932b66a55 (commit)
       via  e954cfb45dbe418c151144cc42847b848c9b0ebf (commit)
       via  d2da940fb4be60affe0a350c7f82cf91180f1e3a (commit)
       via  2a5e5a512747d9bd92ffe1f89a991879a0897e4e (commit)
       via  24cc808e44765268ddbcecbfed0645b09fe7777d (commit)
       via  ad1b12ef73a2981ed40f3dbdb8d6e1cc9d35a2cd (commit)
       via  d4489afef9924986e7a04201602f2b810ddfa9d2 (commit)
       via  bdfda992c607ed4ca591dbf310e659faa370a881 (commit)
       via  70db44a1978e7078ba212a7766cb320aafc26fd6 (commit)
       via  d329bdf89ea30acc0e9a95bcb7bc4338f8beeebb (commit)
       via  2f857ebcd67a607b7fde9c0ea4808ac30c591876 (commit)
       via  dcab8d69b5fb93c025c49fd85bf39d038c4fb3d0 (commit)
       via  13700efea8cd742fbb4888252f3d06788f5fd845 (commit)
       via  2b22123ad358b8c6667e42779f6e52e9a14d9289 (commit)
       via  6e2546d78a606e19fc974b5e4443880cf6da282a (commit)
       via  bc9df86de8ce648f14193315f037ca4a2b119773 (commit)
       via  84c94f372cead175c61c3dd1c69486ce87d91539 (commit)
       via  fc1c9792135dfbf36c148985b402871339d98561 (commit)
       via  43a384e98b698de75f66dcc5a0241a1246ddd447 (commit)
       via  6dfcd99cab6ea26ef947bdc2c90020ccea1c925b (commit)
      from  c75768d4dfcc49eadf410077390ef8c281261594 (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 b58c1d9f7ce635aeab54c02faa516593305967e3
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 22 17:10:35 2019 -0400

    Fixes auth-action test merge refs #15088
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/auth/auth-action.test.ts b/src/store/auth/auth-action.test.ts
index f401d4a7..926121e3 100644
--- a/src/store/auth/auth-action.test.ts
+++ b/src/store/auth/auth-action.test.ts
@@ -72,7 +72,6 @@ describe('auth-actions', () => {
                 zzzzz: "zzzzz.arvadosapi.com",
                 xc59z: "xc59z.arvadosapi.com"
             },
-	    remoteHostsConfig: {},
             sessions: [{
                 "active": true,
                 "baseUrl": undefined,

commit 23199ed951991534b1c582dce5b609f758f50a68
Merge: c75768d4 547ea9ae
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 22 17:01:20 2019 -0400

    Merge branch '15088-merge-account'
    
    refs #15088
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>


commit 547ea9aed5331a2d7a9ffd64807c48294721a686
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 22 16:52:06 2019 -0400

    15088: Text clarification for linking as a remote user
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index a5b1e35e..0eb494e6 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -126,7 +126,7 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                             You are currently logged in as {displayUser(targetUser, true, true)}
                         </Grid>
                         {targetUser.isActive ? <> <Grid item>
-                            This a remote account. You can link a local Arvados account to this one. After linking, you can access the local account's data by logging into the <b>{localCluster}</b> cluster with the <b>{targetUser.email}</b> account.
+                            This a remote account. You can link a local Arvados account to this one. After linking, you can access the local account's data by logging into the <b>{localCluster}</b> cluster as user <b>{targetUser.email}</b> from <b>{targetUser.uuid.substr(0,5)}</b>.
                         </Grid >
                         <Grid item>
                             <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>

commit 081ace17e2bbed7176960006baa57e20f13b7bdb
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 22 14:47:54 2019 -0400

    15088: Fixes auth-action test and simplifies link account cancel snackbar
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/auth/auth-action.test.ts b/src/store/auth/auth-action.test.ts
index 4a69fc15..926121e3 100644
--- a/src/store/auth/auth-action.test.ts
+++ b/src/store/auth/auth-action.test.ts
@@ -23,6 +23,7 @@ import { configureStore, RootStore } from "../store";
 import createBrowserHistory from "history/createBrowserHistory";
 import { Config, mockConfig } from '~/common/config';
 import { ApiActions } from "~/services/api/api-actions";
+import { ACCOUNT_LINK_STATUS_KEY} from '~/services/link-account-service/link-account-service';
 
 describe('auth-actions', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
@@ -40,6 +41,8 @@ describe('auth-actions', () => {
 
     it('should initialise state with user and api token from local storage', () => {
 
+        // Only test the case when a link account operation is not being cancelled
+        sessionStorage.setItem(ACCOUNT_LINK_STATUS_KEY, "0");
         localStorage.setItem(API_TOKEN_KEY, "token");
         localStorage.setItem(USER_EMAIL_KEY, "test at test.com");
         localStorage.setItem(USER_FIRST_NAME_KEY, "John");
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index be6e9f48..1d1ad18c 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -51,7 +51,7 @@ function removeAuthorizationHeader(client: AxiosInstance) {
 }
 
 export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-    // Cancel any link account ops in progess unless the user has
+    // Cancel any link account ops in progress unless the user has
     // just logged in or there has been a successful link operation
     const data = services.linkAccountService.getLinkOpStatus();
     if (!matchTokenRoute(location.pathname) && (!matchFedTokenRoute(location.pathname)) && data === undefined) {
@@ -62,7 +62,6 @@ export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () =>
     else {
         dispatch<any>(init(config));
     }
-
 };
 
 const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 57f0681a..cdc99660 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -223,16 +223,15 @@ export const cancelLinking = (reload: boolean = false) =>
                 setAuthorizationHeader(services, linkAccountData.token);
                 user = await services.userService.get(linkAccountData.userUuid);
                 dispatch(switchUser(user, linkAccountData.token));
+                services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED);
             }
         }
         finally {
             if (reload) {
-                services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED);
                 location.reload();
             }
             else {
                 dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Account link cancelled!", kind: SnackbarKind.INFO, hideDuration: 3000 }));
             }
         }
     };

commit 6cbd20e54c830bbd70b6c314c53e95cf06c4e4ed
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 22 11:47:25 2019 -0400

    15088: Changes link account page times to local. Adds cancel snackbar.
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 9f94fa75..57f0681a 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -227,10 +227,12 @@ export const cancelLinking = (reload: boolean = false) =>
         }
         finally {
             if (reload) {
+                services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.CANCELLED);
                 location.reload();
             }
             else {
                 dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Account link cancelled!", kind: SnackbarKind.INFO, hideDuration: 3000 }));
             }
         }
     };
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index c38cbe64..a5b1e35e 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -56,7 +56,7 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false, showClu
         disp.push(<span> hosted on cluster <b>{homeCluster}</b> and </span>);
     }
     if (showCreatedAt) {
-        disp.push(<span> created on <b>{formatDate(user.createdAt, true)}</b></span>);
+        disp.push(<span> created on <b>{formatDate(user.createdAt)}</b></span>);
     }
     return disp;
 }

commit 010156ef2c2c6977b9ad67fc613b369d8b3ae94f
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue May 21 16:37:12 2019 -0400

    15088: Fixes page refresh on link accounts page
    
    - Makes the link accounts page to not show text if data is being processed
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index 5317ede4..be6e9f48 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -55,9 +55,17 @@ export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () =>
     // just logged in or there has been a successful link operation
     const data = services.linkAccountService.getLinkOpStatus();
     if (!matchTokenRoute(location.pathname) && (!matchFedTokenRoute(location.pathname)) && data === undefined) {
-        dispatch<any>(cancelLinking());
+        dispatch<any>(cancelLinking()).then(() => {
+            dispatch<any>(init(config));
+        });
+    }
+    else {
+        dispatch<any>(init(config));
     }
 
+};
+
+const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     const user = services.authService.getUser();
     const token = services.authService.getApiToken();
     const homeCluster = services.authService.getHomeCluster();
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 11159e9a..c38cbe64 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -80,7 +80,7 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     <CircularProgress/>
                 </Grid>
             </Grid> }
-            { status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
+            { !isProcessing && status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
                 { isLocalUser(targetUser.uuid, localCluster) ? <Grid container spacing={24}>
                     <Grid container item direction="column" spacing={24}>
                         <Grid item>
@@ -139,7 +139,7 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     </Grid>
                 </Grid>}
             </div> }
-            { (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
+            { !isProcessing && (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
             <Grid container spacing={24}>
                 { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
                     <Grid item>

commit b0c7c2e4ae762e5b25a180c5166b1cb8b3290260
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue May 21 13:50:32 2019 -0400

    15088: Adds unique message for inactive remote users linking accounts
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 772fe38b..11159e9a 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -125,14 +125,17 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                         <Grid item>
                             You are currently logged in as {displayUser(targetUser, true, true)}
                         </Grid>
-                        <Grid item>
+                        {targetUser.isActive ? <> <Grid item>
                             This a remote account. You can link a local Arvados account to this one. After linking, you can access the local account's data by logging into the <b>{localCluster}</b> cluster with the <b>{targetUser.email}</b> account.
                         </Grid >
                         <Grid item>
                             <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
                                 Link an account from {localCluster} to this account
                             </Button>
-                        </Grid>
+                        </Grid> </>
+                        : <Grid item>
+                          This an inactive remote account. An administrator must activate your account before you can proceed. After your accounts is activated, you can link a local Arvados account hosted by the <b>{localCluster}</b> cluster to this one.
+                        </Grid >}
                     </Grid>
                 </Grid>}
             </div> }

commit 950b520777c389353222c717ee3ac18e80a468f2
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue May 21 09:59:17 2019 -0400

    15088: Fixes linkAccountPanel reducer tests
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-reducer.test.ts b/src/store/link-account-panel/link-account-panel-reducer.test.ts
index 4cb88a85..d1bd8dfd 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.test.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.test.ts
@@ -16,6 +16,8 @@ describe('link-account-panel-reducer', () => {
         const state = linkAccountPanelReducer(initialState, linkAccountPanelActions.LINK_INIT({targetUser}));
         expect(state).toEqual({
             targetUser,
+            isProcessing: false,
+            selectedCluster: undefined,
             targetUserToken: undefined,
             userToLink: undefined,
             userToLinkToken: undefined,
@@ -41,6 +43,8 @@ describe('link-account-panel-reducer', () => {
         expect(state).toEqual({
             targetUser,
             targetUserToken,
+            isProcessing: false,
+            selectedCluster: undefined,
             userToLink,
             userToLinkToken,
             originatingUser: OriginatingUser.TARGET_USER,
@@ -63,6 +67,8 @@ describe('link-account-panel-reducer', () => {
         expect(state).toEqual({
             targetUser,
             targetUserToken: undefined,
+            isProcessing: false,
+            selectedCluster: undefined,
             userToLink,
             userToLinkToken: undefined,
             originatingUser: OriginatingUser.TARGET_USER,

commit a69cc610cc0827653d5fbfac1477b832ff5efaba
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Mon May 20 16:45:13 2019 -0400

    15088: Adds progress circle when loading the link panel
    
    - Improves error handling in loadLinkAccountPanel
    - Stops using saveApiToken for cross user API calls.
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index 87eb3f75..5317ede4 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -37,7 +37,7 @@ export const authActions = unionize({
     REMOTE_CLUSTER_CONFIG: ofType<{ config: Config }>(),
 });
 
-function setAuthorizationHeader(services: ServiceRepository, token: string) {
+export function setAuthorizationHeader(services: ServiceRepository, token: string) {
     services.apiClient.defaults.headers.common = {
         Authorization: `OAuth2 ${token}`
     };
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 7a92f4aa..9f94fa75 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -13,7 +13,7 @@ import { unionize, ofType, UnionOf } from '~/common/unionize';
 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 { login, logout, setAuthorizationHeader } from "~/store/auth/auth-action";
 import { progressIndicatorActions } from "~/store/progress-indicator/progress-indicator-actions";
 import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
 
@@ -33,6 +33,8 @@ export const linkAccountPanelActions = unionize({
         error: LinkAccountPanelError }>(),
     SET_SELECTED_CLUSTER: ofType<{
         selectedCluster: string }>(),
+    SET_IS_PROCESSING: ofType<{
+        isProcessing: boolean}>(),
     HAS_SESSION_DATA: {}
 });
 
@@ -99,85 +101,92 @@ export const linkFailed = () =>
 
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        // If there are remote hosts, set the initial selected cluster by getting the first cluster that isn't the local cluster
-        if (getState().linkAccountPanel.selectedCluster === undefined) {
-            const localCluster = getState().auth.localCluster;
-            let selectedCluster = localCluster;
-            for (const key in getState().auth.remoteHosts) {
-                if (key !== localCluster) {
-                    selectedCluster = key;
-                    break;
+        try {
+            // If there are remote hosts, set the initial selected cluster by getting the first cluster that isn't the local cluster
+            if (getState().linkAccountPanel.selectedCluster === undefined) {
+                const localCluster = getState().auth.localCluster;
+                let selectedCluster = localCluster;
+                for (const key in getState().auth.remoteHosts) {
+                    if (key !== localCluster) {
+                        selectedCluster = key;
+                        break;
+                    }
                 }
+                dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }));
             }
-            dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }));
-        }
 
-        // First check if an account link operation has completed
-        dispatch(checkForLinkStatus());
+            // First check if an account link operation has completed
+            dispatch(checkForLinkStatus());
 
-        // Continue loading the link account panel
-        dispatch(setBreadcrumbs([{ label: 'Link account'}]));
-        const curUser = getState().auth.user;
-        const curToken = getState().auth.apiToken;
-        if (curUser && curToken) {
-            const curUserResource = await services.userService.get(curUser.uuid);
-            const linkAccountData = services.linkAccountService.getAccountToLink();
+            // Continue loading the link account panel
+            dispatch(setBreadcrumbs([{ label: 'Link account'}]));
+            const curUser = getState().auth.user;
+            const curToken = getState().auth.apiToken;
+            if (curUser && curToken) {
 
-            // If there is link account session data, then the user has logged in a second time
-            if (linkAccountData) {
+                // If there is link account session data, then the user has logged in a second time
+                const linkAccountData = services.linkAccountService.getAccountToLink();
+                if (linkAccountData) {
 
-                // 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));
-                const savedUserResource = await services.userService.get(linkAccountData.userUuid);
-                dispatch(saveApiToken(curToken));
+                    dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: true }));
+                    const curUserResource = await services.userService.get(curUser.uuid);
 
-                let params: any;
-                if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
-                    params = {
-                        originatingUser: OriginatingUser.USER_TO_LINK,
-                        targetUser: curUserResource,
-                        targetUserToken: curToken,
-                        userToLink: savedUserResource,
-                        userToLinkToken: linkAccountData.token
-                    };
-                }
-                else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN || linkAccountData.type === LinkAccountType.ADD_LOCAL_TO_REMOTE) {
-                    params = {
-                        originatingUser: OriginatingUser.TARGET_USER,
-                        targetUser: savedUserResource,
-                        targetUserToken: linkAccountData.token,
-                        userToLink: curUserResource,
-                        userToLinkToken: curToken
-                    };
-                }
-                else {
-                    // 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));
-                    services.linkAccountService.removeAccountToLink();
-                    dispatch(linkAccountPanelActions.LINK_INIT({targetUser:savedUserResource}));
-                }
+                    // 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.
+                    setAuthorizationHeader(services, linkAccountData.token);
+                    const savedUserResource = await services.userService.get(linkAccountData.userUuid);
+                    setAuthorizationHeader(services, curToken);
+
+                    let params: any;
+                    if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
+                        params = {
+                            originatingUser: OriginatingUser.USER_TO_LINK,
+                            targetUser: curUserResource,
+                            targetUserToken: curToken,
+                            userToLink: savedUserResource,
+                            userToLinkToken: linkAccountData.token
+                        };
+                    }
+                    else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN || linkAccountData.type === LinkAccountType.ADD_LOCAL_TO_REMOTE) {
+                        params = {
+                            originatingUser: OriginatingUser.TARGET_USER,
+                            targetUser: savedUserResource,
+                            targetUserToken: linkAccountData.token,
+                            userToLink: curUserResource,
+                            userToLinkToken: curToken
+                        };
+                    }
+                    else {
+                        throw new Error("Unknown link account type");
+                    }
 
-                dispatch(switchUser(params.targetUser, params.targetUserToken));
-                const error = validateLink(params.userToLink, params.targetUser);
-                if (error === LinkAccountPanelError.NONE) {
-                    dispatch(linkAccountPanelActions.LINK_LOAD(params));
+                    dispatch(switchUser(params.targetUser, params.targetUserToken));
+                    const error = validateLink(params.userToLink, params.targetUser);
+                    if (error === LinkAccountPanelError.NONE) {
+                        dispatch(linkAccountPanelActions.LINK_LOAD(params));
+                    }
+                    else {
+                        dispatch(linkAccountPanelActions.LINK_INVALID({
+                            originatingUser: params.originatingUser,
+                            targetUser: params.targetUser,
+                            userToLink: params.userToLink,
+                            error}));
+                        return;
+                    }
                 }
                 else {
-                    dispatch(linkAccountPanelActions.LINK_INVALID({
-                        originatingUser: params.originatingUser,
-                        targetUser: params.targetUser,
-                        userToLink: params.userToLink,
-                        error}));
+                    // If there is no link account session data, set the state to invoke the initial UI
+                    const curUserResource = await services.userService.get(curUser.uuid);
+                    dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
                     return;
                 }
             }
-            else {
-                // If there is no link account session data, set the state to invoke the initial UI
-                dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
-                return;
-            }
+        }
+        catch (e) {
+            dispatch(linkFailed());
+        }
+        finally {
+            dispatch(linkAccountPanelActions.SET_IS_PROCESSING({ isProcessing: false }));
         }
     };
 
@@ -202,23 +211,27 @@ export const getAccountLinkData = () =>
         return services.linkAccountService.getAccountToLink();
     };
 
-export const cancelLinking = () =>
+export const cancelLinking = (reload: boolean = false) =>
     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)
+            dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
             const linkAccountData = services.linkAccountService.getAccountToLink();
             if (linkAccountData) {
-                dispatch(saveApiToken(linkAccountData.token));
+                services.linkAccountService.removeAccountToLink();
+                setAuthorizationHeader(services, linkAccountData.token);
                 user = await services.userService.get(linkAccountData.userUuid);
                 dispatch(switchUser(user, linkAccountData.token));
             }
         }
         finally {
-            services.linkAccountService.removeAccountToLink();
-            dispatch(linkAccountPanelActions.LINK_INIT({targetUser:user}));
-            dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+            if (reload) {
+                location.reload();
+            }
+            else {
+                dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+            }
         }
     };
 
@@ -243,8 +256,8 @@ export const linkAccount = () =>
 
             try {
                 // The merge api links the user sending the request into the user
-                // specified in the request, so switch users for this api call
-                dispatch(saveApiToken(linkState.userToLinkToken));
+                // specified in the request, so change the authorization header accordingly
+                setAuthorizationHeader(services, linkState.userToLinkToken);
                 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
                 services.linkAccountService.removeAccountToLink();
@@ -254,7 +267,7 @@ export const linkAccount = () =>
             catch(e) {
                 // If the link operation fails, delete the previously made project
                 try {
-                    dispatch(saveApiToken(linkState.targetUserToken));
+                    setAuthorizationHeader(services, linkState.targetUserToken);
                     await services.projectService.delete(newGroup.uuid);
                 }
                 finally {
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 3d205842..21c2c9eb 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -35,6 +35,7 @@ export interface LinkAccountPanelState {
     userToLinkToken: string | undefined;
     status: LinkAccountPanelStatus;
     error: LinkAccountPanelError;
+    isProcessing: boolean;
 }
 
 const initialState = {
@@ -44,6 +45,7 @@ const initialState = {
     targetUserToken: undefined,
     userToLink: undefined,
     userToLinkToken: undefined,
+    isProcessing: false,
     status: LinkAccountPanelStatus.NONE,
     error: LinkAccountPanelError.NONE
 };
@@ -74,6 +76,10 @@ export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialSt
         SET_SELECTED_CLUSTER: ({ selectedCluster }) => ({
             ...state, selectedCluster
         }),
+        SET_IS_PROCESSING: ({ isProcessing }) =>({
+            ...state,
+            isProcessing
+        }),
         HAS_SESSION_DATA: () => ({
             ...state, status: LinkAccountPanelStatus.HAS_SESSION_DATA
         })
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 12dae426..772fe38b 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -11,7 +11,8 @@ import {
     CardContent,
     Button,
     Grid,
-    Select
+    Select,
+    CircularProgress
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { UserResource } from "~/models/user";
@@ -19,7 +20,7 @@ import { LinkAccountType } from "~/models/link-account";
 import { formatDate } from "~/common/formatters";
 import { LinkAccountPanelStatus, LinkAccountPanelError } from "~/store/link-account-panel/link-account-panel-reducer";
 
-type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
+type CssRules = 'root';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -37,6 +38,7 @@ export interface LinkAccountPanelRootDataProps {
     status : LinkAccountPanelStatus;
     error: LinkAccountPanelError;
     selectedCluster?: string;
+    isProcessing: boolean;
 }
 
 export interface LinkAccountPanelRootActionProps {
@@ -66,10 +68,18 @@ function isLocalUser(uuid: string, localCluster: string) {
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, targetUser, userToLink, status, error, startLinking, cancelLinking, linkAccount,
+    ({classes, targetUser, userToLink, status, isProcessing, error, startLinking, cancelLinking, linkAccount,
       remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
+            { isProcessing && <Grid container item direction="column" alignContent="center" spacing={24}>
+                <Grid item>
+                    Loading user info. Please wait.
+                </Grid>
+                <Grid item style={{ alignSelf: 'center' }}>
+                    <CircularProgress/>
+                </Grid>
+            </Grid> }
             { status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
                 { isLocalUser(targetUser.uuid, localCluster) ? <Grid container spacing={24}>
                     <Grid container item direction="column" spacing={24}>
@@ -164,6 +174,6 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     </Grid>
                 </Grid>
             </Grid> }
-            </CardContent>
-        </Card> ;
+        </CardContent>
+    </Card>;
 });
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index 3bdfbe43..c3ad51cf 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -22,13 +22,14 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
         targetUser: state.linkAccountPanel.targetUser,
         userToLink: state.linkAccountPanel.userToLink,
         status: state.linkAccountPanel.status,
-        error: state.linkAccountPanel.error
+        error: state.linkAccountPanel.error,
+        isProcessing: state.linkAccountPanel.isProcessing
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
     startLinking: (type: LinkAccountType) => dispatch<any>(startLinking(type)),
-    cancelLinking: () => dispatch<any>(cancelLinking()),
+    cancelLinking: () => dispatch<any>(cancelLinking(true)),
     linkAccount: () => dispatch<any>(linkAccount()),
     setSelectedCluster: (selectedCluster: string) => dispatch<any>(linkAccountPanelActions.SET_SELECTED_CLUSTER({selectedCluster}))
 });

commit 122fba47e0b0629ebb449c5206ba400021bc6de1
Merge: eb9611ea cd10c61b
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Fri May 17 17:19:51 2019 -0400

    Merge remote-tracking branch 'origin/master' into 15088-merge-account


commit eb9611ea28acee0a8fdef772ef8d1b31ac689e4c
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Fri May 17 15:04:17 2019 -0400

    15088: Improves federated linking logic and UI
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/models/link-account.ts b/src/models/link-account.ts
index 1c2029cf..f5b60400 100644
--- a/src/models/link-account.ts
+++ b/src/models/link-account.ts
@@ -10,7 +10,9 @@ export enum LinkAccountStatus {
 
 export enum LinkAccountType {
     ADD_OTHER_LOGIN,
-    ACCESS_OTHER_ACCOUNT
+    ADD_LOCAL_TO_REMOTE,
+    ACCESS_OTHER_ACCOUNT,
+    ACCESS_OTHER_REMOTE_ACCOUNT
 }
 
 export interface AccountToLink {
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 76f5c32d..7e6897a8 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { matchPath } from 'react-router';
+import { matchPath, Router } from 'react-router';
 import { ResourceKind, RESOURCE_UUID_PATTERN, extractUuidKind } from '~/models/resource';
 import { getProjectUrl } from '~/models/project';
 import { getCollectionUrl } from '~/models/collection';
@@ -127,6 +127,9 @@ export const matchKeepServicesRoute = (route: string) =>
 export const matchTokenRoute = (route: string) =>
     matchPath(route, { path: Routes.TOKEN });
 
+export const matchFedTokenRoute = (route: string) =>
+    matchPath(route, {path: Routes.FED_LOGIN});
+
 export const matchUsersRoute = (route: string) =>
     matchPath(route, { path: Routes.USERS });
 
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index 6ca71403..87eb3f75 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -13,7 +13,7 @@ import { Session } from "~/models/session";
 import { getDiscoveryURL, Config } from '~/common/config';
 import { initSessions } from "~/store/auth/auth-action-session";
 import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
-import { matchTokenRoute } from '~/routes/routes';
+import { matchTokenRoute, matchFedTokenRoute } from '~/routes/routes';
 import Axios from "axios";
 import { AxiosError } from "axios";
 
@@ -54,7 +54,7 @@ export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () =>
     // Cancel any link account ops in progess unless the user has
     // just logged in or there has been a successful link operation
     const data = services.linkAccountService.getLinkOpStatus();
-    if (!matchTokenRoute(location.pathname) && data === undefined) {
+    if (!matchTokenRoute(location.pathname) && (!matchFedTokenRoute(location.pathname)) && data === undefined) {
         dispatch<any>(cancelLinking());
     }
 
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index eec5bf3d..7a92f4aa 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -99,8 +99,17 @@ export const linkFailed = () =>
 
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        // If there are remote hosts, set the initial selected cluster by getting the first cluster that isn't the local cluster
         if (getState().linkAccountPanel.selectedCluster === undefined) {
-            dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster: getState().auth.localCluster }));
+            const localCluster = getState().auth.localCluster;
+            let selectedCluster = localCluster;
+            for (const key in getState().auth.remoteHosts) {
+                if (key !== localCluster) {
+                    selectedCluster = key;
+                    break;
+                }
+            }
+            dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }));
         }
 
         // First check if an account link operation has completed
@@ -124,7 +133,7 @@ export const loadLinkAccountPanel = () =>
                 dispatch(saveApiToken(curToken));
 
                 let params: any;
-                if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
+                if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT || linkAccountData.type === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
                     params = {
                         originatingUser: OriginatingUser.USER_TO_LINK,
                         targetUser: curUserResource,
@@ -133,7 +142,7 @@ export const loadLinkAccountPanel = () =>
                         userToLinkToken: linkAccountData.token
                     };
                 }
-                else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
+                else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN || linkAccountData.type === LinkAccountType.ADD_LOCAL_TO_REMOTE) {
                     params = {
                         originatingUser: OriginatingUser.TARGET_USER,
                         targetUser: savedUserResource,
@@ -176,9 +185,14 @@ 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.saveAccountToLink(accountToLink);
+
         const auth = getState().auth;
-        const selectedCluster = getState().linkAccountPanel.selectedCluster;
-        const homeCluster = selectedCluster ? selectedCluster : auth.homeCluster;
+        const isLocalUser = auth.user!.uuid.substring(0,5) === auth.localCluster;
+        let homeCluster = auth.localCluster;
+        if (isLocalUser && t === LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT) {
+            homeCluster = getState().linkAccountPanel.selectedCluster!;
+        }
+
         dispatch(logout());
         dispatch(login(auth.localCluster, homeCluster, auth.remoteHosts));
     };
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 581e86dd..12dae426 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -33,6 +33,7 @@ export interface LinkAccountPanelRootDataProps {
     userToLink?: UserResource;
     remoteHosts:  { [key: string]: string };
     hasRemoteHosts: boolean;
+    localCluster: string;
     status : LinkAccountPanelStatus;
     error: LinkAccountPanelError;
     selectedCluster?: string;
@@ -58,55 +59,88 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false, showClu
     return disp;
 }
 
+function isLocalUser(uuid: string, localCluster: string) {
+    return uuid.substring(0, 5) === localCluster;
+}
+
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
     ({classes, targetUser, userToLink, status, error, startLinking, cancelLinking, linkAccount,
-      remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster}: LinkAccountPanelRootProps) => {
+      remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-            { status === LinkAccountPanelStatus.INITIAL && targetUser &&
-            <Grid container spacing={24}>
-                <Grid container item direction="column" spacing={24}>
-                    <Grid item>
-                        You are currently logged in as {displayUser(targetUser, true, hasRemoteHosts)}
+            { status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
+                { isLocalUser(targetUser.uuid, localCluster) ? <Grid container spacing={24}>
+                    <Grid container item direction="column" spacing={24}>
+                        <Grid item>
+                            You are currently logged in as {displayUser(targetUser, true)}
+                        </Grid>
+                        <Grid item>
+                            You can link Arvados accounts. After linking, either login will take you to the same account.
+                        </Grid >
                     </Grid>
-                    <Grid item>
-                        You can link Arvados accounts. After linking, either login will take you to the same account.
-                    </Grid >
-                    {hasRemoteHosts && selectedCluster && <Grid item>
-                        Please select the cluster that hosts the account you want to link with:
-                            <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
-                                onChange={(event) => setSelectedCluster(event.target.value)}>
-                                {Object.keys(remoteHosts).map((k) => <option key={k} value={k}>{k}</option>)}
-                            </Select>
-                    </Grid> }
-                </Grid>
-                <Grid container item direction="row" spacing={24}>
-                    <Grid item>
-                        <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
-                            Add another login {hasRemoteHosts ? <label> from {selectedCluster} </label> : null} to this account
-                        </Button>
+                    <Grid container item direction="row" spacing={24}>
+                        <Grid item>
+                            <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
+                                Add another login to this account
+                            </Button>
+                        </Grid>
+                        <Grid item>
+                            <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
+                                Use this login to access another account
+                            </Button>
+                        </Grid>
                     </Grid>
-                    <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
-                            Use this login to access another account {hasRemoteHosts ? <label> on {selectedCluster} </label> : null}
-                        </Button>
+                    { hasRemoteHosts && selectedCluster && <Grid container item direction="column" spacing={24}>
+                        <Grid item>
+                            You can also link {displayUser(targetUser, false)} with an account from a remote cluster.
+                        </Grid>
+                        <Grid item>
+                            Please select the cluster that hosts the account you want to link with:
+                                <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
+                                    onChange={(event) => setSelectedCluster(event.target.value)}>
+                                    {Object.keys(remoteHosts).map((k) => k !== localCluster ? <option key={k} value={k}>{k}</option> : null)}
+                                </Select>
+                            </Grid>
+                        <Grid item>
+                            <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT)}>
+                                Link with an account on {hasRemoteHosts ? <label>{selectedCluster} </label> : null}
+                            </Button>
+                        </Grid>
+                    </Grid> }
+                </Grid> :
+                <Grid container spacing={24}>
+                    <Grid container item direction="column" spacing={24}>
+                        <Grid item>
+                            You are currently logged in as {displayUser(targetUser, true, true)}
+                        </Grid>
+                        <Grid item>
+                            This a remote account. You can link a local Arvados account to this one. After linking, you can access the local account's data by logging into the <b>{localCluster}</b> cluster with the <b>{targetUser.email}</b> account.
+                        </Grid >
+                        <Grid item>
+                            <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
+                                Link an account from {localCluster} to this account
+                            </Button>
+                        </Grid>
                     </Grid>
-                </Grid>
-            </Grid> }
+                </Grid>}
+            </div> }
             { (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
             <Grid container spacing={24}>
                 { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        Clicking 'Link accounts' will link {displayUser(userToLink, true, hasRemoteHosts)} to {displayUser(targetUser, true, hasRemoteHosts)}.
+                        Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
                     </Grid>
-                    <Grid item>
+                    { (isLocalUser(targetUser.uuid, localCluster)) && <Grid item>
                         After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
-                    </Grid>
+                    </Grid> }
                     <Grid item>
-                       Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
+                        Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
                     </Grid>
+                    { !isLocalUser(targetUser.uuid, localCluster) && <Grid item>
+                        You can access <b>{userToLink.email}</b> data by logging into <b>{localCluster}</b> with the <b>{targetUser.email}</b> account.
+                    </Grid> }
                 </Grid> }
                 { error === LinkAccountPanelError.NON_ADMIN && <Grid item>
                     Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}.
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index 06359111..3bdfbe43 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -18,6 +18,7 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
         remoteHosts: state.auth.remoteHosts,
         hasRemoteHosts: Object.keys(state.auth.remoteHosts).length > 1,
         selectedCluster: state.linkAccountPanel.selectedCluster,
+        localCluster: state.auth.localCluster,
         targetUser: state.linkAccountPanel.targetUser,
         userToLink: state.linkAccountPanel.userToLink,
         status: state.linkAccountPanel.status,

commit 842eaeb05a5df83493e9448bc720760d9cc288cc
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Thu May 16 10:06:13 2019 -0400

    15088: Fixes UI on link account failure
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index b531b82d..eec5bf3d 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -87,15 +87,14 @@ export const linkFailed = () =>
         if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
             if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
-                dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
             }
             else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
                 dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
-                dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.userToLink}));
             }
-            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
         }
         services.linkAccountService.removeAccountToLink();
+        services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.FAILED);
+        location.reload();
     };
 
 export const loadLinkAccountPanel = () =>

commit 6e1ba15c736bacce7dc187f24a33216bb9ad37f3
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 15 15:46:40 2019 -0400

    15088: Adds federated account linking
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index e63da1a4..b531b82d 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -18,7 +18,8 @@ import { progressIndicatorActions } from "~/store/progress-indicator/progress-in
 import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
 
 export const linkAccountPanelActions = unionize({
-    LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
+    LINK_INIT: ofType<{
+        targetUser: UserResource | undefined }>(),
     LINK_LOAD: ofType<{
         originatingUser: OriginatingUser | undefined,
         targetUser: UserResource | undefined,
@@ -30,6 +31,8 @@ export const linkAccountPanelActions = unionize({
         targetUser: UserResource | undefined,
         userToLink: UserResource | undefined,
         error: LinkAccountPanelError }>(),
+    SET_SELECTED_CLUSTER: ofType<{
+        selectedCluster: string }>(),
     HAS_SESSION_DATA: {}
 });
 
@@ -97,6 +100,10 @@ export const linkFailed = () =>
 
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        if (getState().linkAccountPanel.selectedCluster === undefined) {
+            dispatch(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster: getState().auth.localCluster }));
+        }
+
         // First check if an account link operation has completed
         dispatch(checkForLinkStatus());
 
@@ -171,8 +178,10 @@ export const startLinking = (t: LinkAccountType) =>
         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
         services.linkAccountService.saveAccountToLink(accountToLink);
         const auth = getState().auth;
+        const selectedCluster = getState().linkAccountPanel.selectedCluster;
+        const homeCluster = selectedCluster ? selectedCluster : auth.homeCluster;
         dispatch(logout());
-        dispatch(login(auth.localCluster, auth.homeCluster, auth.remoteHosts));
+        dispatch(login(auth.localCluster, homeCluster, auth.remoteHosts));
     };
 
 export const getAccountLinkData = () =>
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 0e61abdd..3d205842 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -27,6 +27,7 @@ export enum OriginatingUser {
 }
 
 export interface LinkAccountPanelState {
+    selectedCluster: string | undefined;
     originatingUser: OriginatingUser | undefined;
     targetUser: UserResource | undefined;
     targetUserToken: string | undefined;
@@ -37,6 +38,7 @@ export interface LinkAccountPanelState {
 }
 
 const initialState = {
+    selectedCluster: undefined,
     originatingUser: OriginatingUser.NONE,
     targetUser: undefined,
     targetUserToken: undefined,
@@ -50,22 +52,28 @@ export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialSt
     linkAccountPanelActions.match(action, {
         default: () => state,
         LINK_INIT: ({ targetUser }) => ({
+            ...state,
             targetUser, targetUserToken: undefined,
             userToLink: undefined, userToLinkToken: undefined,
             status: LinkAccountPanelStatus.INITIAL, error: LinkAccountPanelError.NONE, originatingUser: OriginatingUser.NONE
         }),
         LINK_LOAD: ({ originatingUser, userToLink, targetUser, targetUserToken, userToLinkToken}) => ({
+            ...state,
             originatingUser,
             targetUser, targetUserToken,
             userToLink, userToLinkToken,
             status: LinkAccountPanelStatus.LINKING, error: LinkAccountPanelError.NONE
         }),
-        LINK_INVALID: ({originatingUser, targetUser, userToLink, error}) => ({
+        LINK_INVALID: ({ originatingUser, targetUser, userToLink, error }) => ({
+            ...state,
             originatingUser,
             targetUser, targetUserToken: undefined,
             userToLink, userToLinkToken: undefined,
             error, status: LinkAccountPanelStatus.ERROR
         }),
+        SET_SELECTED_CLUSTER: ({ selectedCluster }) => ({
+            ...state, selectedCluster
+        }),
         HAS_SESSION_DATA: () => ({
             ...state, status: LinkAccountPanelStatus.HAS_SESSION_DATA
         })
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 19c4b97a..581e86dd 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -11,6 +11,7 @@ import {
     CardContent,
     Button,
     Grid,
+    Select
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { UserResource } from "~/models/user";
@@ -30,19 +31,27 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export interface LinkAccountPanelRootDataProps {
     targetUser?: UserResource;
     userToLink?: UserResource;
+    remoteHosts:  { [key: string]: string };
+    hasRemoteHosts: boolean;
     status : LinkAccountPanelStatus;
     error: LinkAccountPanelError;
+    selectedCluster?: string;
 }
 
 export interface LinkAccountPanelRootActionProps {
     startLinking: (type: LinkAccountType) => void;
     cancelLinking: () => void;
     linkAccount: () => void;
+    setSelectedCluster: (cluster: string) => void;
 }
 
-function displayUser(user: UserResource, showCreatedAt: boolean = false) {
+function displayUser(user: UserResource, showCreatedAt: boolean = false, showCluster: boolean = false) {
     const disp = [];
     disp.push(<span><b>{user.email}</b> ({user.username}, {user.uuid})</span>);
+    if (showCluster) {
+        const homeCluster = user.uuid.substr(0,5);
+        disp.push(<span> hosted on cluster <b>{homeCluster}</b> and </span>);
+    }
     if (showCreatedAt) {
         disp.push(<span> created on <b>{formatDate(user.createdAt, true)}</b></span>);
     }
@@ -52,28 +61,36 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false) {
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, targetUser, userToLink, status, error, startLinking, cancelLinking, linkAccount}: LinkAccountPanelRootProps) => {
+    ({classes, targetUser, userToLink, status, error, startLinking, cancelLinking, linkAccount,
+      remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
             { status === LinkAccountPanelStatus.INITIAL && targetUser &&
             <Grid container spacing={24}>
                 <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        You are currently logged in as {displayUser(targetUser, true)}
+                        You are currently logged in as {displayUser(targetUser, true, hasRemoteHosts)}
                     </Grid>
                     <Grid item>
                         You can link Arvados accounts. After linking, either login will take you to the same account.
-                    </Grid>
+                    </Grid >
+                    {hasRemoteHosts && selectedCluster && <Grid item>
+                        Please select the cluster that hosts the account you want to link with:
+                            <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
+                                onChange={(event) => setSelectedCluster(event.target.value)}>
+                                {Object.keys(remoteHosts).map((k) => <option key={k} value={k}>{k}</option>)}
+                            </Select>
+                    </Grid> }
                 </Grid>
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
                         <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
-                            Add another login to this account
+                            Add another login {hasRemoteHosts ? <label> from {selectedCluster} </label> : null} to this account
                         </Button>
                     </Grid>
                     <Grid item>
                         <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
-                            Use this login to access another account
+                            Use this login to access another account {hasRemoteHosts ? <label> on {selectedCluster} </label> : null}
                         </Button>
                     </Grid>
                 </Grid>
@@ -82,7 +99,7 @@ export const LinkAccountPanelRoot = withStyles(styles) (
             <Grid container spacing={24}>
                 { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        Clicking 'Link accounts' will link {displayUser(userToLink, true)} to {displayUser(targetUser, true)}.
+                        Clicking 'Link accounts' will link {displayUser(userToLink, true, hasRemoteHosts)} to {displayUser(targetUser, true, hasRemoteHosts)}.
                     </Grid>
                     <Grid item>
                         After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index f620b568..06359111 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -5,7 +5,7 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { startLinking, cancelLinking, linkAccount } from '~/store/link-account-panel/link-account-panel-actions';
+import { startLinking, cancelLinking, linkAccount, linkAccountPanelActions } from '~/store/link-account-panel/link-account-panel-actions';
 import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
@@ -15,6 +15,9 @@ import {
 
 const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     return {
+        remoteHosts: state.auth.remoteHosts,
+        hasRemoteHosts: Object.keys(state.auth.remoteHosts).length > 1,
+        selectedCluster: state.linkAccountPanel.selectedCluster,
         targetUser: state.linkAccountPanel.targetUser,
         userToLink: state.linkAccountPanel.userToLink,
         status: state.linkAccountPanel.status,
@@ -25,7 +28,8 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
 const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
     startLinking: (type: LinkAccountType) => dispatch<any>(startLinking(type)),
     cancelLinking: () => dispatch<any>(cancelLinking()),
-    linkAccount: () => dispatch<any>(linkAccount())
+    linkAccount: () => dispatch<any>(linkAccount()),
+    setSelectedCluster: (selectedCluster: string) => dispatch<any>(linkAccountPanelActions.SET_SELECTED_CLUSTER({selectedCluster}))
 });
 
 export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);

commit 06f1c8b752ff3f2b788773eac42438335bb86a91
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 15 11:35:08 2019 -0400

    15088: Fixes tests broken by the merge
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/auth/auth-action.test.ts b/src/store/auth/auth-action.test.ts
index 11f29e45..4a69fc15 100644
--- a/src/store/auth/auth-action.test.ts
+++ b/src/store/auth/auth-action.test.ts
@@ -64,6 +64,7 @@ describe('auth-actions', () => {
             sshKeys: [],
             homeCluster: "zzzzz",
             localCluster: "zzzzz",
+            remoteHostsConfig: {},
             remoteHosts: {
                 zzzzz: "zzzzz.arvadosapi.com",
                 xc59z: "xc59z.arvadosapi.com"
diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts
index 38cf1581..14d92803 100644
--- a/src/store/auth/auth-reducer.test.ts
+++ b/src/store/auth/auth-reducer.test.ts
@@ -43,7 +43,8 @@ describe('auth-reducer', () => {
             sessions: [],
             homeCluster: "zzzzz",
             localCluster: "",
-            remoteHosts: {}
+            remoteHosts: {},
+            remoteHostsConfig: {}
         });
     });
 
@@ -59,6 +60,7 @@ describe('auth-reducer', () => {
             homeCluster: "",
             localCluster: "",
             remoteHosts: {},
+            remoteHostsConfig: {}
         });
     });
 
@@ -85,6 +87,7 @@ describe('auth-reducer', () => {
             homeCluster: "",
             localCluster: "",
             remoteHosts: {},
+            remoteHostsConfig: {},
             user: {
                 email: "test at test.com",
                 firstName: "John",

commit 3bc058f0bcd746912230e29683509d692fa8203e
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 15 11:29:53 2019 -0400

    15088: Fix to use new login function
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 55528824..e63da1a4 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -172,7 +172,7 @@ export const startLinking = (t: LinkAccountType) =>
         services.linkAccountService.saveAccountToLink(accountToLink);
         const auth = getState().auth;
         dispatch(logout());
-        dispatch(login(auth.localCluster, auth.remoteHosts[auth.homeCluster]));
+        dispatch(login(auth.localCluster, auth.homeCluster, auth.remoteHosts));
     };
 
 export const getAccountLinkData = () =>
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 27ac76f3..9917782a 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -94,7 +94,6 @@ import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-deta
 import { DataTableFetchMode } from "~/components/data-table/data-table";
 import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action';
 import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
-import { USER_LINK_ACCOUNT_KEY } from '~/services/link-account-service/link-account-service';
 import { loadCollectionsContentAddressPanel, collectionsContentAddressActions } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
 import { collectionContentAddressPanelColumns } from '~/views/collection-content-address-panel/collection-content-address-panel';
 
@@ -117,7 +116,7 @@ const handleFirstTimeLoad = (action: any) =>
     };
 
 export const loadWorkbench = () =>
-    async (dispatch: Dispatch, getState: () => RootState) => {
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
         const { auth, router } = getState();
         const { user } = auth;
@@ -138,7 +137,7 @@ export const loadWorkbench = () =>
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
 
-            if (sessionStorage.getItem(USER_LINK_ACCOUNT_KEY)) {
+            if (services.linkAccountService.getAccountToLink()) {
                 dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
             }
 
diff --git a/src/views-components/api-token/api-token.tsx b/src/views-components/api-token/api-token.tsx
index 2f8d87e1..fae099d5 100644
--- a/src/views-components/api-token/api-token.tsx
+++ b/src/views-components/api-token/api-token.tsx
@@ -31,12 +31,12 @@ export const ApiToken = connect()(
                 this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
             }).finally(() => {
                 if (loadMainApp) {
-                if (this.props.dispatch(getAccountLinkData())) {
-                    this.props.dispatch(navigateToLinkAccount);
-                }
-                else {
-                    this.props.dispatch(navigateToRootProject);
-                }
+                    if (this.props.dispatch(getAccountLinkData())) {
+                        this.props.dispatch(navigateToLinkAccount);
+                    }
+                    else {
+                        this.props.dispatch(navigateToRootProject);
+                    }
                 }
             });
         }

commit 3b53b656e65fdabc32b3bc748074eb35e9df98eb
Merge: 967f2bb4 53a66ff4
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 15 11:04:57 2019 -0400

    Merge branch 'master' into 15088-merge-account

diff --cc src/store/auth/auth-action.ts
index 7ee83992,c088418a..6ca71403
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@@ -8,12 -8,12 +8,14 @@@ import { AxiosInstance } from "axios"
  import { RootState } from "../store";
  import { ServiceRepository } from "~/services/services";
  import { SshKeyResource } from '~/models/ssh-key';
 -import { User } from "~/models/user";
 +import { User, UserResource } from "~/models/user";
  import { Session } from "~/models/session";
- import { Config } from '~/common/config';
+ import { getDiscoveryURL, Config } from '~/common/config';
  import { initSessions } from "~/store/auth/auth-action-session";
 +import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
 +import { matchTokenRoute } from '~/routes/routes';
+ import Axios from "axios";
+ import { AxiosError } from "axios";
  
  export const authActions = unionize({
      SAVE_API_TOKEN: ofType<string>(),
@@@ -48,15 -48,9 +51,16 @@@ function removeAuthorizationHeader(clie
  }
  
  export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    // Cancel any link account ops in progess unless the user has
 +    // just logged in or there has been a successful link operation
 +    const data = services.linkAccountService.getLinkOpStatus();
 +    if (!matchTokenRoute(location.pathname) && data === undefined) {
 +        dispatch<any>(cancelLinking());
 +    }
 +
      const user = services.authService.getUser();
      const token = services.authService.getApiToken();
+     const homeCluster = services.authService.getHomeCluster();
      if (token) {
          setAuthorizationHeader(services, token);
      }
@@@ -76,13 -82,8 +92,13 @@@ export const saveApiToken = (token: str
      dispatch(authActions.SAVE_API_TOKEN(token));
  };
  
 +export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    services.authService.saveUser(user);
 +    dispatch(authActions.SAVE_USER(user));
 +};
 +
- export const login = (uuidPrefix: string, homeCluster: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-     services.authService.login(uuidPrefix, homeCluster);
+ export const login = (uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+     services.authService.login(uuidPrefix, homeCluster, remoteHosts);
      dispatch(authActions.LOGIN());
  };
  
diff --cc src/store/store.ts
index 6a37572b,ff9a495e..8a2ca240
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@@ -62,7 -62,9 +62,10 @@@ import { ApiClientAuthorizationMiddlewa
  import { PublicFavoritesMiddlewareService } from '~/store/public-favorites-panel/public-favorites-middleware-service';
  import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
  import { publicFavoritesReducer } from '~/store/public-favorites/public-favorites-reducer';
 +import { linkAccountPanelReducer } from './link-account-panel/link-account-panel-reducer';
+ import { CollectionsWithSameContentAddressMiddlewareService } from '~/store/collections-content-address-panel/collections-content-address-middleware-service';
+ import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+ import { ownerNameReducer } from '~/store/owner-name/owner-name-reducer';
  
  const composeEnhancers =
      (process.env.NODE_ENV === 'development' &&
diff --cc src/store/workbench/workbench-actions.ts
index 0cffcace,2363b579..27ac76f3
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@@ -95,7 -93,8 +94,9 @@@ import { groupDetailsPanelColumns } fro
  import { DataTableFetchMode } from "~/components/data-table/data-table";
  import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action';
  import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
 +import { USER_LINK_ACCOUNT_KEY } from '~/services/link-account-service/link-account-service';
+ import { loadCollectionsContentAddressPanel, collectionsContentAddressActions } from '~/store/collections-content-address-panel/collections-content-address-panel-actions';
+ import { collectionContentAddressPanelColumns } from '~/views/collection-content-address-panel/collection-content-address-panel';
  
  export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
  
@@@ -135,11 -134,8 +136,12 @@@ export const loadWorkbench = () =
              dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
              dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
              dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
+             dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
  
 +            if (sessionStorage.getItem(USER_LINK_ACCOUNT_KEY)) {
 +                dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
 +            }
 +
              dispatch<any>(initSidePanelTree());
              if (router.location) {
                  const match = matchRootRoute(router.location.pathname);
diff --cc src/views-components/api-token/api-token.tsx
index b0fd0313,b78e7192..2f8d87e1
--- a/src/views-components/api-token/api-token.tsx
+++ b/src/views-components/api-token/api-token.tsx
@@@ -28,12 -29,9 +30,14 @@@ export const ApiToken = connect()
              this.props.dispatch<any>(getUserDetails()).then((user: User) => {
                  this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
              }).finally(() => {
+                 if (loadMainApp) {
 +                if (this.props.dispatch(getAccountLinkData())) {
 +                    this.props.dispatch(navigateToLinkAccount);
 +                }
 +                else {
                      this.props.dispatch(navigateToRootProject);
                  }
++                }
              });
          }
          render() {
diff --cc src/views/workbench/workbench.tsx
index e852150c,20cbbdea..d015d4ec
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@@ -93,7 -92,8 +93,9 @@@ import { GroupMemberAttributesDialog } 
  import { AddGroupMembersDialog } from '~/views-components/dialog-forms/add-group-member-dialog';
  import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
  import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-panel';
 +import { LinkAccountPanel } from '~/views/link-account-panel/link-account-panel';
+ import { FedLogin } from './fed-login';
+ import { CollectionsContentAddressPanel } from '~/views/collection-content-address-panel/collection-content-address-panel';
  
  type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
  
@@@ -183,7 -178,7 +185,8 @@@ export const WorkbenchPanel 
                                  <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
                                  <Route path={Routes.LINKS} component={LinkPanel} />
                                  <Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
 +                                <Route path={Routes.LINK_ACCOUNT} component={LinkAccountPanel} />
+                                 <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
                              </Switch>
                          </Grid>
                      </Grid>

commit 967f2bb4cc911c0f3dae6246ac81e19c177751ba
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue May 14 17:32:04 2019 -0400

    15088: Moves link account operation check to initAuth
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/index.tsx b/src/index.tsx
index 014627a9..9f9b27ca 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -64,9 +64,6 @@ 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()}]`);
 
@@ -111,13 +108,6 @@ 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());
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index 8be02ed0..7ee83992 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -12,7 +12,8 @@ import { User, UserResource } from "~/models/user";
 import { Session } from "~/models/session";
 import { Config } from '~/common/config';
 import { initSessions } from "~/store/auth/auth-action-session";
-import { UserRepositoryCreate } from '~/views-components/dialog-create/dialog-user-create';
+import { cancelLinking } from '~/store/link-account-panel/link-account-panel-actions';
+import { matchTokenRoute } from '~/routes/routes';
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
@@ -47,6 +48,13 @@ function removeAuthorizationHeader(client: AxiosInstance) {
 }
 
 export const initAuth = (config: Config) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    // Cancel any link account ops in progess unless the user has
+    // just logged in or there has been a successful link operation
+    const data = services.linkAccountService.getLinkOpStatus();
+    if (!matchTokenRoute(location.pathname) && data === undefined) {
+        dispatch<any>(cancelLinking());
+    }
+
     const user = services.authService.getUser();
     const token = services.authService.getApiToken();
     if (token) {

commit c801917eb9aeabf5104e5512dd17abf932b66a55
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue May 14 12:00:12 2019 -0400

    15088: Removes unused my-account-panel action
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/my-account/my-account-panel-actions.ts b/src/store/my-account/my-account-panel-actions.ts
index 294f77f6..34bb2693 100644
--- a/src/store/my-account/my-account-panel-actions.ts
+++ b/src/store/my-account/my-account-panel-actions.ts
@@ -9,7 +9,6 @@ import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { authActions } from "~/store/auth/auth-action";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
-import { navigateToLinkAccount } from '~/store/navigation/navigation-action';
 
 export const MY_ACCOUNT_FORM = 'myAccountForm';
 
@@ -18,11 +17,6 @@ export const loadMyAccountPanel = () =>
         dispatch(setBreadcrumbs([{ label: 'User profile'}]));
     };
 
-export const openLinkAccount = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        dispatch<any>(navigateToLinkAccount);
-    };
-
 export const saveEditedUser = (resource: any) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         try {

commit e954cfb45dbe418c151144cc42847b848c9b0ebf
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Mon May 13 16:34:30 2019 -0400

    15088: Handles browser navigation during link account ops
    
    - Inactive page link account button brings the user to the initial link account page
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/index.tsx b/src/index.tsx
index 9f9b27ca..014627a9 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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());
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 02835fcc..ba7e2a45 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -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 });
 
diff --git a/src/services/link-account-service/link-account-service.ts b/src/services/link-account-service/link-account-service.ts
index ebae69ac..42fae365 100644
--- a/src/services/link-account-service/link-account-service.ts
+++ b/src/services/link-account-service/link-account-service.ts
@@ -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;
     }
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index 1198ff8d..8be02ed0 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -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();
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 61dc657e..55528824 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -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
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 80878c34..0e61abdd 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -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
 };
 
diff --git a/src/views/inactive-panel/inactive-panel.tsx b/src/views/inactive-panel/inactive-panel.tsx
index 5f045f69..8d53a21e 100644
--- a/src/views/inactive-panel/inactive-panel.tsx
+++ b/src/views/inactive-panel/inactive-panel.tsx
@@ -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;
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 2439cc92..19c4b97a 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -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>
diff --git a/src/views/main-panel/main-panel-root.tsx b/src/views/main-panel/main-panel-root.tsx
index 51287fa8..43bc7fbc 100644
--- a/src/views/main-panel/main-panel-root.tsx
+++ b/src/views/main-panel/main-panel-root.tsx
@@ -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>
+            </>
 );
diff --git a/src/views/main-panel/main-panel.tsx b/src/views/main-panel/main-panel.tsx
index 178db25c..5bf03da3 100644
--- a/src/views/main-panel/main-panel.tsx
+++ b/src/views/main-panel/main-panel.tsx
@@ -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
     };
 };
 
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 07b05655..e852150c 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -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} />

commit d2da940fb4be60affe0a350c7f82cf91180f1e3a
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Thu May 9 17:39:18 2019 -0400

    15088: Adds link account panel reducer tests
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-reducer.test.ts b/src/store/link-account-panel/link-account-panel-reducer.test.ts
new file mode 100644
index 00000000..4cb88a85
--- /dev/null
+++ b/src/store/link-account-panel/link-account-panel-reducer.test.ts
@@ -0,0 +1,73 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { linkAccountPanelReducer, LinkAccountPanelError, LinkAccountPanelStatus, OriginatingUser } from "~/store/link-account-panel/link-account-panel-reducer";
+import { linkAccountPanelActions } from "~/store/link-account-panel/link-account-panel-actions";
+import { UserResource } from "~/models/user";
+
+describe('link-account-panel-reducer', () => {
+    const initialState = undefined;
+
+    it('handles initial link account state', () => {
+        const targetUser = { } as any;
+        targetUser.username = "targetUser";
+
+        const state = linkAccountPanelReducer(initialState, linkAccountPanelActions.LINK_INIT({targetUser}));
+        expect(state).toEqual({
+            targetUser,
+            targetUserToken: undefined,
+            userToLink: undefined,
+            userToLinkToken: undefined,
+            originatingUser: OriginatingUser.NONE,
+            error: LinkAccountPanelError.NONE,
+            status: LinkAccountPanelStatus.INITIAL
+        });
+    });
+
+    it('handles loaded link account state', () => {
+        const targetUser = { } as any;
+        targetUser.username = "targetUser";
+        const targetUserToken = "targettoken";
+
+        const userToLink = { } as any;
+        userToLink.username = "userToLink";
+        const userToLinkToken = "usertoken";
+
+        const originatingUser = OriginatingUser.TARGET_USER;
+
+        const state = linkAccountPanelReducer(initialState, linkAccountPanelActions.LINK_LOAD({
+            originatingUser, targetUser, targetUserToken, userToLink, userToLinkToken}));
+        expect(state).toEqual({
+            targetUser,
+            targetUserToken,
+            userToLink,
+            userToLinkToken,
+            originatingUser: OriginatingUser.TARGET_USER,
+            error: LinkAccountPanelError.NONE,
+            status: LinkAccountPanelStatus.LINKING
+        });
+    });
+
+    it('handles loaded invalid account state', () => {
+        const targetUser = { } as any;
+        targetUser.username = "targetUser";
+
+        const userToLink = { } as any;
+        userToLink.username = "userToLink";
+
+        const originatingUser = OriginatingUser.TARGET_USER;
+        const error = LinkAccountPanelError.NON_ADMIN;
+
+        const state = linkAccountPanelReducer(initialState, linkAccountPanelActions.LINK_INVALID({targetUser, userToLink, originatingUser, error}));
+        expect(state).toEqual({
+            targetUser,
+            targetUserToken: undefined,
+            userToLink,
+            userToLinkToken: undefined,
+            originatingUser: OriginatingUser.TARGET_USER,
+            error: LinkAccountPanelError.NON_ADMIN,
+            status: LinkAccountPanelStatus.ERROR
+        });
+    });
+});
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index a8533185..80878c34 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -36,7 +36,7 @@ export interface LinkAccountPanelState {
 }
 
 const initialState = {
-    originatingUser: undefined,
+    originatingUser: OriginatingUser.NONE,
     targetUser: undefined,
     targetUserToken: undefined,
     userToLink: undefined,
@@ -49,13 +49,21 @@ export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialSt
     linkAccountPanelActions.match(action, {
         default: () => state,
         LINK_INIT: ({ targetUser }) => ({
-            ...state, targetUser, status: LinkAccountPanelStatus.INITIAL, error: LinkAccountPanelError.NONE, originatingUser: OriginatingUser.NONE
+            targetUser, targetUserToken: undefined,
+            userToLink: undefined, userToLinkToken: undefined,
+            status: LinkAccountPanelStatus.INITIAL, error: LinkAccountPanelError.NONE, originatingUser: OriginatingUser.NONE
         }),
         LINK_LOAD: ({ originatingUser, userToLink, targetUser, targetUserToken, userToLinkToken}) => ({
-            ...state, originatingUser, targetUser, targetUserToken, userToLink, userToLinkToken, status: LinkAccountPanelStatus.LINKING, error: LinkAccountPanelError.NONE
+            originatingUser,
+            targetUser, targetUserToken,
+            userToLink, userToLinkToken,
+            status: LinkAccountPanelStatus.LINKING, error: LinkAccountPanelError.NONE
         }),
         LINK_INVALID: ({originatingUser, targetUser, userToLink, error}) => ({
-            ...state, originatingUser, targetUser, userToLink, error, status: LinkAccountPanelStatus.ERROR
+            originatingUser,
+            targetUser, targetUserToken: undefined,
+            userToLink, userToLinkToken: undefined,
+            error, status: LinkAccountPanelStatus.ERROR
         }),
         HAS_SESSION_DATA: () => ({
             ...state, status: LinkAccountPanelStatus.HAS_SESSION_DATA

commit 2a5e5a512747d9bd92ffe1f89a991879a0897e4e
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Thu May 9 10:44:41 2019 -0400

    15088: Adds reload after link account operation
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/models/link-account.ts b/src/models/link-account.ts
index 8932a335..1c2029cf 100644
--- a/src/models/link-account.ts
+++ b/src/models/link-account.ts
@@ -2,6 +2,12 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+export enum LinkAccountStatus {
+    SUCCESS,
+    CANCELLED,
+    FAILED
+}
+
 export enum LinkAccountType {
     ADD_OTHER_LOGIN,
     ACCESS_OTHER_ACCOUNT
diff --git a/src/services/link-account-service/link-account-service.ts b/src/services/link-account-service/link-account-service.ts
index 74fc5eb6..ebae69ac 100644
--- a/src/services/link-account-service/link-account-service.ts
+++ b/src/services/link-account-service/link-account-service.ts
@@ -4,10 +4,11 @@
 
 import { AxiosInstance } from "axios";
 import { ApiActions } from "~/services/api/api-actions";
-import { AccountToLink } from "~/models/link-account";
+import { AccountToLink, LinkAccountStatus } from "~/models/link-account";
 import { CommonService } from "~/services/common-service/common-service";
 
 export const USER_LINK_ACCOUNT_KEY = 'accountToLink';
+export const ACCOUNT_LINK_STATUS_KEY = 'accountLinkStatus';
 
 export class LinkAccountService {
 
@@ -28,6 +29,19 @@ export class LinkAccountService {
         return data ? JSON.parse(data) as AccountToLink : undefined;
     }
 
+    public saveLinkOpStatus(status: LinkAccountStatus) {
+        sessionStorage.setItem(ACCOUNT_LINK_STATUS_KEY, JSON.stringify(status));
+    }
+
+    public removeLinkOpStatus() {
+        sessionStorage.removeItem(ACCOUNT_LINK_STATUS_KEY);
+    }
+
+    public getLinkOpStatus() {
+        const data = sessionStorage.getItem(ACCOUNT_LINK_STATUS_KEY);
+        return data ? JSON.parse(data) as LinkAccountStatus : undefined;
+    }
+
     public linkAccounts(newUserToken: string, newGroupUuid: string) {
         const params = {
             new_user_token: newUserToken,
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index c175e604..61dc657e 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -7,13 +7,12 @@ import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
-import { LinkAccountType, AccountToLink } from "~/models/link-account";
+import { LinkAccountType, AccountToLink, LinkAccountStatus } from "~/models/link-account";
 import { saveApiToken, saveUser } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { UserResource } from "~/models/user";
 import { GroupResource } from "~/models/group";
 import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
-import { navigateToRootProject } from "../navigation/navigation-action";
 import { login, logout } from "~/store/auth/auth-action";
 
 export const linkAccountPanelActions = unionize({
@@ -47,6 +46,36 @@ function validateLink(userToLink: UserResource, targetUser: UserResource) {
     return LinkAccountPanelError.NONE;
 }
 
+export const checkForLinkStatus = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const status = services.linkAccountService.getLinkOpStatus();
+        if (status !== undefined) {
+            let msg: string;
+            let msgKind: SnackbarKind;
+            if (status.valueOf() === LinkAccountStatus.CANCELLED) {
+                msg = "Account link cancelled!", msgKind = SnackbarKind.INFO;
+            }
+            else if (status.valueOf() === LinkAccountStatus.FAILED) {
+                msg = "Account link failed!", msgKind = SnackbarKind.ERROR;
+            }
+            else if (status.valueOf() === LinkAccountStatus.SUCCESS) {
+                msg = "Account link success!", msgKind = SnackbarKind.SUCCESS;
+            }
+            else {
+                msg = "Unknown Error!", msgKind = SnackbarKind.ERROR;
+            }
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: msg, kind: msgKind, hideDuration: 3000 }));
+            services.linkAccountService.removeLinkOpStatus();
+        }
+    };
+
+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));
@@ -68,11 +97,16 @@ export const linkFailed = () =>
             }
             dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
         }
-        services.linkAccountService.removeFromSession();
+        dispatch(finishLinking(LinkAccountStatus.FAILED));
     };
 
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+
+        // First check if an account link operation has completed
+        dispatch(checkForLinkStatus());
+
+        // Continue loading the link account panel
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
         const curUser = getState().auth.user;
         const curToken = getState().auth.apiToken;
@@ -121,8 +155,7 @@ 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(linkAccountPanelActions.LINK_INIT({targetUser: savedUserResource}));
-                    throw new Error("Invalid link account type.");
+                    dispatch(finishLinking(LinkAccountStatus.FAILED));
                 }
 
                 dispatch(switchUser(params.targetUser, params.targetUserToken));
@@ -174,8 +207,7 @@ export const cancelLinking = () =>
             }
         }
         finally {
-            services.linkAccountService.removeFromSession();
-            dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: user }));
+            dispatch(finishLinking(LinkAccountStatus.CANCELLED));
         }
     };
 
@@ -201,17 +233,14 @@ export const linkAccount = () =>
             try {
                 // The merge api links the user sending the request into the user
                 // specified in the request, so switch users for this api call
-                dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
+                dispatch(saveApiToken(linkState.userToLinkToken));
                 await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
                 dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
-                dispatch(navigateToRootProject);
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link success!', kind: SnackbarKind.SUCCESS, hideDuration: 3000 }));
-                dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
             }
             catch(e) {
                 // If the link operation fails, delete the previously made project
                 try {
-                    dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
+                    dispatch(saveApiToken(linkState.targetUserToken));
                     await services.projectService.delete(newGroup.uuid);
                 }
                 finally {
@@ -220,7 +249,7 @@ export const linkAccount = () =>
                 throw e;
             }
             finally {
-                services.linkAccountService.removeFromSession();
+                dispatch(finishLinking(LinkAccountStatus.SUCCESS));
             }
         }
     };
\ No newline at end of file

commit 24cc808e44765268ddbcecbfed0645b09fe7777d
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 8 17:10:48 2019 -0400

    15088: Function name clarification
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 54657377..c175e604 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -147,7 +147,7 @@ export const loadLinkAccountPanel = () =>
         }
     };
 
-export const saveAccountLinkData = (t: LinkAccountType) =>
+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);
diff --git a/src/views/inactive-panel/inactive-panel.tsx b/src/views/inactive-panel/inactive-panel.tsx
index 25e03b85..5f045f69 100644
--- a/src/views/inactive-panel/inactive-panel.tsx
+++ b/src/views/inactive-panel/inactive-panel.tsx
@@ -5,7 +5,7 @@
 import * as React from 'react';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { saveAccountLinkData } from '~/store/link-account-panel/link-account-panel-actions';
+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';
@@ -39,16 +39,16 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 export interface InactivePanelActionProps {
-    linkAccount: () => void;
+    startLinking: () => void;
 }
 
 const mapDispatchToProps = (dispatch: Dispatch): InactivePanelActionProps => ({
-    linkAccount: () => dispatch<any>(saveAccountLinkData(LinkAccountType.ACCESS_OTHER_ACCOUNT))
+    startLinking: () => dispatch<any>(startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT))
 });
 
 type InactivePanelProps =  WithStyles<CssRules> & InactivePanelActionProps;
 
-export const InactivePanel = connect(null, mapDispatchToProps)(withStyles(styles)((({ classes, linkAccount }: InactivePanelProps) =>
+export const InactivePanel = connect(null, mapDispatchToProps)(withStyles(styles)((({ classes, startLinking }: InactivePanelProps) =>
         <Grid container justify="center" alignItems="center" direction="column" spacing={24}
             className={classes.root}
             style={{ marginTop: 56, height: "100%" }}>
@@ -68,7 +68,7 @@ export const InactivePanel = connect(null, mapDispatchToProps)(withStyles(styles
                 </Typography>
             </Grid>
             <Grid item>
-                <Button className={classes.ontop} color="primary" variant="contained" onClick={() => linkAccount()}>
+                <Button className={classes.ontop} color="primary" variant="contained" onClick={() => startLinking()}>
                     Link Account
                 </Button>
             </Grid>
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 50a8b7ca..2439cc92 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -35,7 +35,7 @@ export interface LinkAccountPanelRootDataProps {
 }
 
 export interface LinkAccountPanelRootActionProps {
-    saveAccountLinkData: (type: LinkAccountType) => void;
+    startLinking: (type: LinkAccountType) => void;
     cancelLinking: () => void;
     linkAccount: () => void;
 }
@@ -52,7 +52,7 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false) {
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, targetUser, userToLink, status, error, saveAccountLinkData, cancelLinking, linkAccount}: LinkAccountPanelRootProps) => {
+    ({classes, targetUser, userToLink, status, error, startLinking, cancelLinking, linkAccount}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
             { status === LinkAccountPanelStatus.INITIAL && targetUser &&
@@ -67,12 +67,12 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                 </Grid>
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ADD_OTHER_LOGIN)}>
+                        <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
                             Add another login to this account
                         </Button>
                     </Grid>
                     <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
+                        <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
                             Use this login to access another account
                         </Button>
                     </Grid>
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index 45045a3b..f620b568 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -5,7 +5,7 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { saveAccountLinkData, cancelLinking, linkAccount } from '~/store/link-account-panel/link-account-panel-actions';
+import { startLinking, cancelLinking, linkAccount } from '~/store/link-account-panel/link-account-panel-actions';
 import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
@@ -23,7 +23,7 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
-    saveAccountLinkData: (type: LinkAccountType) => dispatch<any>(saveAccountLinkData(type)),
+    startLinking: (type: LinkAccountType) => dispatch<any>(startLinking(type)),
     cancelLinking: () => dispatch<any>(cancelLinking()),
     linkAccount: () => dispatch<any>(linkAccount())
 });

commit ad1b12ef73a2981ed40f3dbdb8d6e1cc9d35a2cd
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 8 15:36:59 2019 -0400

    15088: Adds logout before logging in to second account
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 5e7a02b4..54657377 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -14,6 +14,7 @@ import { UserResource } from "~/models/user";
 import { GroupResource } from "~/models/group";
 import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
 import { navigateToRootProject } from "../navigation/navigation-action";
+import { login, logout } from "~/store/auth/auth-action";
 
 export const linkAccountPanelActions = unionize({
     LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
@@ -151,7 +152,8 @@ export const saveAccountLinkData = (t: LinkAccountType) =>
         const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
         services.linkAccountService.saveToSession(accountToLink);
         const auth = getState().auth;
-        services.authService.login(auth.localCluster, auth.remoteHosts[auth.homeCluster]);
+        dispatch(logout());
+        dispatch(login(auth.localCluster, auth.remoteHosts[auth.homeCluster]));
     };
 
 export const getAccountLinkData = () =>

commit d4489afef9924986e7a04201602f2b810ddfa9d2
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Wed May 8 11:39:33 2019 -0400

    15088: Changes link account dispplay times to UTC
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/common/formatters.ts b/src/common/formatters.ts
index 60e6cd59..377e78e4 100644
--- a/src/common/formatters.ts
+++ b/src/common/formatters.ts
@@ -4,10 +4,16 @@
 
 import { PropertyValue } from "~/models/search-bar";
 
-export const formatDate = (isoDate?: string | null) => {
+export const formatDate = (isoDate?: string | null, utc: boolean = false) => {
     if (isoDate) {
         const date = new Date(isoDate);
-        const text = date.toLocaleString();
+        let text: string;
+        if (utc) {
+            text = date.toUTCString();
+        }
+        else {
+            text = date.toLocaleString();
+        }
         return text === 'Invalid Date' ? "(none)" : text;
     }
     return "(none)";
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 88730b89..50a8b7ca 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -44,7 +44,7 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false) {
     const disp = [];
     disp.push(<span><b>{user.email}</b> ({user.username}, {user.uuid})</span>);
     if (showCreatedAt) {
-        disp.push(<span> created on <b>{formatDate(user.createdAt)}</b></span>);
+        disp.push(<span> created on <b>{formatDate(user.createdAt, true)}</b></span>);
     }
     return disp;
 }

commit bdfda992c607ed4ca591dbf310e659faa370a881
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue May 7 13:51:00 2019 -0400

    15088: Adds link account feature to inactive page
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index ee58e588..5e7a02b4 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -40,7 +40,7 @@ function validateLink(userToLink: UserResource, targetUser: UserResource) {
     else if (userToLink.isAdmin && !targetUser.isAdmin) {
         return LinkAccountPanelError.NON_ADMIN;
     }
-    else if (userToLink.isActive && !targetUser.isActive) {
+    else if (!targetUser.isActive) {
         return LinkAccountPanelError.INACTIVE;
     }
     return LinkAccountPanelError.NONE;
diff --git a/src/views/inactive-panel/inactive-panel.tsx b/src/views/inactive-panel/inactive-panel.tsx
index abfa1f81..25e03b85 100644
--- a/src/views/inactive-panel/inactive-panel.tsx
+++ b/src/views/inactive-panel/inactive-panel.tsx
@@ -3,15 +3,16 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { connect, DispatchProp } from 'react-redux';
-import { Grid, Typography, Button, Select } from '@material-ui/core';
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { saveAccountLinkData } 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 { login, authActions } from '~/store/auth/auth-action';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { RootState } from '~/store/store';
-import * as classNames from 'classnames';
+import { LinkAccountType } from '~/models/link-account';
 
-type CssRules = 'root' | 'container' | 'title' | 'content' | 'content__bolder' | 'button';
+
+type CssRules = 'root' | 'ontop' | 'title';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -28,51 +29,48 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
             opacity: 0.2,
         }
     },
-    container: {
-        width: '560px',
+    ontop: {
         zIndex: 10
     },
     title: {
         marginBottom: theme.spacing.unit * 6,
         color: theme.palette.grey["800"]
-    },
-    content: {
-        marginBottom: theme.spacing.unit * 3,
-        lineHeight: '1.2rem',
-        color: theme.palette.grey["800"]
-    },
-    'content__bolder': {
-        fontWeight: 'bolder'
-    },
-    button: {
-        boxShadow: 'none'
     }
 });
 
-type LoginPanelProps = DispatchProp<any> & WithStyles<CssRules> & {
-    remoteHosts: { [key: string]: string },
-    homeCluster: string,
-    uuidPrefix: string
-};
+export interface InactivePanelActionProps {
+    linkAccount: () => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): InactivePanelActionProps => ({
+    linkAccount: () => dispatch<any>(saveAccountLinkData(LinkAccountType.ACCESS_OTHER_ACCOUNT))
+});
+
+type InactivePanelProps =  WithStyles<CssRules> & InactivePanelActionProps;
 
-export const InactivePanel = withStyles(styles)(
-    connect((state: RootState) => ({
-        remoteHosts: state.auth.remoteHosts,
-        homeCluster: state.auth.homeCluster,
-        uuidPrefix: state.auth.localCluster
-    }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix }: LoginPanelProps) =>
-        <Grid container justify="center" alignItems="center"
+export const InactivePanel = connect(null, mapDispatchToProps)(withStyles(styles)((({ classes, linkAccount }: InactivePanelProps) =>
+        <Grid container justify="center" alignItems="center" direction="column" spacing={24}
             className={classes.root}
-            style={{ marginTop: 56, overflowY: "auto", height: "100%" }}>
-            <Grid item className={classes.container}>
+            style={{ marginTop: 56, height: "100%" }}>
+            <Grid item>
                 <Typography variant='h6' align="center" className={classes.title}>
                     Hi! You're logged in, but...
-		</Typography>
-                <Typography>
-                    Your account is inactive.
-
-		    An administrator must activate your account before you can get any further.
-		</Typography>
+                </Typography>
+            </Grid>
+            <Grid item>
+                <Typography align="center">
+                    Your account is inactive. An administrator must activate your account before you can get any further.
+                </Typography>
+            </Grid>
+            <Grid item>
+                <Typography align="center">
+                    If you would like to use this login to access another account click "Link Account".
+                </Typography>
+            </Grid>
+            <Grid item>
+                <Button className={classes.ontop} color="primary" variant="contained" onClick={() => linkAccount()}>
+                    Link Account
+                </Button>
             </Grid>
         </Grid >
-    ));
+    )));
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 8661f665..88730b89 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -98,7 +98,7 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     Cannot link {displayUser(targetUser)} to the same account.
                 </Grid> }
                 { error === LinkAccountPanelError.INACTIVE && <Grid item>
-                    Cannot link active account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
+                    Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
                 </Grid> }
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>

commit 70db44a1978e7078ba212a7766cb320aafc26fd6
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue May 7 12:01:13 2019 -0400

    15088: Adds support for inactive account linking
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 5d853b57..ee58e588 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -40,6 +40,9 @@ function validateLink(userToLink: UserResource, targetUser: UserResource) {
     else if (userToLink.isAdmin && !targetUser.isAdmin) {
         return LinkAccountPanelError.NON_ADMIN;
     }
+    else if (userToLink.isActive && !targetUser.isActive) {
+        return LinkAccountPanelError.INACTIVE;
+    }
     return LinkAccountPanelError.NONE;
 }
 
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 93987ff3..a8533185 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -14,6 +14,7 @@ export enum LinkAccountPanelStatus {
 
 export enum LinkAccountPanelError {
     NONE,
+    INACTIVE,
     NON_ADMIN,
     SAME_USER
 }
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index a2cdee60..8661f665 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -97,6 +97,9 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                 { error === LinkAccountPanelError.SAME_USER && <Grid item>
                     Cannot link {displayUser(targetUser)} to the same account.
                 </Grid> }
+                { error === LinkAccountPanelError.INACTIVE && <Grid item>
+                    Cannot link active account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
+                </Grid> }
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
                         <Button variant="contained" onClick={() => cancelLinking()}>

commit d329bdf89ea30acc0e9a95bcb7bc4338f8beeebb
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Mon May 6 15:48:36 2019 -0400

    15088: Removes navigation after second login
    
    - Deletes link account session data if the user logs off
    - Second login goes directly to arvados login
    - After second login, page refresh or navigation cancels link operation
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index bd685280..1198ff8d 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -78,7 +78,10 @@ export const login = (uuidPrefix: string, homeCluster: string) => (dispatch: Dis
     dispatch(authActions.LOGIN());
 };
 
-export const logout = () => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const logout = (deleteLinkData: boolean = false) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    if (deleteLinkData) {
+        services.linkAccountService.removeFromSession();
+    }
     services.authService.removeApiToken();
     services.authService.removeUser();
     removeAuthorizationHeader(services.apiClient);
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 17baa4f5..5d853b57 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -8,7 +8,7 @@ import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
-import { logout, saveApiToken, saveUser } from "~/store/auth/auth-action";
+import { saveApiToken, saveUser } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
 import { UserResource } from "~/models/user";
 import { GroupResource } from "~/models/group";
@@ -28,6 +28,7 @@ export const linkAccountPanelActions = unionize({
         targetUser: UserResource | undefined,
         userToLink: UserResource | undefined,
         error: LinkAccountPanelError }>(),
+    HAS_SESSION_DATA: {}
 });
 
 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
@@ -69,7 +70,6 @@ export const linkFailed = () =>
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
-
         const curUser = getState().auth.user;
         const curToken = getState().auth.apiToken;
         if (curUser && curToken) {
@@ -78,6 +78,16 @@ export const loadLinkAccountPanel = () =>
 
             // 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));
@@ -137,7 +147,8 @@ export const saveAccountLinkData = (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);
-        dispatch(logout());
+        const auth = getState().auth;
+        services.authService.login(auth.localCluster, auth.remoteHosts[auth.homeCluster]);
     };
 
 export const getAccountLinkData = () =>
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 9e0bd3f7..93987ff3 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -7,6 +7,7 @@ import { UserResource } from "~/models/user";
 
 export enum LinkAccountPanelStatus {
     INITIAL,
+    HAS_SESSION_DATA,
     LINKING,
     ERROR
 }
@@ -54,5 +55,8 @@ export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialSt
         }),
         LINK_INVALID: ({originatingUser, targetUser, userToLink, error}) => ({
             ...state, originatingUser, targetUser, userToLink, error, status: LinkAccountPanelStatus.ERROR
+        }),
+        HAS_SESSION_DATA: () => ({
+            ...state, status: LinkAccountPanelStatus.HAS_SESSION_DATA
         })
     });
\ No newline at end of file
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 163a1885..0cffcace 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -59,7 +59,7 @@ import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
 import { loadSshKeysPanel } from '~/store/auth/auth-action-ssh';
 import { loadMyAccountPanel } from '~/store/my-account/my-account-panel-actions';
-import { loadLinkAccountPanel } from '~/store/link-account-panel/link-account-panel-actions';
+import { loadLinkAccountPanel, linkAccountPanelActions } from '~/store/link-account-panel/link-account-panel-actions';
 import { loadSiteManagerPanel } from '~/store/auth/auth-action-session';
 import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
@@ -95,6 +95,7 @@ import { groupDetailsPanelColumns } from '~/views/group-details-panel/group-deta
 import { DataTableFetchMode } from "~/components/data-table/data-table";
 import { loadPublicFavoritePanel, publicFavoritePanelActions } from '~/store/public-favorites-panel/public-favorites-action';
 import { publicFavoritePanelColumns } from '~/views/public-favorites-panel/public-favorites-panel';
+import { USER_LINK_ACCOUNT_KEY } from '~/services/link-account-service/link-account-service';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -135,6 +136,10 @@ export const loadWorkbench = () =>
             dispatch(computeNodesActions.SET_COLUMNS({ columns: computeNodePanelColumns }));
             dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
 
+            if (sessionStorage.getItem(USER_LINK_ACCOUNT_KEY)) {
+                dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
+            }
+
             dispatch<any>(initSidePanelTree());
             if (router.location) {
                 const match = matchRootRoute(router.location.pathname);
diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
index 93aeadae..1b8424c0 100644
--- a/src/views-components/main-app-bar/account-menu.tsx
+++ b/src/views-components/main-app-bar/account-menu.tsx
@@ -83,6 +83,6 @@ export const AccountMenu = withStyles(styles)(
                             className={classes.link}>
                             Switch to Workbench v1</a></MenuItem>
                     <Divider />
-                    <MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
+                    <MenuItem onClick={() => dispatch(logout(true))}>Logout</MenuItem>
                 </DropdownMenu>
                 : null));
diff --git a/src/views/main-panel/main-panel-root.tsx b/src/views/main-panel/main-panel-root.tsx
index 4c64b0b8..51287fa8 100644
--- a/src/views/main-panel/main-panel-root.tsx
+++ b/src/views/main-panel/main-panel-root.tsx
@@ -11,6 +11,7 @@ import { LoginPanel } from '~/views/login-panel/login-panel';
 import { InactivePanel } from '~/views/inactive-panel/inactive-panel';
 import { WorkbenchLoadingScreen } from '~/views/workbench/workbench-loading-screen';
 import { MainAppBar } from '~/views-components/main-app-bar/main-app-bar';
+import { LinkAccountPanel } from '~/views/link-account-panel/link-account-panel';
 
 type CssRules = 'root';
 
@@ -28,23 +29,24 @@ export interface MainPanelRootDataProps {
     loading: boolean;
     buildInfo: string;
     uuidPrefix: string;
+    isNotLinking: boolean;
 }
 
 type MainPanelRootProps = MainPanelRootDataProps & WithStyles<CssRules>;
 
 export const MainPanelRoot = withStyles(styles)(
-    ({ classes, loading, working, user, buildInfo, uuidPrefix }: MainPanelRootProps) =>
+    ({ classes, loading, working, user, buildInfo, uuidPrefix, isNotLinking }: MainPanelRootProps) =>
         loading
             ? <WorkbenchLoadingScreen />
-            : <>
-                <MainAppBar
+            : <> { isNotLinking ? <>
+               <MainAppBar
                     user={user}
                     buildInfo={buildInfo}
                     uuidPrefix={uuidPrefix}>
                     {working ? <LinearProgress color="secondary" /> : null}
                 </MainAppBar>
                 <Grid container direction="column" className={classes.root}>
-                    {user ? (user.isActive ? <WorkbenchPanel /> : <InactivePanel />) : <LoginPanel />}
-                </Grid>
-            </>
+                    { user ? (user.isActive ? <WorkbenchPanel /> : <InactivePanel />) : <LoginPanel />}
+               </Grid>
+            </> : user ? <LinkAccountPanel/> : <LoginPanel /> } </>
 );
diff --git a/src/views/main-panel/main-panel.tsx b/src/views/main-panel/main-panel.tsx
index 5009f129..178db25c 100644
--- a/src/views/main-panel/main-panel.tsx
+++ b/src/views/main-panel/main-panel.tsx
@@ -7,6 +7,7 @@ import { connect } from 'react-redux';
 import { MainPanelRoot, MainPanelRootDataProps } from '~/views/main-panel/main-panel-root';
 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';
 
 const mapStateToProps = (state: RootState): MainPanelRootDataProps => {
     return {
@@ -14,7 +15,8 @@ const mapStateToProps = (state: RootState): MainPanelRootDataProps => {
         working: isSystemWorking(state.progressIndicator),
         loading: isWorkbenchLoading(state),
         buildInfo: state.appInfo.buildInfo,
-        uuidPrefix: state.auth.localCluster
+        uuidPrefix: state.auth.localCluster,
+        isNotLinking: state.linkAccountPanel.status === LinkAccountPanelStatus.INITIAL
     };
 };
 

commit 2f857ebcd67a607b7fde9c0ea4808ac30c591876
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Fri May 3 17:46:34 2019 -0400

    15088: Sets correct account after second login
    
    - Fixes linking admin and non-admin accounts
    - If linking fails or is cancelled, login as the user who originated the link operation
    - Improved some naming to make the direction of the linking more clear
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/services/link-account-service/link-account-service.ts b/src/services/link-account-service/link-account-service.ts
index cc7c62eb..74fc5eb6 100644
--- a/src/services/link-account-service/link-account-service.ts
+++ b/src/services/link-account-service/link-account-service.ts
@@ -6,7 +6,6 @@ import { AxiosInstance } from "axios";
 import { ApiActions } from "~/services/api/api-actions";
 import { AccountToLink } from "~/models/link-account";
 import { CommonService } from "~/services/common-service/common-service";
-import { AuthService } from "../auth-service/auth-service";
 
 export const USER_LINK_ACCOUNT_KEY = 'accountToLink';
 
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 47e27f7d..17baa4f5 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -10,37 +10,62 @@ import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
 import { logout, saveApiToken, saveUser } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { UserResource, User } from "~/models/user";
-import { navigateToRootProject } from "~/store/navigation/navigation-action";
+import { UserResource } from "~/models/user";
 import { GroupResource } from "~/models/group";
-import { LinkAccountPanelError } from "./link-account-panel-reducer";
+import { LinkAccountPanelError, OriginatingUser } from "./link-account-panel-reducer";
+import { navigateToRootProject } from "../navigation/navigation-action";
 
 export const linkAccountPanelActions = unionize({
-    INIT: ofType<{ user: UserResource | undefined }>(),
-    LOAD: ofType<{
-        user: UserResource | undefined,
-        userToken: string | undefined,
+    LINK_INIT: ofType<{ targetUser: UserResource | undefined }>(),
+    LINK_LOAD: ofType<{
+        originatingUser: OriginatingUser | undefined,
+        targetUser: UserResource | undefined,
+        targetUserToken: string | undefined,
         userToLink: UserResource | undefined,
         userToLinkToken: string | undefined }>(),
-    RESET: {},
-    INVALID: ofType<{
-        user: UserResource | undefined,
+    LINK_INVALID: ofType<{
+        originatingUser: OriginatingUser | undefined,
+        targetUser: UserResource | undefined,
         userToLink: UserResource | undefined,
         error: LinkAccountPanelError }>(),
 });
 
 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
 
-function validateLink(user: UserResource, userToLink: UserResource) {
-    if (user.uuid === userToLink.uuid) {
+function validateLink(userToLink: UserResource, targetUser: UserResource) {
+    if (userToLink.uuid === targetUser.uuid) {
         return LinkAccountPanelError.SAME_USER;
     }
-    else if (!user.isAdmin && userToLink.isAdmin) {
+    else if (userToLink.isAdmin && !targetUser.isAdmin) {
         return LinkAccountPanelError.NON_ADMIN;
     }
     return LinkAccountPanelError.NONE;
 }
 
+export const switchUser = (user: UserResource, token: string) =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(saveUser(user));
+        dispatch(saveApiToken(token));
+    };
+
+export const linkFailed = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        // If the link fails, switch to the user account that originated the link operation
+        const linkState = getState().linkAccountPanel;
+        if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
+            if (linkState.originatingUser === OriginatingUser.TARGET_USER) {
+                dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
+                dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
+            }
+            else if ((linkState.originatingUser === OriginatingUser.USER_TO_LINK)) {
+                dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
+                dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.userToLink}));
+            }
+            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000 }));
+        }
+        services.linkAccountService.removeFromSession();
+    };
+
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
@@ -53,38 +78,48 @@ export const loadLinkAccountPanel = () =>
 
             // If there is link account session data, then the user has logged in a second time
             if (linkAccountData) {
-                dispatch<any>(saveApiToken(linkAccountData.token));
+                // 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));
                 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
-                dispatch<any>(saveApiToken(curToken));
+                dispatch(saveApiToken(curToken));
 
                 let params: any;
                 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
                     params = {
-                        user: savedUserResource,
-                        userToken: linkAccountData.token,
-                        userToLink: curUserResource,
-                        userToLinkToken: curToken
+                        originatingUser: OriginatingUser.USER_TO_LINK,
+                        targetUser: curUserResource,
+                        targetUserToken: curToken,
+                        userToLink: savedUserResource,
+                        userToLinkToken: linkAccountData.token
                     };
                 }
                 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
                     params = {
-                        user: curUserResource,
-                        userToken: curToken,
-                        userToLink: savedUserResource,
-                        userToLinkToken: linkAccountData.token
+                        originatingUser: OriginatingUser.TARGET_USER,
+                        targetUser: savedUserResource,
+                        targetUserToken: linkAccountData.token,
+                        userToLink: curUserResource,
+                        userToLinkToken: curToken
                     };
                 }
                 else {
+                    // 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(linkAccountPanelActions.LINK_INIT({targetUser: savedUserResource}));
                     throw new Error("Invalid link account type.");
                 }
 
-                const error = validateLink(params.user, params.userToLink);
+                dispatch(switchUser(params.targetUser, params.targetUserToken));
+                const error = validateLink(params.userToLink, params.targetUser);
                 if (error === LinkAccountPanelError.NONE) {
-                    dispatch<any>(linkAccountPanelActions.LOAD(params));
+                    dispatch(linkAccountPanelActions.LINK_LOAD(params));
                 }
                 else {
-                    dispatch<any>(linkAccountPanelActions.INVALID({
-                        user: params.user,
+                    dispatch(linkAccountPanelActions.LINK_INVALID({
+                        originatingUser: params.originatingUser,
+                        targetUser: params.targetUser,
                         userToLink: params.userToLink,
                         error}));
                     return;
@@ -92,9 +127,8 @@ export const loadLinkAccountPanel = () =>
             }
             else {
                 // If there is no link account session data, set the state to invoke the initial UI
-                dispatch<any>(linkAccountPanelActions.INIT({
-                    user: curUserResource }
-                ));
+                dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: curUserResource }));
+                return;
             }
         }
     };
@@ -111,75 +145,66 @@ export const getAccountLinkData = () =>
         return services.linkAccountService.getFromSession();
     };
 
-export const removeAccountLinkData = () =>
+export const cancelLinking = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        let user: UserResource | undefined;
         try {
+            // When linking is cancelled switch to the originating user (i.e. the user saved in session data)
             const linkAccountData = services.linkAccountService.getFromSession();
             if (linkAccountData) {
-                const savedUser = await services.userService.get(linkAccountData.userUuid);
-                dispatch<any>(saveUser(savedUser));
-                dispatch<any>(saveApiToken(linkAccountData.token));
+                dispatch(saveApiToken(linkAccountData.token));
+                user = await services.userService.get(linkAccountData.userUuid);
+                dispatch(switchUser(user, linkAccountData.token));
             }
         }
         finally {
-            dispatch<any>(navigateToRootProject);
-            dispatch(linkAccountPanelActions.RESET());
             services.linkAccountService.removeFromSession();
+            dispatch(linkAccountPanelActions.LINK_INIT({ targetUser: user }));
         }
     };
 
 export const linkAccount = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const linkState = getState().linkAccountPanel;
-        const currentToken = getState().auth.apiToken;
-        if (linkState.userToLink && linkState.userToLinkToken && linkState.user && linkState.userToken && currentToken) {
+        if (linkState.userToLink && linkState.userToLinkToken && linkState.targetUser && linkState.targetUserToken) {
 
-            // First create a project owned by the "userToLink" to accept everything from the current user
-            const projectName = `Migrated from ${linkState.user.email} (${linkState.user.uuid})`;
+            // First create a project owned by the target user
+            const projectName = `Migrated from ${linkState.userToLink.email} (${linkState.userToLink.uuid})`;
             let newGroup: GroupResource;
             try {
-                dispatch<any>(saveApiToken(linkState.userToLinkToken));
                 newGroup = await services.projectService.create({
                     name: projectName,
                     ensure_unique_name: true
                 });
             }
             catch (e) {
-                dispatch<any>(saveApiToken(currentToken));
-                dispatch(snackbarActions.OPEN_SNACKBAR({
-                    message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000
-                }));
+                dispatch(linkFailed());
                 throw e;
             }
 
             try {
-                // Use the token of the account that is getting merged to call the merge api
-                dispatch<any>(saveApiToken(linkState.userToken));
-                await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid);
-
-                // If the link was successful, switch to the account that was merged with
-                dispatch<any>(saveUser(linkState.userToLink));
-                dispatch<any>(saveApiToken(linkState.userToLinkToken));
+                // The merge api links the user sending the request into the user
+                // specified in the request, so switch users for this api call
+                dispatch(switchUser(linkState.userToLink, linkState.userToLinkToken));
+                await services.linkAccountService.linkAccounts(linkState.targetUserToken, newGroup.uuid);
+                dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
+                dispatch(navigateToRootProject);
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Account link success!', kind: SnackbarKind.SUCCESS, hideDuration: 3000 }));
+                dispatch(linkAccountPanelActions.LINK_INIT({targetUser: linkState.targetUser}));
             }
             catch(e) {
                 // If the link operation fails, delete the previously made project
-                // and stay logged in to the current account.
                 try {
-                    dispatch<any>(saveApiToken(linkState.userToLinkToken));
+                    dispatch(switchUser(linkState.targetUser, linkState.targetUserToken));
                     await services.projectService.delete(newGroup.uuid);
                 }
                 finally {
-                    dispatch<any>(saveApiToken(currentToken));
-                    dispatch(snackbarActions.OPEN_SNACKBAR({
-                        message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000
-                    }));
+                    dispatch(linkFailed());
                 }
                 throw e;
             }
             finally {
-                dispatch<any>(navigateToRootProject);
                 services.linkAccountService.removeFromSession();
-                dispatch(linkAccountPanelActions.RESET());
             }
         }
     };
\ No newline at end of file
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 7f7e3eb3..9e0bd3f7 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -17,9 +17,16 @@ export enum LinkAccountPanelError {
     SAME_USER
 }
 
+export enum OriginatingUser {
+    NONE,
+    TARGET_USER,
+    USER_TO_LINK
+}
+
 export interface LinkAccountPanelState {
-    user: UserResource | undefined;
-    userToken: string | undefined;
+    originatingUser: OriginatingUser | undefined;
+    targetUser: UserResource | undefined;
+    targetUserToken: string | undefined;
     userToLink: UserResource | undefined;
     userToLinkToken: string | undefined;
     status: LinkAccountPanelStatus;
@@ -27,8 +34,9 @@ export interface LinkAccountPanelState {
 }
 
 const initialState = {
-    user: undefined,
-    userToken: undefined,
+    originatingUser: undefined,
+    targetUser: undefined,
+    targetUserToken: undefined,
     userToLink: undefined,
     userToLinkToken: undefined,
     status: LinkAccountPanelStatus.INITIAL,
@@ -38,16 +46,13 @@ const initialState = {
 export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialState, action: LinkAccountPanelAction) =>
     linkAccountPanelActions.match(action, {
         default: () => state,
-        INIT: ({ user }) => ({
-            ...state, user, state: LinkAccountPanelStatus.INITIAL, error: LinkAccountPanelError.NONE
-        }),
-        LOAD: ({ userToLink, user, userToken, userToLinkToken}) => ({
-            ...state, user, userToken, userToLink, userToLinkToken, status: LinkAccountPanelStatus.LINKING, error: LinkAccountPanelError.NONE
+        LINK_INIT: ({ targetUser }) => ({
+            ...state, targetUser, status: LinkAccountPanelStatus.INITIAL, error: LinkAccountPanelError.NONE, originatingUser: OriginatingUser.NONE
         }),
-        RESET: () => ({
-            ...state, userToken: undefined, userToLink: undefined, userToLinkToken: undefined, status: LinkAccountPanelStatus.INITIAL,  error: LinkAccountPanelError.NONE
+        LINK_LOAD: ({ originatingUser, userToLink, targetUser, targetUserToken, userToLinkToken}) => ({
+            ...state, originatingUser, targetUser, targetUserToken, userToLink, userToLinkToken, status: LinkAccountPanelStatus.LINKING, error: LinkAccountPanelError.NONE
         }),
-        INVALID: ({user, userToLink, error}) => ({
-            ...state, user, userToLink, error, status: LinkAccountPanelStatus.ERROR
+        LINK_INVALID: ({originatingUser, targetUser, userToLink, error}) => ({
+            ...state, originatingUser, targetUser, userToLink, error, status: LinkAccountPanelStatus.ERROR
         })
     });
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 68f407b5..a2cdee60 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -10,7 +10,6 @@ import {
     Card,
     CardContent,
     Button,
-    Typography,
     Grid,
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
@@ -29,7 +28,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 export interface LinkAccountPanelRootDataProps {
-    user?: UserResource;
+    targetUser?: UserResource;
     userToLink?: UserResource;
     status : LinkAccountPanelStatus;
     error: LinkAccountPanelError;
@@ -37,7 +36,7 @@ export interface LinkAccountPanelRootDataProps {
 
 export interface LinkAccountPanelRootActionProps {
     saveAccountLinkData: (type: LinkAccountType) => void;
-    removeAccountLinkData: () => void;
+    cancelLinking: () => void;
     linkAccount: () => void;
 }
 
@@ -53,14 +52,14 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false) {
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, user, userToLink, status, error, saveAccountLinkData, removeAccountLinkData, linkAccount}: LinkAccountPanelRootProps) => {
+    ({classes, targetUser, userToLink, status, error, saveAccountLinkData, cancelLinking, linkAccount}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-            { status === LinkAccountPanelStatus.INITIAL && user &&
+            { status === LinkAccountPanelStatus.INITIAL && targetUser &&
             <Grid container spacing={24}>
                 <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        You are currently logged in as {displayUser(user, true)}
+                        You are currently logged in as {displayUser(targetUser, true)}
                     </Grid>
                     <Grid item>
                         You can link Arvados accounts. After linking, either login will take you to the same account.
@@ -79,28 +78,28 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     </Grid>
                 </Grid>
             </Grid> }
-            { (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && user &&
+            { (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
             <Grid container spacing={24}>
                 { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        Clicking 'Link accounts' will link {displayUser(user, true)} to {displayUser(userToLink, true)}.
+                        Clicking 'Link accounts' will link {displayUser(userToLink, true)} to {displayUser(targetUser, true)}.
                     </Grid>
                     <Grid item>
-                        After linking, logging in as {displayUser(user)} will log you into the same account as {displayUser(userToLink)}.
+                        After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
                     </Grid>
                     <Grid item>
-                       Any object owned by {displayUser(user)} will be transfered to {displayUser(userToLink)}.
+                       Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
                     </Grid>
                 </Grid> }
                 { error === LinkAccountPanelError.NON_ADMIN && <Grid item>
-                    Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(user)}.
+                    Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}.
                 </Grid> }
                 { error === LinkAccountPanelError.SAME_USER && <Grid item>
-                    Cannot link {displayUser(userToLink)} to the same account.
+                    Cannot link {displayUser(targetUser)} to the same account.
                 </Grid> }
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
-                        <Button variant="contained" onClick={() => removeAccountLinkData()}>
+                        <Button variant="contained" onClick={() => cancelLinking()}>
                             Cancel
                         </Button>
                     </Grid>
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index 20727b39..45045a3b 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -5,7 +5,7 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { saveAccountLinkData, removeAccountLinkData, linkAccount } from '~/store/link-account-panel/link-account-panel-actions';
+import { saveAccountLinkData, cancelLinking, linkAccount } from '~/store/link-account-panel/link-account-panel-actions';
 import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
@@ -15,7 +15,7 @@ import {
 
 const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     return {
-        user: state.linkAccountPanel.user,
+        targetUser: state.linkAccountPanel.targetUser,
         userToLink: state.linkAccountPanel.userToLink,
         status: state.linkAccountPanel.status,
         error: state.linkAccountPanel.error
@@ -24,7 +24,7 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
     saveAccountLinkData: (type: LinkAccountType) => dispatch<any>(saveAccountLinkData(type)),
-    removeAccountLinkData: () => dispatch<any>(removeAccountLinkData()),
+    cancelLinking: () => dispatch<any>(cancelLinking()),
     linkAccount: () => dispatch<any>(linkAccount())
 });
 

commit dcab8d69b5fb93c025c49fd85bf39d038c4fb3d0
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Fri May 3 11:27:12 2019 -0400

    15088: Adds link accounts to drop down. Removes changes to my account
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
index 87bfe0c6..93aeadae 100644
--- a/src/views-components/main-app-bar/account-menu.tsx
+++ b/src/views-components/main-app-bar/account-menu.tsx
@@ -17,7 +17,8 @@ import { openRepositoriesPanel } from "~/store/repositories/repositories-actions
 import {
     navigateToSiteManager,
     navigateToSshKeysUser,
-    navigateToMyAccount
+    navigateToMyAccount,
+    navigateToLinkAccount
 } from '~/store/navigation/navigation-action';
 import { openUserVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions";
 
@@ -75,6 +76,7 @@ export const AccountMenu = withStyles(styles)(
                         <MenuItem onClick={() => dispatch(navigateToSshKeysUser)}>Ssh Keys</MenuItem>
                         <MenuItem onClick={() => dispatch(navigateToSiteManager)}>Site Manager</MenuItem>
                         <MenuItem onClick={() => dispatch(navigateToMyAccount)}>My account</MenuItem>
+                        <MenuItem onClick={() => dispatch(navigateToLinkAccount)}>Link account</MenuItem>
                     </> : null}
                     <MenuItem>
                         <a href={`${workbenchURL.replace(/\/$/, "")}/${wb1URL(currentRoute)}?api_token=${apiToken}`}
diff --git a/src/views/my-account-panel/my-account-panel-root.tsx b/src/views/my-account-panel/my-account-panel-root.tsx
index 7dcbe09a..e84b3b64 100644
--- a/src/views/my-account-panel/my-account-panel-root.tsx
+++ b/src/views/my-account-panel/my-account-panel-root.tsx
@@ -6,7 +6,6 @@ import * as React from 'react';
 import { Field, InjectedFormProps, WrappedFieldProps } from "redux-form";
 import { TextField } from "~/components/text-field/text-field";
 import { NativeSelectField } from "~/components/select-field/select-field";
-import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
 import {
     StyleRulesCallback,
     WithStyles,
@@ -22,7 +21,7 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import { User } from "~/models/user";
 import { MY_ACCOUNT_VALIDATION } from "~/validators/validators";
 
-type CssRules = 'root' | 'gridItem' | 'label' | 'title' | 'actions' | 'link';
+type CssRules = 'root' | 'gridItem' | 'label' | 'title' | 'actions';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -40,24 +39,13 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         marginBottom: theme.spacing.unit * 3,
         color: theme.palette.grey["600"]
     },
-    link: {
-        lineHeight:'2.1',
-        whiteSpace: 'nowrap',
-        fontSize: '1rem',
-        color: theme.palette.primary.main,
-        '&:hover': {
-            cursor: 'pointer'
-        }
-    },
     actions: {
         display: 'flex',
         justifyContent: 'flex-end'
     }
 });
 
-export interface MyAccountPanelRootActionProps {
-    openLinkAccount: () => void;
- }
+export interface MyAccountPanelRootActionProps { }
 
 export interface MyAccountPanelRootDataProps {
     isPristine: boolean;
@@ -76,7 +64,7 @@ const RoleTypes = [
     { key: 'Other', value: 'Other' }
 ];
 
-type MyAccountPanelRootProps = InjectedFormProps & MyAccountPanelRootActionProps & MyAccountPanelRootDataProps & WithStyles<CssRules>;
+type MyAccountPanelRootProps = InjectedFormProps<MyAccountPanelRootActionProps> & MyAccountPanelRootDataProps & WithStyles<CssRules>;
 
 type LocalClusterProp = { localCluster: string };
 const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
@@ -84,21 +72,12 @@ const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = (
 );
 
 export const MyAccountPanelRoot = withStyles(styles)(
-    ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting, localCluster, openLinkAccount}: MyAccountPanelRootProps) => {
+    ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting, localCluster }: MyAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-                <Grid container spacing={24}>
-                    <Grid item className={classes.gridItem}>
-                        <Typography variant="title" className={classes.title}>
-                            Logged in as <Field name="uuid" component={renderField} localCluster={localCluster} />
-                        </Typography>
-                    </Grid>
-                    <Grid item className={classes.gridItem}>
-                        <span onClick={() => openLinkAccount()}>
-                            <DetailsAttribute classLabel={classes.link} label='Link account' />
-                        </span>
-                    </Grid>
-                </Grid>
+                <Typography variant="title" className={classes.title}>
+                    Logged in as <Field name="uuid" component={renderField} localCluster={localCluster} />
+                </Typography>
                 <form onSubmit={handleSubmit}>
                     <Grid container spacing={24}>
                         <Grid item className={classes.gridItem} sm={6} xs={12}>
diff --git a/src/views/my-account-panel/my-account-panel.tsx b/src/views/my-account-panel/my-account-panel.tsx
index 85f285d6..bd1f5874 100644
--- a/src/views/my-account-panel/my-account-panel.tsx
+++ b/src/views/my-account-panel/my-account-panel.tsx
@@ -3,12 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { RootState } from '~/store/store';
-import { Dispatch } from 'redux';
 import { compose } from 'redux';
 import { reduxForm, isPristine, isValid } from 'redux-form';
 import { connect } from 'react-redux';
-import { saveEditedUser, openLinkAccount } from '~/store/my-account/my-account-panel-actions';
-import { MyAccountPanelRoot, MyAccountPanelRootDataProps, MyAccountPanelRootActionProps } from '~/views/my-account-panel/my-account-panel-root';
+import { saveEditedUser } from '~/store/my-account/my-account-panel-actions';
+import { MyAccountPanelRoot, MyAccountPanelRootDataProps } from '~/views/my-account-panel/my-account-panel-root';
 import { MY_ACCOUNT_FORM } from "~/store/my-account/my-account-panel-actions";
 
 const mapStateToProps = (state: RootState): MyAccountPanelRootDataProps => ({
@@ -18,12 +17,8 @@ const mapStateToProps = (state: RootState): MyAccountPanelRootDataProps => ({
     localCluster: state.auth.localCluster
 });
 
-const mapDispatchToProps = (dispatch: Dispatch): MyAccountPanelRootActionProps => ({
-    openLinkAccount: () => dispatch<any>(openLinkAccount())
-});
-
 export const MyAccountPanel = compose(
-    connect(mapStateToProps, mapDispatchToProps),
+    connect(mapStateToProps),
     reduxForm({
         form: MY_ACCOUNT_FORM,
         onSubmit: (data, dispatch) => {

commit 13700efea8cd742fbb4888252f3d06788f5fd845
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Thu May 2 16:28:51 2019 -0400

    15088: Adds invalid states and UI and improves action error handling
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 4e505284..47e27f7d 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -6,23 +6,41 @@ import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
 import { logout, saveApiToken, saveUser } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
-import { UserResource } from "~/models/user";
+import { UserResource, User } from "~/models/user";
 import { navigateToRootProject } from "~/store/navigation/navigation-action";
+import { GroupResource } from "~/models/group";
+import { LinkAccountPanelError } from "./link-account-panel-reducer";
 
 export const linkAccountPanelActions = unionize({
-    LOAD_LINKING: ofType<{
+    INIT: ofType<{ user: UserResource | undefined }>(),
+    LOAD: ofType<{
         user: UserResource | undefined,
         userToken: string | undefined,
         userToLink: UserResource | undefined,
         userToLinkToken: string | undefined }>(),
-    RESET_LINKING: {}
+    RESET: {},
+    INVALID: ofType<{
+        user: UserResource | undefined,
+        userToLink: UserResource | undefined,
+        error: LinkAccountPanelError }>(),
 });
 
 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
 
+function validateLink(user: UserResource, userToLink: UserResource) {
+    if (user.uuid === userToLink.uuid) {
+        return LinkAccountPanelError.SAME_USER;
+    }
+    else if (!user.isAdmin && userToLink.isAdmin) {
+        return LinkAccountPanelError.NON_ADMIN;
+    }
+    return LinkAccountPanelError.NONE;
+}
+
 export const loadLinkAccountPanel = () =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
@@ -33,41 +51,49 @@ export const loadLinkAccountPanel = () =>
             const curUserResource = await services.userService.get(curUser.uuid);
             const linkAccountData = services.linkAccountService.getFromSession();
 
-            // If there is link account data, then the user has logged in a second time
+            // If there is link account session data, then the user has logged in a second time
             if (linkAccountData) {
-                // Use the saved token to make the api call to override the current users permissions
                 dispatch<any>(saveApiToken(linkAccountData.token));
                 const savedUserResource = await services.userService.get(linkAccountData.userUuid);
                 dispatch<any>(saveApiToken(curToken));
+
+                let params: any;
                 if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
-                    const params = {
+                    params = {
                         user: savedUserResource,
                         userToken: linkAccountData.token,
                         userToLink: curUserResource,
                         userToLinkToken: curToken
                     };
-                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
                 }
                 else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
-                    const params = {
+                    params = {
                         user: curUserResource,
                         userToken: curToken,
                         userToLink: savedUserResource,
                         userToLinkToken: linkAccountData.token
                     };
-                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
                 }
                 else {
                     throw new Error("Invalid link account type.");
                 }
+
+                const error = validateLink(params.user, params.userToLink);
+                if (error === LinkAccountPanelError.NONE) {
+                    dispatch<any>(linkAccountPanelActions.LOAD(params));
+                }
+                else {
+                    dispatch<any>(linkAccountPanelActions.INVALID({
+                        user: params.user,
+                        userToLink: params.userToLink,
+                        error}));
+                    return;
+                }
             }
             else {
                 // If there is no link account session data, set the state to invoke the initial UI
-                dispatch<any>(linkAccountPanelActions.LOAD_LINKING({
-                    user: curUserResource,
-                    userToken: curToken,
-                    userToLink: undefined,
-                    userToLinkToken: undefined }
+                dispatch<any>(linkAccountPanelActions.INIT({
+                    user: curUserResource }
                 ));
             }
         }
@@ -86,17 +112,19 @@ export const getAccountLinkData = () =>
     };
 
 export const removeAccountLinkData = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const linkAccountData = services.linkAccountService.getFromSession();
-        services.linkAccountService.removeFromSession();
-        dispatch(linkAccountPanelActions.RESET_LINKING());
-        if (linkAccountData) {
-            services.userService.get(linkAccountData.userUuid).then(savedUser => {
-                dispatch(setBreadcrumbs([{ label: ''}]));
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const linkAccountData = services.linkAccountService.getFromSession();
+            if (linkAccountData) {
+                const savedUser = await services.userService.get(linkAccountData.userUuid);
                 dispatch<any>(saveUser(savedUser));
                 dispatch<any>(saveApiToken(linkAccountData.token));
-                dispatch<any>(navigateToRootProject);
-            });
+            }
+        }
+        finally {
+            dispatch<any>(navigateToRootProject);
+            dispatch(linkAccountPanelActions.RESET());
+            services.linkAccountService.removeFromSession();
         }
     };
 
@@ -108,36 +136,50 @@ export const linkAccount = () =>
 
             // First create a project owned by the "userToLink" to accept everything from the current user
             const projectName = `Migrated from ${linkState.user.email} (${linkState.user.uuid})`;
-            dispatch<any>(saveApiToken(linkState.userToLinkToken));
-            const newGroup = await services.projectService.create({
-                name: projectName,
-                ensure_unique_name: true
-            });
-            dispatch<any>(saveApiToken(currentToken));
+            let newGroup: GroupResource;
+            try {
+                dispatch<any>(saveApiToken(linkState.userToLinkToken));
+                newGroup = await services.projectService.create({
+                    name: projectName,
+                    ensure_unique_name: true
+                });
+            }
+            catch (e) {
+                dispatch<any>(saveApiToken(currentToken));
+                dispatch(snackbarActions.OPEN_SNACKBAR({
+                    message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000
+                }));
+                throw e;
+            }
 
             try {
+                // Use the token of the account that is getting merged to call the merge api
                 dispatch<any>(saveApiToken(linkState.userToken));
                 await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid);
-                dispatch<any>(saveApiToken(currentToken));
 
                 // If the link was successful, switch to the account that was merged with
-                if (linkState.userToLink && linkState.userToLinkToken) {
-                    dispatch<any>(saveUser(linkState.userToLink));
+                dispatch<any>(saveUser(linkState.userToLink));
+                dispatch<any>(saveApiToken(linkState.userToLinkToken));
+            }
+            catch(e) {
+                // If the link operation fails, delete the previously made project
+                // and stay logged in to the current account.
+                try {
                     dispatch<any>(saveApiToken(linkState.userToLinkToken));
-                    dispatch<any>(navigateToRootProject);
+                    await services.projectService.delete(newGroup.uuid);
                 }
-                services.linkAccountService.removeFromSession();
-                dispatch(linkAccountPanelActions.RESET_LINKING());
+                finally {
+                    dispatch<any>(saveApiToken(currentToken));
+                    dispatch(snackbarActions.OPEN_SNACKBAR({
+                        message: 'Account link failed.', kind: SnackbarKind.ERROR , hideDuration: 3000
+                    }));
+                }
+                throw e;
             }
-            catch(e) {
-                // If the account link operation fails, delete the previously made project
-                // and reset the link account state. The user will have to restart the process.
-                dispatch<any>(saveApiToken(linkState.userToLinkToken));
-                services.projectService.delete(newGroup.uuid);
-                dispatch<any>(saveApiToken(currentToken));
+            finally {
+                dispatch<any>(navigateToRootProject);
                 services.linkAccountService.removeFromSession();
-                dispatch(linkAccountPanelActions.RESET_LINKING());
-                throw e;
+                dispatch(linkAccountPanelActions.RESET());
             }
         }
     };
\ No newline at end of file
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index fabef94e..7f7e3eb3 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -3,25 +3,51 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { linkAccountPanelActions, LinkAccountPanelAction } from "~/store/link-account-panel/link-account-panel-actions";
-import { UserResource, User } from "~/models/user";
+import { UserResource } from "~/models/user";
+
+export enum LinkAccountPanelStatus {
+    INITIAL,
+    LINKING,
+    ERROR
+}
+
+export enum LinkAccountPanelError {
+    NONE,
+    NON_ADMIN,
+    SAME_USER
+}
 
 export interface LinkAccountPanelState {
     user: UserResource | undefined;
     userToken: string | undefined;
     userToLink: UserResource | undefined;
     userToLinkToken: string | undefined;
+    status: LinkAccountPanelStatus;
+    error: LinkAccountPanelError;
 }
 
 const initialState = {
     user: undefined,
     userToken: undefined,
     userToLink: undefined,
-    userToLinkToken: undefined
+    userToLinkToken: undefined,
+    status: LinkAccountPanelStatus.INITIAL,
+    error: LinkAccountPanelError.NONE
 };
 
 export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialState, action: LinkAccountPanelAction) =>
     linkAccountPanelActions.match(action, {
         default: () => state,
-        LOAD_LINKING: ({ userToLink, user, userToken, userToLinkToken}) => ({ ...state, user, userToken, userToLink, userToLinkToken }),
-        RESET_LINKING: () => ({ ...state, user: undefined, userToken: undefined, userToLink: undefined, userToLinkToken: undefined })
+        INIT: ({ user }) => ({
+            ...state, user, state: LinkAccountPanelStatus.INITIAL, error: LinkAccountPanelError.NONE
+        }),
+        LOAD: ({ userToLink, user, userToken, userToLinkToken}) => ({
+            ...state, user, userToken, userToLink, userToLinkToken, status: LinkAccountPanelStatus.LINKING, error: LinkAccountPanelError.NONE
+        }),
+        RESET: () => ({
+            ...state, userToken: undefined, userToLink: undefined, userToLinkToken: undefined, status: LinkAccountPanelStatus.INITIAL,  error: LinkAccountPanelError.NONE
+        }),
+        INVALID: ({user, userToLink, error}) => ({
+            ...state, user, userToLink, error, status: LinkAccountPanelStatus.ERROR
+        })
     });
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index f4570de2..68f407b5 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -17,6 +17,7 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import { UserResource } from "~/models/user";
 import { LinkAccountType } from "~/models/link-account";
 import { formatDate } from "~/common/formatters";
+import { LinkAccountPanelStatus, LinkAccountPanelError } from "~/store/link-account-panel/link-account-panel-reducer";
 
 type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
 
@@ -30,6 +31,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export interface LinkAccountPanelRootDataProps {
     user?: UserResource;
     userToLink?: UserResource;
+    status : LinkAccountPanelStatus;
+    error: LinkAccountPanelError;
 }
 
 export interface LinkAccountPanelRootActionProps {
@@ -50,10 +53,11 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false) {
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, user, userToLink, saveAccountLinkData, removeAccountLinkData, linkAccount}: LinkAccountPanelRootProps) => {
+    ({classes, user, userToLink, status, error, saveAccountLinkData, removeAccountLinkData, linkAccount}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-            { user && userToLink===undefined && <Grid container spacing={24}>
+            { status === LinkAccountPanelStatus.INITIAL && user &&
+            <Grid container spacing={24}>
                 <Grid container item direction="column" spacing={24}>
                     <Grid item>
                         You are currently logged in as {displayUser(user, true)}
@@ -74,9 +78,10 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                         </Button>
                     </Grid>
                 </Grid>
-            </Grid>}
-            { userToLink && user && <Grid container spacing={24}>
-                <Grid container item direction="column" spacing={24}>
+            </Grid> }
+            { (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && user &&
+            <Grid container spacing={24}>
+                { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
                     <Grid item>
                         Clicking 'Link accounts' will link {displayUser(user, true)} to {displayUser(userToLink, true)}.
                     </Grid>
@@ -86,7 +91,13 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     <Grid item>
                        Any object owned by {displayUser(user)} will be transfered to {displayUser(userToLink)}.
                     </Grid>
-                </Grid>
+                </Grid> }
+                { error === LinkAccountPanelError.NON_ADMIN && <Grid item>
+                    Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(user)}.
+                </Grid> }
+                { error === LinkAccountPanelError.SAME_USER && <Grid item>
+                    Cannot link {displayUser(userToLink)} to the same account.
+                </Grid> }
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
                         <Button variant="contained" onClick={() => removeAccountLinkData()}>
@@ -94,7 +105,7 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                         </Button>
                     </Grid>
                     <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => linkAccount()}>
+                        <Button disabled={status === LinkAccountPanelStatus.ERROR} color="primary" variant="contained" onClick={() => linkAccount()}>
                             Link accounts
                         </Button>
                     </Grid>
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index 498eff88..20727b39 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -16,7 +16,9 @@ import {
 const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     return {
         user: state.linkAccountPanel.user,
-        userToLink: state.linkAccountPanel.userToLink
+        userToLink: state.linkAccountPanel.userToLink,
+        status: state.linkAccountPanel.status,
+        error: state.linkAccountPanel.error
     };
 };
 

commit 2b22123ad358b8c6667e42779f6e52e9a14d9289
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue Apr 30 15:44:59 2019 -0400

    15088: Adds account linking functionality
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/models/group.ts b/src/models/group.ts
index e13fbcbf..f34ede0a 100644
--- a/src/models/group.ts
+++ b/src/models/group.ts
@@ -11,6 +11,7 @@ export interface GroupResource extends TrashableResource {
     description: string;
     properties: any;
     writeableBy: string[];
+    ensure_unique_name: boolean;
 }
 
 export enum GroupClass {
diff --git a/src/models/test-utils.ts b/src/models/test-utils.ts
index b08ce5a0..22a94f16 100644
--- a/src/models/test-utils.ts
+++ b/src/models/test-utils.ts
@@ -24,6 +24,7 @@ export const mockGroupResource = (data: Partial<GroupResource> = {}): GroupResou
     trashAt: "",
     uuid: "",
     writeableBy: [],
+    ensure_unique_name: true,
     ...data
 });
 
diff --git a/src/services/link-account-service/link-account-service.ts b/src/services/link-account-service/link-account-service.ts
index fe78d484..cc7c62eb 100644
--- a/src/services/link-account-service/link-account-service.ts
+++ b/src/services/link-account-service/link-account-service.ts
@@ -5,25 +5,40 @@
 import { AxiosInstance } from "axios";
 import { ApiActions } from "~/services/api/api-actions";
 import { AccountToLink } from "~/models/link-account";
+import { CommonService } from "~/services/common-service/common-service";
+import { AuthService } from "../auth-service/auth-service";
 
 export const USER_LINK_ACCOUNT_KEY = 'accountToLink';
 
 export class LinkAccountService {
 
     constructor(
-        protected apiClient: AxiosInstance,
+        protected serverApi: AxiosInstance,
         protected actions: ApiActions) { }
 
-    public saveLinkAccount(account: AccountToLink) {
+    public saveToSession(account: AccountToLink) {
         sessionStorage.setItem(USER_LINK_ACCOUNT_KEY, JSON.stringify(account));
     }
 
-    public removeLinkAccount() {
+    public removeFromSession() {
         sessionStorage.removeItem(USER_LINK_ACCOUNT_KEY);
     }
 
-    public getLinkAccount() {
+    public getFromSession() {
         const data = sessionStorage.getItem(USER_LINK_ACCOUNT_KEY);
         return data ? JSON.parse(data) as AccountToLink : undefined;
     }
+
+    public linkAccounts(newUserToken: string, newGroupUuid: string) {
+        const params = {
+            new_user_token: newUserToken,
+            new_owner_uuid: newGroupUuid,
+            redirect_to_new_user: true
+        };
+        return CommonService.defaultResponse(
+            this.serverApi.post('/users/merge/', params),
+            this.actions,
+            false
+        );
+    }
 }
\ No newline at end of file
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 42fd6c52..4e505284 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -13,57 +13,83 @@ import { UserResource } from "~/models/user";
 import { navigateToRootProject } from "~/store/navigation/navigation-action";
 
 export const linkAccountPanelActions = unionize({
-    LOAD_LINKING: ofType<{ user: UserResource | undefined, userToLink: UserResource | undefined }>(),
-    REMOVE_LINKING: {}
+    LOAD_LINKING: ofType<{
+        user: UserResource | undefined,
+        userToken: string | undefined,
+        userToLink: UserResource | undefined,
+        userToLinkToken: string | undefined }>(),
+    RESET_LINKING: {}
 });
 
 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
 
 export const loadLinkAccountPanel = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
 
         const curUser = getState().auth.user;
-        if (curUser) {
-            services.userService.get(curUser.uuid).then(curUserResource => {
-                const linkAccountData = services.linkAccountService.getLinkAccount();
-                if (linkAccountData) {
-                    services.userService.get(linkAccountData.userUuid).then(savedUserResource => {
-                        if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
-                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: curUserResource, user: savedUserResource }));
-                        }
-                        else if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
-                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: savedUserResource, user: curUserResource }));
-                        }
-                        else {
-                            throw new Error('Invalid link account type.');
-                        }
-                    });
+        const curToken = getState().auth.apiToken;
+        if (curUser && curToken) {
+            const curUserResource = await services.userService.get(curUser.uuid);
+            const linkAccountData = services.linkAccountService.getFromSession();
+
+            // If there is link account data, then the user has logged in a second time
+            if (linkAccountData) {
+                // Use the saved token to make the api call to override the current users permissions
+                dispatch<any>(saveApiToken(linkAccountData.token));
+                const savedUserResource = await services.userService.get(linkAccountData.userUuid);
+                dispatch<any>(saveApiToken(curToken));
+                if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
+                    const params = {
+                        user: savedUserResource,
+                        userToken: linkAccountData.token,
+                        userToLink: curUserResource,
+                        userToLinkToken: curToken
+                    };
+                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
+                }
+                else if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
+                    const params = {
+                        user: curUserResource,
+                        userToken: curToken,
+                        userToLink: savedUserResource,
+                        userToLinkToken: linkAccountData.token
+                    };
+                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING(params));
                 }
                 else {
-                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: undefined, user: curUserResource }));
+                    throw new Error("Invalid link account type.");
                 }
-            });
+            }
+            else {
+                // If there is no link account session data, set the state to invoke the initial UI
+                dispatch<any>(linkAccountPanelActions.LOAD_LINKING({
+                    user: curUserResource,
+                    userToken: curToken,
+                    userToLink: undefined,
+                    userToLinkToken: undefined }
+                ));
+            }
         }
     };
 
 export const saveAccountLinkData = (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.saveLinkAccount(accountToLink);
+        services.linkAccountService.saveToSession(accountToLink);
         dispatch(logout());
     };
 
 export const getAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        return services.linkAccountService.getLinkAccount();
+        return services.linkAccountService.getFromSession();
     };
 
 export const removeAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const linkAccountData = services.linkAccountService.getLinkAccount();
-        services.linkAccountService.removeLinkAccount();
-        dispatch(linkAccountPanelActions.REMOVE_LINKING());
+        const linkAccountData = services.linkAccountService.getFromSession();
+        services.linkAccountService.removeFromSession();
+        dispatch(linkAccountPanelActions.RESET_LINKING());
         if (linkAccountData) {
             services.userService.get(linkAccountData.userUuid).then(savedUser => {
                 dispatch(setBreadcrumbs([{ label: ''}]));
@@ -75,5 +101,43 @@ export const removeAccountLinkData = () =>
     };
 
 export const linkAccount = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const linkState = getState().linkAccountPanel;
+        const currentToken = getState().auth.apiToken;
+        if (linkState.userToLink && linkState.userToLinkToken && linkState.user && linkState.userToken && currentToken) {
+
+            // First create a project owned by the "userToLink" to accept everything from the current user
+            const projectName = `Migrated from ${linkState.user.email} (${linkState.user.uuid})`;
+            dispatch<any>(saveApiToken(linkState.userToLinkToken));
+            const newGroup = await services.projectService.create({
+                name: projectName,
+                ensure_unique_name: true
+            });
+            dispatch<any>(saveApiToken(currentToken));
+
+            try {
+                dispatch<any>(saveApiToken(linkState.userToken));
+                await services.linkAccountService.linkAccounts(linkState.userToLinkToken, newGroup.uuid);
+                dispatch<any>(saveApiToken(currentToken));
+
+                // If the link was successful, switch to the account that was merged with
+                if (linkState.userToLink && linkState.userToLinkToken) {
+                    dispatch<any>(saveUser(linkState.userToLink));
+                    dispatch<any>(saveApiToken(linkState.userToLinkToken));
+                    dispatch<any>(navigateToRootProject);
+                }
+                services.linkAccountService.removeFromSession();
+                dispatch(linkAccountPanelActions.RESET_LINKING());
+            }
+            catch(e) {
+                // If the account link operation fails, delete the previously made project
+                // and reset the link account state. The user will have to restart the process.
+                dispatch<any>(saveApiToken(linkState.userToLinkToken));
+                services.projectService.delete(newGroup.uuid);
+                dispatch<any>(saveApiToken(currentToken));
+                services.linkAccountService.removeFromSession();
+                dispatch(linkAccountPanelActions.RESET_LINKING());
+                throw e;
+            }
+        }
     };
\ No newline at end of file
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index cbfb315b..fabef94e 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -7,17 +7,21 @@ import { UserResource, User } from "~/models/user";
 
 export interface LinkAccountPanelState {
     user: UserResource | undefined;
+    userToken: string | undefined;
     userToLink: UserResource | undefined;
+    userToLinkToken: string | undefined;
 }
 
 const initialState = {
     user: undefined,
-    userToLink: undefined
+    userToken: undefined,
+    userToLink: undefined,
+    userToLinkToken: undefined
 };
 
 export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialState, action: LinkAccountPanelAction) =>
     linkAccountPanelActions.match(action, {
         default: () => state,
-        LOAD_LINKING: ({ userToLink, user }) => ({ ...state, user, userToLink }),
-        REMOVE_LINKING: () => ({ ...state, user: undefined, userToLink: undefined })
+        LOAD_LINKING: ({ userToLink, user, userToken, userToLinkToken}) => ({ ...state, user, userToken, userToLink, userToLinkToken }),
+        RESET_LINKING: () => ({ ...state, user: undefined, userToken: undefined, userToLink: undefined, userToLinkToken: undefined })
     });
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 8b3fb45a..f4570de2 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -14,9 +14,9 @@ import {
     Grid,
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { User, UserResource } from "~/models/user";
-import { LinkAccountType, AccountToLink } from "~/models/link-account";
-import { formatDate }from "~/common/formatters";
+import { UserResource } from "~/models/user";
+import { LinkAccountType } from "~/models/link-account";
+import { formatDate } from "~/common/formatters";
 
 type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
 
@@ -78,13 +78,13 @@ export const LinkAccountPanelRoot = withStyles(styles) (
             { userToLink && user && <Grid container spacing={24}>
                 <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        Clicking 'Link accounts' will link {displayUser(userToLink, true)} to {displayUser(user, true)}.
+                        Clicking 'Link accounts' will link {displayUser(user, true)} to {displayUser(userToLink, true)}.
                     </Grid>
                     <Grid item>
-                        After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(user)}.
+                        After linking, logging in as {displayUser(user)} will log you into the same account as {displayUser(userToLink)}.
                     </Grid>
                     <Grid item>
-                       Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(user)}.
+                       Any object owned by {displayUser(user)} will be transfered to {displayUser(userToLink)}.
                     </Grid>
                 </Grid>
                 <Grid container item direction="row" spacing={24}>

commit 6e2546d78a606e19fc974b5e4443880cf6da282a
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Mon Apr 29 15:28:58 2019 -0400

    15088: Updates state to account for both types of linking and adds UI
    
    - Simplifies user type in the link-account-panel to only be a UserResource, and subsequently removes createdAt from the auth user.
    - Switches users back to the originating user when account linking is cancelled after logging on with the second account.
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com

diff --git a/src/models/link-account.ts b/src/models/link-account.ts
index dd22bc95..8932a335 100644
--- a/src/models/link-account.ts
+++ b/src/models/link-account.ts
@@ -9,5 +9,6 @@ export enum LinkAccountType {
 
 export interface AccountToLink {
     type: LinkAccountType;
-    userToken: string;
+    userUuid: string;
+    token: string;
 }
diff --git a/src/models/user.ts b/src/models/user.ts
index afc4fc72..24978645 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -24,7 +24,6 @@ export interface User {
     prefs: UserPrefs;
     isAdmin: boolean;
     isActive: boolean;
-    createdAt: string;
 }
 
 export const getUserFullname = (user?: User) => {
diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts
index e7641fdc..eae219dd 100644
--- a/src/services/auth-service/auth-service.ts
+++ b/src/services/auth-service/auth-service.ts
@@ -20,7 +20,6 @@ export const USER_IS_ADMIN = 'isAdmin';
 export const USER_IS_ACTIVE = 'isActive';
 export const USER_USERNAME = 'username';
 export const USER_PREFS = 'prefs';
-export const USER_CREATED_AT = 'createdAt';
 
 export interface UserDetailsResponse {
     email: string;
@@ -31,7 +30,6 @@ export interface UserDetailsResponse {
     is_admin: boolean;
     is_active: boolean;
     username: string;
-    created_at: string;
     prefs: UserPrefs;
 }
 
@@ -79,11 +77,10 @@ export class AuthService {
         const isAdmin = this.getIsAdmin();
         const isActive = this.getIsActive();
         const username = localStorage.getItem(USER_USERNAME);
-        const createdAt = localStorage.getItem(USER_CREATED_AT);
         const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}');
 
-        return email && firstName && lastName && uuid && ownerUuid && username && createdAt && prefs
-            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, createdAt, prefs }
+        return email && firstName && lastName && uuid && ownerUuid && username && prefs
+            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, prefs }
             : undefined;
     }
 
@@ -96,7 +93,6 @@ export class AuthService {
         localStorage.setItem(USER_IS_ADMIN, JSON.stringify(user.isAdmin));
         localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(user.isActive));
         localStorage.setItem(USER_USERNAME, user.username);
-        localStorage.setItem(USER_CREATED_AT, user.createdAt);
         localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs));
     }
 
@@ -109,7 +105,6 @@ export class AuthService {
         localStorage.removeItem(USER_IS_ADMIN);
         localStorage.removeItem(USER_IS_ACTIVE);
         localStorage.removeItem(USER_USERNAME);
-        localStorage.removeItem(USER_CREATED_AT);
         localStorage.removeItem(USER_PREFS);
     }
 
@@ -140,7 +135,6 @@ export class AuthService {
                     isAdmin: resp.data.is_admin,
                     isActive: resp.data.is_active,
                     username: resp.data.username,
-                    createdAt: resp.data.created_at,
                     prefs
                 };
             })
diff --git a/src/store/auth/auth-action-session.ts b/src/store/auth/auth-action-session.ts
index 0a41fe58..5bb192b8 100644
--- a/src/store/auth/auth-action-session.ts
+++ b/src/store/auth/auth-action-session.ts
@@ -94,7 +94,6 @@ const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: S
             isAdmin: user.is_admin,
             isActive: user.is_active,
             username: user.username,
-            createdAt: user.created_at,
             prefs: user.prefs
         },
         token: saltedToken
diff --git a/src/store/auth/auth-action.ts b/src/store/auth/auth-action.ts
index baf80595..bd685280 100644
--- a/src/store/auth/auth-action.ts
+++ b/src/store/auth/auth-action.ts
@@ -8,13 +8,15 @@ import { AxiosInstance } from "axios";
 import { RootState } from "../store";
 import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
-import { User } from "~/models/user";
+import { User, UserResource } from "~/models/user";
 import { Session } from "~/models/session";
 import { Config } from '~/common/config';
 import { initSessions } from "~/store/auth/auth-action-session";
+import { UserRepositoryCreate } from '~/views-components/dialog-create/dialog-user-create';
 
 export const authActions = unionize({
     SAVE_API_TOKEN: ofType<string>(),
+    SAVE_USER: ofType<UserResource>(),
     LOGIN: {},
     LOGOUT: {},
     CONFIG: ofType<{ config: Config }>(),
@@ -66,6 +68,11 @@ export const saveApiToken = (token: string) => (dispatch: Dispatch, getState: ()
     dispatch(authActions.SAVE_API_TOKEN(token));
 };
 
+export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    services.authService.saveUser(user);
+    dispatch(authActions.SAVE_USER(user));
+};
+
 export const login = (uuidPrefix: string, homeCluster: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     services.authService.login(uuidPrefix, homeCluster);
     dispatch(authActions.LOGIN());
diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts
index e0088182..38cf1581 100644
--- a/src/store/auth/auth-reducer.test.ts
+++ b/src/store/auth/auth-reducer.test.ts
@@ -33,8 +33,7 @@ describe('auth-reducer', () => {
             username: "username",
             prefs: {},
             isAdmin: false,
-            isActive: true,
-            createdAt: "createdAt"
+            isActive: true
         };
         const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
         expect(state).toEqual({
@@ -75,8 +74,7 @@ describe('auth-reducer', () => {
             username: "username",
             prefs: {},
             isAdmin: false,
-            isActive: true,
-            createdAt: "createdAt"
+            isActive: true
         };
 
         const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
@@ -96,8 +94,7 @@ describe('auth-reducer', () => {
                 username: "username",
                 prefs: {},
                 isAdmin: false,
-                isActive: true,
-                createdAt: "createdAt"
+                isActive: true
             }
         });
     });
diff --git a/src/store/auth/auth-reducer.ts b/src/store/auth/auth-reducer.ts
index 03357526..c87fc5dd 100644
--- a/src/store/auth/auth-reducer.ts
+++ b/src/store/auth/auth-reducer.ts
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { authActions, AuthAction } from "./auth-action";
-import { User } from "~/models/user";
+import { User, UserResource } from "~/models/user";
 import { ServiceRepository } from "~/services/services";
 import { SshKeyResource } from '~/models/ssh-key';
 import { Session } from "~/models/session";
@@ -33,6 +33,9 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
         SAVE_API_TOKEN: (token: string) => {
             return { ...state, apiToken: token };
         },
+        SAVE_USER: (user: UserResource) => {
+            return { ...state, user};
+        },
         CONFIG: ({ config }) => {
             return {
                 ...state,
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index dd3a79e9..42fd6c52 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -7,11 +7,13 @@ import { RootState } from "~/store/store";
 import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
-import { logout } from "~/store/auth/auth-action";
+import { logout, saveApiToken, saveUser } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
+import { UserResource } from "~/models/user";
+import { navigateToRootProject } from "~/store/navigation/navigation-action";
 
 export const linkAccountPanelActions = unionize({
-    LOAD_LINKING: ofType<AccountToLink>(),
+    LOAD_LINKING: ofType<{ user: UserResource | undefined, userToLink: UserResource | undefined }>(),
     REMOVE_LINKING: {}
 });
 
@@ -21,15 +23,33 @@ export const loadLinkAccountPanel = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         dispatch(setBreadcrumbs([{ label: 'Link account'}]));
 
-        const linkAccountData = services.linkAccountService.getLinkAccount();
-        if (linkAccountData) {
-            dispatch(linkAccountPanelActions.LOAD_LINKING(linkAccountData));
+        const curUser = getState().auth.user;
+        if (curUser) {
+            services.userService.get(curUser.uuid).then(curUserResource => {
+                const linkAccountData = services.linkAccountService.getLinkAccount();
+                if (linkAccountData) {
+                    services.userService.get(linkAccountData.userUuid).then(savedUserResource => {
+                        if (linkAccountData.type === LinkAccountType.ADD_OTHER_LOGIN) {
+                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: curUserResource, user: savedUserResource }));
+                        }
+                        else if (linkAccountData.type === LinkAccountType.ACCESS_OTHER_ACCOUNT) {
+                            dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: savedUserResource, user: curUserResource }));
+                        }
+                        else {
+                            throw new Error('Invalid link account type.');
+                        }
+                    });
+                }
+                else {
+                    dispatch<any>(linkAccountPanelActions.LOAD_LINKING({ userToLink: undefined, user: curUserResource }));
+                }
+            });
         }
     };
 
 export const saveAccountLinkData = (t: LinkAccountType) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
-        const accountToLink = {type: t, userToken: services.authService.getApiToken()} as AccountToLink;
+        const accountToLink = {type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken()} as AccountToLink;
         services.linkAccountService.saveLinkAccount(accountToLink);
         dispatch(logout());
     };
@@ -41,6 +61,19 @@ export const getAccountLinkData = () =>
 
 export const removeAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const linkAccountData = services.linkAccountService.getLinkAccount();
         services.linkAccountService.removeLinkAccount();
         dispatch(linkAccountPanelActions.REMOVE_LINKING());
+        if (linkAccountData) {
+            services.userService.get(linkAccountData.userUuid).then(savedUser => {
+                dispatch(setBreadcrumbs([{ label: ''}]));
+                dispatch<any>(saveUser(savedUser));
+                dispatch<any>(saveApiToken(linkAccountData.token));
+                dispatch<any>(navigateToRootProject);
+            });
+        }
+    };
+
+export const linkAccount = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
     };
\ No newline at end of file
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 47718e3b..cbfb315b 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -3,19 +3,21 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { linkAccountPanelActions, LinkAccountPanelAction } from "~/store/link-account-panel/link-account-panel-actions";
-import { AccountToLink } from "~/models/link-account";
+import { UserResource, User } from "~/models/user";
 
 export interface LinkAccountPanelState {
-    accountToLink: AccountToLink | undefined;
+    user: UserResource | undefined;
+    userToLink: UserResource | undefined;
 }
 
 const initialState = {
-    accountToLink: undefined
+    user: undefined,
+    userToLink: undefined
 };
 
 export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialState, action: LinkAccountPanelAction) =>
     linkAccountPanelActions.match(action, {
         default: () => state,
-        LOAD_LINKING: (accountToLink) => ({ ...state, accountToLink }),
-        REMOVE_LINKING: () => ({...state, accountToLink: undefined})
+        LOAD_LINKING: ({ userToLink, user }) => ({ ...state, user, userToLink }),
+        REMOVE_LINKING: () => ({ ...state, user: undefined, userToLink: undefined })
     });
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 4b3b6979..8b3fb45a 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -14,7 +14,7 @@ import {
     Grid,
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
-import { User } from "~/models/user";
+import { User, UserResource } from "~/models/user";
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
 import { formatDate }from "~/common/formatters";
 
@@ -28,25 +28,35 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 export interface LinkAccountPanelRootDataProps {
-    user?: User;
-    accountToLink?: AccountToLink;
+    user?: UserResource;
+    userToLink?: UserResource;
 }
 
 export interface LinkAccountPanelRootActionProps {
     saveAccountLinkData: (type: LinkAccountType) => void;
     removeAccountLinkData: () => void;
+    linkAccount: () => void;
+}
+
+function displayUser(user: UserResource, showCreatedAt: boolean = false) {
+    const disp = [];
+    disp.push(<span><b>{user.email}</b> ({user.username}, {user.uuid})</span>);
+    if (showCreatedAt) {
+        disp.push(<span> created on <b>{formatDate(user.createdAt)}</b></span>);
+    }
+    return disp;
 }
 
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, user, accountToLink, saveAccountLinkData, removeAccountLinkData}: LinkAccountPanelRootProps) => {
+    ({classes, user, userToLink, saveAccountLinkData, removeAccountLinkData, linkAccount}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-            { user && accountToLink===undefined && <Grid container spacing={24}>
+            { user && userToLink===undefined && <Grid container spacing={24}>
                 <Grid container item direction="column" spacing={24}>
                     <Grid item>
-                        You are currently logged in as <b>{user.email}</b> ({user.username}, {user.uuid}) created on <b>{formatDate(user.createdAt)}</b>
+                        You are currently logged in as {displayUser(user, true)}
                     </Grid>
                     <Grid item>
                         You can link Arvados accounts. After linking, either login will take you to the same account.
@@ -65,7 +75,18 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                     </Grid>
                 </Grid>
             </Grid>}
-            { accountToLink && <Grid container spacing={24}>
+            { userToLink && user && <Grid container spacing={24}>
+                <Grid container item direction="column" spacing={24}>
+                    <Grid item>
+                        Clicking 'Link accounts' will link {displayUser(userToLink, true)} to {displayUser(user, true)}.
+                    </Grid>
+                    <Grid item>
+                        After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(user)}.
+                    </Grid>
+                    <Grid item>
+                       Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(user)}.
+                    </Grid>
+                </Grid>
                 <Grid container item direction="row" spacing={24}>
                     <Grid item>
                         <Button variant="contained" onClick={() => removeAccountLinkData()}>
@@ -73,12 +94,12 @@ export const LinkAccountPanelRoot = withStyles(styles) (
                         </Button>
                     </Grid>
                     <Grid item>
-                        <Button color="primary" variant="contained" onClick={() => {}}>
+                        <Button color="primary" variant="contained" onClick={() => linkAccount()}>
                             Link accounts
                         </Button>
                     </Grid>
                 </Grid>
             </Grid> }
             </CardContent>
-        </Card>;
+        </Card> ;
 });
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index 44b4bb54..498eff88 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -5,7 +5,7 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { saveAccountLinkData, removeAccountLinkData } from '~/store/link-account-panel/link-account-panel-actions';
+import { saveAccountLinkData, removeAccountLinkData, linkAccount } from '~/store/link-account-panel/link-account-panel-actions';
 import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
@@ -15,14 +15,15 @@ import {
 
 const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     return {
-        user: state.auth.user,
-        accountToLink: state.linkAccountPanel.accountToLink
+        user: state.linkAccountPanel.user,
+        userToLink: state.linkAccountPanel.userToLink
     };
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
     saveAccountLinkData: (type: LinkAccountType) => dispatch<any>(saveAccountLinkData(type)),
-    removeAccountLinkData: () => dispatch<any>(removeAccountLinkData())
+    removeAccountLinkData: () => dispatch<any>(removeAccountLinkData()),
+    linkAccount: () => dispatch<any>(linkAccount())
 });
 
 export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);

commit bc9df86de8ce648f14193315f037ca4a2b119773
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Fri Apr 26 15:46:59 2019 -0400

    15088: Adds the ability to cancel account linking after logging in
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index 3a8661b7..dd3a79e9 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -11,7 +11,8 @@ import { logout } from "~/store/auth/auth-action";
 import { unionize, ofType, UnionOf } from '~/common/unionize';
 
 export const linkAccountPanelActions = unionize({
-    LOAD_LINKING: ofType<AccountToLink>()
+    LOAD_LINKING: ofType<AccountToLink>(),
+    REMOVE_LINKING: {}
 });
 
 export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
@@ -36,4 +37,10 @@ export const saveAccountLinkData = (t: LinkAccountType) =>
 export const getAccountLinkData = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         return services.linkAccountService.getLinkAccount();
+    };
+
+export const removeAccountLinkData = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        services.linkAccountService.removeLinkAccount();
+        dispatch(linkAccountPanelActions.REMOVE_LINKING());
     };
\ No newline at end of file
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
index 4515cff4..47718e3b 100644
--- a/src/store/link-account-panel/link-account-panel-reducer.ts
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -17,4 +17,5 @@ export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialSt
     linkAccountPanelActions.match(action, {
         default: () => state,
         LOAD_LINKING: (accountToLink) => ({ ...state, accountToLink }),
+        REMOVE_LINKING: () => ({...state, accountToLink: undefined})
     });
\ No newline at end of file
diff --git a/src/views-components/api-token/api-token.tsx b/src/views-components/api-token/api-token.tsx
index 43c55a92..b0fd0313 100644
--- a/src/views-components/api-token/api-token.tsx
+++ b/src/views-components/api-token/api-token.tsx
@@ -8,10 +8,11 @@ import { connect, DispatchProp } from "react-redux";
 import { authActions, getUserDetails, saveApiToken } from "~/store/auth/auth-action";
 import { getUrlParameter } from "~/common/url";
 import { AuthService } from "~/services/auth-service/auth-service";
-import { navigateToRootProject } from "~/store/navigation/navigation-action";
+import { navigateToRootProject, navigateToLinkAccount } from "~/store/navigation/navigation-action";
 import { User } from "~/models/user";
 import { Config } from "~/common/config";
 import { initSessions } from "~/store/auth/auth-action-session";
+import { getAccountLinkData } from "~/store/link-account-panel/link-account-panel-actions";
 
 interface ApiTokenProps {
     authService: AuthService;
@@ -27,7 +28,12 @@ export const ApiToken = connect()(
             this.props.dispatch<any>(getUserDetails()).then((user: User) => {
                 this.props.dispatch(initSessions(this.props.authService, this.props.config, user));
             }).finally(() => {
-                this.props.dispatch(navigateToRootProject);
+                if (this.props.dispatch(getAccountLinkData())) {
+                    this.props.dispatch(navigateToLinkAccount);
+                }
+                else {
+                    this.props.dispatch(navigateToRootProject);
+                }
             });
         }
         render() {
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index 3b2e2041..4b3b6979 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -34,36 +34,51 @@ export interface LinkAccountPanelRootDataProps {
 
 export interface LinkAccountPanelRootActionProps {
     saveAccountLinkData: (type: LinkAccountType) => void;
+    removeAccountLinkData: () => void;
 }
 
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, user, saveAccountLinkData}: LinkAccountPanelRootProps) => {
+    ({classes, user, accountToLink, saveAccountLinkData, removeAccountLinkData}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-            <Grid container spacing={24}>
-            { user && <Grid container item direction="column" spacing={24}>
-                <Grid item>
-                    You are currently logged in as <b>{user.email}</b> ({user.username}, {user.uuid}) created on <b>{formatDate(user.createdAt)}</b>
+            { user && accountToLink===undefined && <Grid container spacing={24}>
+                <Grid container item direction="column" spacing={24}>
+                    <Grid item>
+                        You are currently logged in as <b>{user.email}</b> ({user.username}, {user.uuid}) created on <b>{formatDate(user.createdAt)}</b>
+                    </Grid>
+                    <Grid item>
+                        You can link Arvados accounts. After linking, either login will take you to the same account.
+                    </Grid>
                 </Grid>
-                <Grid item>
-                    You can link Arvados accounts. After linking, either login will take you to the same account.
+                <Grid container item direction="row" spacing={24}>
+                    <Grid item>
+                        <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ADD_OTHER_LOGIN)}>
+                            Add another login to this account
+                        </Button>
+                    </Grid>
+                    <Grid item>
+                        <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
+                            Use this login to access another account
+                        </Button>
+                    </Grid>
                 </Grid>
-            </Grid> }
-            <Grid container item direction="row" spacing={24}>
-                <Grid item>
-                    <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ADD_OTHER_LOGIN)}>
-                        Add another login to this account
-                    </Button>
-                </Grid>
-                <Grid item>
-                    <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
-                        Use this login to access another account
-                    </Button>
+            </Grid>}
+            { accountToLink && <Grid container spacing={24}>
+                <Grid container item direction="row" spacing={24}>
+                    <Grid item>
+                        <Button variant="contained" onClick={() => removeAccountLinkData()}>
+                            Cancel
+                        </Button>
+                    </Grid>
+                    <Grid item>
+                        <Button color="primary" variant="contained" onClick={() => {}}>
+                            Link accounts
+                        </Button>
+                    </Grid>
                 </Grid>
-            </Grid>
-            </Grid>
+            </Grid> }
             </CardContent>
         </Card>;
 });
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index 491f2055..44b4bb54 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -5,7 +5,7 @@
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
-import { saveAccountLinkData } from '~/store/link-account-panel/link-account-panel-actions';
+import { saveAccountLinkData, removeAccountLinkData } from '~/store/link-account-panel/link-account-panel-actions';
 import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
@@ -21,7 +21,8 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
-    saveAccountLinkData: (type: LinkAccountType) => dispatch<any>(saveAccountLinkData(type))
+    saveAccountLinkData: (type: LinkAccountType) => dispatch<any>(saveAccountLinkData(type)),
+    removeAccountLinkData: () => dispatch<any>(removeAccountLinkData())
 });
 
 export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);

commit 84c94f372cead175c61c3dd1c69486ce87d91539
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Fri Apr 26 15:14:28 2019 -0400

    15088: Adds link-account state and service
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/services/link-account-service/link-account-service.ts b/src/services/link-account-service/link-account-service.ts
new file mode 100644
index 00000000..fe78d484
--- /dev/null
+++ b/src/services/link-account-service/link-account-service.ts
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { AxiosInstance } from "axios";
+import { ApiActions } from "~/services/api/api-actions";
+import { AccountToLink } from "~/models/link-account";
+
+export const USER_LINK_ACCOUNT_KEY = 'accountToLink';
+
+export class LinkAccountService {
+
+    constructor(
+        protected apiClient: AxiosInstance,
+        protected actions: ApiActions) { }
+
+    public saveLinkAccount(account: AccountToLink) {
+        sessionStorage.setItem(USER_LINK_ACCOUNT_KEY, JSON.stringify(account));
+    }
+
+    public removeLinkAccount() {
+        sessionStorage.removeItem(USER_LINK_ACCOUNT_KEY);
+    }
+
+    public getLinkAccount() {
+        const data = sessionStorage.getItem(USER_LINK_ACCOUNT_KEY);
+        return data ? JSON.parse(data) as AccountToLink : undefined;
+    }
+}
\ No newline at end of file
diff --git a/src/services/services.ts b/src/services/services.ts
index 78ea714b..dd317879 100644
--- a/src/services/services.ts
+++ b/src/services/services.ts
@@ -31,6 +31,7 @@ import { AuthorizedKeysService } from '~/services/authorized-keys-service/author
 import { VocabularyService } from '~/services/vocabulary-service/vocabulary-service';
 import { NodeService } from '~/services/node-service/node-service';
 import { FileViewersConfigService } from '~/services/file-viewers-config-service/file-viewers-config-service';
+import { LinkAccountService } from "./link-account-service/link-account-service";
 
 export type ServiceRepository = ReturnType<typeof createServices>;
 
@@ -56,6 +57,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
     const userService = new UserService(apiClient, actions);
     const virtualMachineService = new VirtualMachinesService(apiClient, actions);
     const workflowService = new WorkflowService(apiClient, actions);
+    const linkAccountService = new LinkAccountService(apiClient, actions);
 
     const ancestorsService = new AncestorService(groupsService, userService);
     const authService = new AuthService(apiClient, config.rootUrl, actions);
@@ -94,6 +96,7 @@ export const createServices = (config: Config, actions: ApiActions) => {
         webdavClient,
         workflowService,
         vocabularyService,
+        linkAccountService
     };
 };
 
diff --git a/src/store/link-account-panel/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
index ff7df9b4..3a8661b7 100644
--- a/src/store/link-account-panel/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -8,15 +8,32 @@ import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { LinkAccountType, AccountToLink } from "~/models/link-account";
 import { logout } from "~/store/auth/auth-action";
+import { unionize, ofType, UnionOf } from '~/common/unionize';
+
+export const linkAccountPanelActions = unionize({
+    LOAD_LINKING: ofType<AccountToLink>()
+});
+
+export type LinkAccountPanelAction = UnionOf<typeof linkAccountPanelActions>;
 
 export const loadLinkAccountPanel = () =>
-    (dispatch: Dispatch<any>) => {
-       dispatch(setBreadcrumbs([{ label: 'Link account'}]));
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        dispatch(setBreadcrumbs([{ label: 'Link account'}]));
+
+        const linkAccountData = services.linkAccountService.getLinkAccount();
+        if (linkAccountData) {
+            dispatch(linkAccountPanelActions.LOAD_LINKING(linkAccountData));
+        }
     };
 
 export const saveAccountLinkData = (t: LinkAccountType) =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         const accountToLink = {type: t, userToken: services.authService.getApiToken()} as AccountToLink;
-        sessionStorage.setItem("accountToLink", JSON.stringify(accountToLink));
+        services.linkAccountService.saveLinkAccount(accountToLink);
         dispatch(logout());
+    };
+
+export const getAccountLinkData = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        return services.linkAccountService.getLinkAccount();
     };
\ No newline at end of file
diff --git a/src/store/link-account-panel/link-account-panel-reducer.ts b/src/store/link-account-panel/link-account-panel-reducer.ts
new file mode 100644
index 00000000..4515cff4
--- /dev/null
+++ b/src/store/link-account-panel/link-account-panel-reducer.ts
@@ -0,0 +1,20 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { linkAccountPanelActions, LinkAccountPanelAction } from "~/store/link-account-panel/link-account-panel-actions";
+import { AccountToLink } from "~/models/link-account";
+
+export interface LinkAccountPanelState {
+    accountToLink: AccountToLink | undefined;
+}
+
+const initialState = {
+    accountToLink: undefined
+};
+
+export const linkAccountPanelReducer = (state: LinkAccountPanelState = initialState, action: LinkAccountPanelAction) =>
+    linkAccountPanelActions.match(action, {
+        default: () => state,
+        LOAD_LINKING: (accountToLink) => ({ ...state, accountToLink }),
+    });
\ No newline at end of file
diff --git a/src/store/store.ts b/src/store/store.ts
index 9d7dcdd4..6a37572b 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -62,6 +62,7 @@ import { ApiClientAuthorizationMiddlewareService } from '~/store/api-client-auth
 import { PublicFavoritesMiddlewareService } from '~/store/public-favorites-panel/public-favorites-middleware-service';
 import { PUBLIC_FAVORITE_PANEL_ID } from '~/store/public-favorites-panel/public-favorites-action';
 import { publicFavoritesReducer } from '~/store/public-favorites/public-favorites-reducer';
+import { linkAccountPanelReducer } from './link-account-panel/link-account-panel-reducer';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -163,5 +164,6 @@ const createRootReducer = (services: ServiceRepository) => combineReducers({
     searchBar: searchBarReducer,
     virtualMachines: virtualMachinesReducer,
     repositories: repositoriesReducer,
-    keepServices: keepServicesReducer
+    keepServices: keepServicesReducer,
+    linkAccountPanel: linkAccountPanelReducer
 });
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index ce27d265..3b2e2041 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -15,7 +15,7 @@ import {
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { User } from "~/models/user";
-import { LinkAccountType } from "~/models/link-account";
+import { LinkAccountType, AccountToLink } from "~/models/link-account";
 import { formatDate }from "~/common/formatters";
 
 type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
@@ -29,6 +29,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 
 export interface LinkAccountPanelRootDataProps {
     user?: User;
+    accountToLink?: AccountToLink;
 }
 
 export interface LinkAccountPanelRootActionProps {
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index eff8ca80..491f2055 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -15,7 +15,8 @@ import {
 
 const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     return {
-        user: state.auth.user
+        user: state.auth.user,
+        accountToLink: state.linkAccountPanel.accountToLink
     };
 };
 

commit fc1c9792135dfbf36c148985b402871339d98561
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Thu Apr 25 15:16:09 2019 -0400

    15088: Saves account link data in session storage and logs out
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/models/link-account.ts b/src/models/link-account.ts
new file mode 100644
index 00000000..dd22bc95
--- /dev/null
+++ b/src/models/link-account.ts
@@ -0,0 +1,13 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export enum LinkAccountType {
+    ADD_OTHER_LOGIN,
+    ACCESS_OTHER_ACCOUNT
+}
+
+export interface AccountToLink {
+    type: LinkAccountType;
+    userToken: string;
+}
diff --git a/src/store/link-account/link-account-panel-actions.ts b/src/store/link-account-panel/link-account-panel-actions.ts
similarity index 53%
rename from src/store/link-account/link-account-panel-actions.ts
rename to src/store/link-account-panel/link-account-panel-actions.ts
index 9d0b6a05..ff7df9b4 100644
--- a/src/store/link-account/link-account-panel-actions.ts
+++ b/src/store/link-account-panel/link-account-panel-actions.ts
@@ -4,13 +4,19 @@
 
 import { Dispatch } from "redux";
 import { RootState } from "~/store/store";
-import { initialize } from "redux-form";
 import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
-import { authActions } from "~/store/auth/auth-action";
-import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
+import { LinkAccountType, AccountToLink } from "~/models/link-account";
+import { logout } from "~/store/auth/auth-action";
 
 export const loadLinkAccountPanel = () =>
-    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch<any>) => {
        dispatch(setBreadcrumbs([{ label: 'Link account'}]));
     };
+
+export const saveAccountLinkData = (t: LinkAccountType) =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        const accountToLink = {type: t, userToken: services.authService.getApiToken()} as AccountToLink;
+        sessionStorage.setItem("accountToLink", JSON.stringify(accountToLink));
+        dispatch(logout());
+    };
\ No newline at end of file
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 10f86a68..163a1885 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -59,7 +59,7 @@ import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
 import { loadSshKeysPanel } from '~/store/auth/auth-action-ssh';
 import { loadMyAccountPanel } from '~/store/my-account/my-account-panel-actions';
-import { loadLinkAccountPanel } from '~/store/link-account/link-account-panel-actions';
+import { loadLinkAccountPanel } from '~/store/link-account-panel/link-account-panel-actions';
 import { loadSiteManagerPanel } from '~/store/auth/auth-action-session';
 import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
index ceb3ffae..ce27d265 100644
--- a/src/views/link-account-panel/link-account-panel-root.tsx
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -15,6 +15,7 @@ import {
 } from '@material-ui/core';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { User } from "~/models/user";
+import { LinkAccountType } from "~/models/link-account";
 import { formatDate }from "~/common/formatters";
 
 type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
@@ -30,12 +31,14 @@ export interface LinkAccountPanelRootDataProps {
     user?: User;
 }
 
-export interface LinkAccountPanelRootActionProps { }
+export interface LinkAccountPanelRootActionProps {
+    saveAccountLinkData: (type: LinkAccountType) => void;
+}
 
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
 export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, user}: LinkAccountPanelRootProps) => {
+    ({classes, user, saveAccountLinkData}: LinkAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
             <Grid container spacing={24}>
@@ -49,10 +52,14 @@ export const LinkAccountPanelRoot = withStyles(styles) (
             </Grid> }
             <Grid container item direction="row" spacing={24}>
                 <Grid item>
-                    <Button color="primary" variant="contained">Add another login to this account</Button>
+                    <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ADD_OTHER_LOGIN)}>
+                        Add another login to this account
+                    </Button>
                 </Grid>
                 <Grid item>
-                    <Button color="primary" variant="contained">Use this login to access another account</Button>
+                    <Button color="primary" variant="contained" onClick={() => saveAccountLinkData(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
+                        Use this login to access another account
+                    </Button>
                 </Grid>
             </Grid>
             </Grid>
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
index c49b511d..eff8ca80 100644
--- a/src/views/link-account-panel/link-account-panel.tsx
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -4,12 +4,9 @@
 
 import { RootState } from '~/store/store';
 import { Dispatch } from 'redux';
-import { compose } from 'redux';
-import { reduxForm } from 'redux-form';
 import { connect } from 'react-redux';
-import { getResource, ResourcesState } from '~/store/resources/resources';
-import { Resource } from '~/models/resource';
-import { User, UserResource } from '~/models/user';
+import { saveAccountLinkData } from '~/store/link-account-panel/link-account-panel-actions';
+import { LinkAccountType } from '~/models/link-account';
 import {
     LinkAccountPanelRoot,
     LinkAccountPanelRootDataProps,
@@ -22,6 +19,8 @@ const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     };
 };
 
-const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({});
+const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({
+    saveAccountLinkData: (type: LinkAccountType) => dispatch<any>(saveAccountLinkData(type))
+});
 
 export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);

commit 43a384e98b698de75f66dcc5a0241a1246ddd447
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Thu Apr 25 10:32:31 2019 -0400

    15088: Adds initial link account UI
    
    Also adds the createdAt attribute to the user state
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/models/user.ts b/src/models/user.ts
index 24978645..afc4fc72 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -24,6 +24,7 @@ export interface User {
     prefs: UserPrefs;
     isAdmin: boolean;
     isActive: boolean;
+    createdAt: string;
 }
 
 export const getUserFullname = (user?: User) => {
diff --git a/src/services/auth-service/auth-service.ts b/src/services/auth-service/auth-service.ts
index eae219dd..e7641fdc 100644
--- a/src/services/auth-service/auth-service.ts
+++ b/src/services/auth-service/auth-service.ts
@@ -20,6 +20,7 @@ export const USER_IS_ADMIN = 'isAdmin';
 export const USER_IS_ACTIVE = 'isActive';
 export const USER_USERNAME = 'username';
 export const USER_PREFS = 'prefs';
+export const USER_CREATED_AT = 'createdAt';
 
 export interface UserDetailsResponse {
     email: string;
@@ -30,6 +31,7 @@ export interface UserDetailsResponse {
     is_admin: boolean;
     is_active: boolean;
     username: string;
+    created_at: string;
     prefs: UserPrefs;
 }
 
@@ -77,10 +79,11 @@ export class AuthService {
         const isAdmin = this.getIsAdmin();
         const isActive = this.getIsActive();
         const username = localStorage.getItem(USER_USERNAME);
+        const createdAt = localStorage.getItem(USER_CREATED_AT);
         const prefs = JSON.parse(localStorage.getItem(USER_PREFS) || '{"profile": {}}');
 
-        return email && firstName && lastName && uuid && ownerUuid && username && prefs
-            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, prefs }
+        return email && firstName && lastName && uuid && ownerUuid && username && createdAt && prefs
+            ? { email, firstName, lastName, uuid, ownerUuid, isAdmin, isActive, username, createdAt, prefs }
             : undefined;
     }
 
@@ -93,6 +96,7 @@ export class AuthService {
         localStorage.setItem(USER_IS_ADMIN, JSON.stringify(user.isAdmin));
         localStorage.setItem(USER_IS_ACTIVE, JSON.stringify(user.isActive));
         localStorage.setItem(USER_USERNAME, user.username);
+        localStorage.setItem(USER_CREATED_AT, user.createdAt);
         localStorage.setItem(USER_PREFS, JSON.stringify(user.prefs));
     }
 
@@ -105,6 +109,7 @@ export class AuthService {
         localStorage.removeItem(USER_IS_ADMIN);
         localStorage.removeItem(USER_IS_ACTIVE);
         localStorage.removeItem(USER_USERNAME);
+        localStorage.removeItem(USER_CREATED_AT);
         localStorage.removeItem(USER_PREFS);
     }
 
@@ -135,6 +140,7 @@ export class AuthService {
                     isAdmin: resp.data.is_admin,
                     isActive: resp.data.is_active,
                     username: resp.data.username,
+                    createdAt: resp.data.created_at,
                     prefs
                 };
             })
diff --git a/src/store/auth/auth-action-session.ts b/src/store/auth/auth-action-session.ts
index 5bb192b8..0a41fe58 100644
--- a/src/store/auth/auth-action-session.ts
+++ b/src/store/auth/auth-action-session.ts
@@ -94,6 +94,7 @@ const clusterLogin = async (clusterId: string, baseUrl: string, activeSession: S
             isAdmin: user.is_admin,
             isActive: user.is_active,
             username: user.username,
+            createdAt: user.created_at,
             prefs: user.prefs
         },
         token: saltedToken
diff --git a/src/store/auth/auth-reducer.test.ts b/src/store/auth/auth-reducer.test.ts
index 38cf1581..e0088182 100644
--- a/src/store/auth/auth-reducer.test.ts
+++ b/src/store/auth/auth-reducer.test.ts
@@ -33,7 +33,8 @@ describe('auth-reducer', () => {
             username: "username",
             prefs: {},
             isAdmin: false,
-            isActive: true
+            isActive: true,
+            createdAt: "createdAt"
         };
         const state = reducer(initialState, authActions.INIT({ user, token: "token" }));
         expect(state).toEqual({
@@ -74,7 +75,8 @@ describe('auth-reducer', () => {
             username: "username",
             prefs: {},
             isAdmin: false,
-            isActive: true
+            isActive: true,
+            createdAt: "createdAt"
         };
 
         const state = reducer(initialState, authActions.USER_DETAILS_SUCCESS(user));
@@ -94,7 +96,8 @@ describe('auth-reducer', () => {
                 username: "username",
                 prefs: {},
                 isAdmin: false,
-                isActive: true
+                isActive: true,
+                createdAt: "createdAt"
             }
         });
     });
diff --git a/src/store/link-account/link-account-panel-actions.ts b/src/store/link-account/link-account-panel-actions.ts
index 51397524..9d0b6a05 100644
--- a/src/store/link-account/link-account-panel-actions.ts
+++ b/src/store/link-account/link-account-panel-actions.ts
@@ -10,8 +10,6 @@ import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { authActions } from "~/store/auth/auth-action";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
 
-export const LINK_ACCOUNT_FORM = 'linkAccountForm';
-
 export const loadLinkAccountPanel = () =>
     (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
        dispatch(setBreadcrumbs([{ label: 'Link account'}]));
diff --git a/src/views/link-account-panel/link-account-panel-root.tsx b/src/views/link-account-panel/link-account-panel-root.tsx
new file mode 100644
index 00000000..ceb3ffae
--- /dev/null
+++ b/src/views/link-account-panel/link-account-panel-root.tsx
@@ -0,0 +1,61 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Card,
+    CardContent,
+    Button,
+    Typography,
+    Grid,
+} from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { User } from "~/models/user";
+import { formatDate }from "~/common/formatters";
+
+type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+        overflow: 'auto'
+    }
+});
+
+export interface LinkAccountPanelRootDataProps {
+    user?: User;
+}
+
+export interface LinkAccountPanelRootActionProps { }
+
+type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
+
+export const LinkAccountPanelRoot = withStyles(styles) (
+    ({classes, user}: LinkAccountPanelRootProps) => {
+        return <Card className={classes.root}>
+            <CardContent>
+            <Grid container spacing={24}>
+            { user && <Grid container item direction="column" spacing={24}>
+                <Grid item>
+                    You are currently logged in as <b>{user.email}</b> ({user.username}, {user.uuid}) created on <b>{formatDate(user.createdAt)}</b>
+                </Grid>
+                <Grid item>
+                    You can link Arvados accounts. After linking, either login will take you to the same account.
+                </Grid>
+            </Grid> }
+            <Grid container item direction="row" spacing={24}>
+                <Grid item>
+                    <Button color="primary" variant="contained">Add another login to this account</Button>
+                </Grid>
+                <Grid item>
+                    <Button color="primary" variant="contained">Use this login to access another account</Button>
+                </Grid>
+            </Grid>
+            </Grid>
+            </CardContent>
+        </Card>;
+});
\ No newline at end of file
diff --git a/src/views/link-account-panel/link-account-panel.tsx b/src/views/link-account-panel/link-account-panel.tsx
new file mode 100644
index 00000000..c49b511d
--- /dev/null
+++ b/src/views/link-account-panel/link-account-panel.tsx
@@ -0,0 +1,27 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
+import { compose } from 'redux';
+import { reduxForm } from 'redux-form';
+import { connect } from 'react-redux';
+import { getResource, ResourcesState } from '~/store/resources/resources';
+import { Resource } from '~/models/resource';
+import { User, UserResource } from '~/models/user';
+import {
+    LinkAccountPanelRoot,
+    LinkAccountPanelRootDataProps,
+    LinkAccountPanelRootActionProps
+} from '~/views/link-account-panel/link-account-panel-root';
+
+const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
+    return {
+        user: state.auth.user
+    };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps => ({});
+
+export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index a31c7d25..07b05655 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -92,6 +92,7 @@ import { GroupMemberAttributesDialog } from '~/views-components/groups-dialog/me
 import { AddGroupMembersDialog } from '~/views-components/dialog-forms/add-group-member-dialog';
 import { PartialCopyToCollectionDialog } from '~/views-components/dialog-forms/partial-copy-to-collection-dialog';
 import { PublicFavoritePanel } from '~/views/public-favorites-panel/public-favorites-panel';
+import { LinkAccountPanel } from '~/views/link-account-panel/link-account-panel';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -176,6 +177,7 @@ export const WorkbenchPanel =
                                 <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
                                 <Route path={Routes.LINKS} component={LinkPanel} />
                                 <Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
+                                <Route path={Routes.LINK_ACCOUNT} component={LinkAccountPanel} />
                             </Switch>
                         </Grid>
                     </Grid>

commit 6dfcd99cab6ea26ef947bdc2c90020ccea1c925b
Author: Eric Biagiotti <ebiagiotti at veritasgenetics.com>
Date:   Tue Apr 23 11:09:35 2019 -0400

    15088: Adds routing for link-account panel
    
    Also adds a link in the my-account panel to access the link-account panel
    
    Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti at veritasgenetics.com>

diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts
index 2811f95a..e0a51550 100644
--- a/src/routes/route-change-handlers.ts
+++ b/src/routes/route-change-handlers.ts
@@ -41,6 +41,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const computeNodesMatch = Routes.matchComputeNodesRoute(pathname);
     const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
     const myAccountMatch = Routes.matchMyAccountRoute(pathname);
+    const linkAccountMatch = Routes.matchLinkAccountRoute(pathname);
     const userMatch = Routes.matchUsersRoute(pathname);
     const groupsMatch = Routes.matchGroupsRoute(pathname);
     const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
@@ -94,6 +95,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(WorkbenchActions.loadApiClientAuthorizations);
     } else if (myAccountMatch) {
         store.dispatch(WorkbenchActions.loadMyAccount);
+    } else if (linkAccountMatch) {
+        store.dispatch(WorkbenchActions.loadLinkAccount);
     } else if (userMatch) {
         store.dispatch(WorkbenchActions.loadUsers);
     } else if (groupsMatch) {
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 3fd6670d..02835fcc 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -27,6 +27,7 @@ export const Routes = {
     SSH_KEYS_USER: `/ssh-keys-user`,
     SITE_MANAGER: `/site-manager`,
     MY_ACCOUNT: '/my-account',
+    LINK_ACCOUNT: '/link_account',
     KEEP_SERVICES: `/keep-services`,
     COMPUTE_NODES: `/nodes`,
     USERS: '/users',
@@ -115,6 +116,9 @@ export const matchSiteManagerRoute = (route: string) =>
 export const matchMyAccountRoute = (route: string) =>
     matchPath(route, { path: Routes.MY_ACCOUNT });
 
+export const matchLinkAccountRoute = (route: string) =>
+    matchPath(route, { path: Routes.LINK_ACCOUNT });
+
 export const matchKeepServicesRoute = (route: string) =>
     matchPath(route, { path: Routes.KEEP_SERVICES });
 
diff --git a/src/store/link-account/link-account-panel-actions.ts b/src/store/link-account/link-account-panel-actions.ts
new file mode 100644
index 00000000..51397524
--- /dev/null
+++ b/src/store/link-account/link-account-panel-actions.ts
@@ -0,0 +1,18 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "~/store/store";
+import { initialize } from "redux-form";
+import { ServiceRepository } from "~/services/services";
+import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
+import { authActions } from "~/store/auth/auth-action";
+import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
+
+export const LINK_ACCOUNT_FORM = 'linkAccountForm';
+
+export const loadLinkAccountPanel = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+       dispatch(setBreadcrumbs([{ label: 'Link account'}]));
+    };
diff --git a/src/store/my-account/my-account-panel-actions.ts b/src/store/my-account/my-account-panel-actions.ts
index 34bb2693..294f77f6 100644
--- a/src/store/my-account/my-account-panel-actions.ts
+++ b/src/store/my-account/my-account-panel-actions.ts
@@ -9,6 +9,7 @@ import { ServiceRepository } from "~/services/services";
 import { setBreadcrumbs } from "~/store/breadcrumbs/breadcrumbs-actions";
 import { authActions } from "~/store/auth/auth-action";
 import { snackbarActions, SnackbarKind } from "~/store/snackbar/snackbar-actions";
+import { navigateToLinkAccount } from '~/store/navigation/navigation-action';
 
 export const MY_ACCOUNT_FORM = 'myAccountForm';
 
@@ -17,6 +18,11 @@ export const loadMyAccountPanel = () =>
         dispatch(setBreadcrumbs([{ label: 'User profile'}]));
     };
 
+export const openLinkAccount = () =>
+    (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+        dispatch<any>(navigateToLinkAccount);
+    };
+
 export const saveEditedUser = (resource: any) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
         try {
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index af7a0c03..3bec1609 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -85,6 +85,8 @@ export const navigateToSiteManager= push(Routes.SITE_MANAGER);
 
 export const navigateToMyAccount = push(Routes.MY_ACCOUNT);
 
+export const navigateToLinkAccount = push(Routes.LINK_ACCOUNT);
+
 export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
 
 export const navigateToComputeNodes = push(Routes.COMPUTE_NODES);
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index adf3fa15..10f86a68 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -59,6 +59,7 @@ import { CopyFormDialogData } from '~/store/copy-dialog/copy-dialog';
 import { loadWorkflowPanel, workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
 import { loadSshKeysPanel } from '~/store/auth/auth-action-ssh';
 import { loadMyAccountPanel } from '~/store/my-account/my-account-panel-actions';
+import { loadLinkAccountPanel } from '~/store/link-account/link-account-panel-actions';
 import { loadSiteManagerPanel } from '~/store/auth/auth-action-session';
 import { workflowPanelColumns } from '~/views/workflow-panel/workflow-panel-view';
 import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions';
@@ -485,6 +486,11 @@ export const loadMyAccount = handleFirstTimeLoad(
         dispatch(loadMyAccountPanel());
     });
 
+export const loadLinkAccount = handleFirstTimeLoad(
+    (dispatch: Dispatch<any>) => {
+        dispatch(loadLinkAccountPanel());
+    });
+
 export const loadKeepServices = handleFirstTimeLoad(
     async (dispatch: Dispatch<any>) => {
         await dispatch(loadKeepServicesPanel());
diff --git a/src/views/my-account-panel/my-account-panel-root.tsx b/src/views/my-account-panel/my-account-panel-root.tsx
index e84b3b64..7dcbe09a 100644
--- a/src/views/my-account-panel/my-account-panel-root.tsx
+++ b/src/views/my-account-panel/my-account-panel-root.tsx
@@ -6,6 +6,7 @@ import * as React from 'react';
 import { Field, InjectedFormProps, WrappedFieldProps } from "redux-form";
 import { TextField } from "~/components/text-field/text-field";
 import { NativeSelectField } from "~/components/select-field/select-field";
+import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
 import {
     StyleRulesCallback,
     WithStyles,
@@ -21,7 +22,7 @@ import { ArvadosTheme } from '~/common/custom-theme';
 import { User } from "~/models/user";
 import { MY_ACCOUNT_VALIDATION } from "~/validators/validators";
 
-type CssRules = 'root' | 'gridItem' | 'label' | 'title' | 'actions';
+type CssRules = 'root' | 'gridItem' | 'label' | 'title' | 'actions' | 'link';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -39,13 +40,24 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         marginBottom: theme.spacing.unit * 3,
         color: theme.palette.grey["600"]
     },
+    link: {
+        lineHeight:'2.1',
+        whiteSpace: 'nowrap',
+        fontSize: '1rem',
+        color: theme.palette.primary.main,
+        '&:hover': {
+            cursor: 'pointer'
+        }
+    },
     actions: {
         display: 'flex',
         justifyContent: 'flex-end'
     }
 });
 
-export interface MyAccountPanelRootActionProps { }
+export interface MyAccountPanelRootActionProps {
+    openLinkAccount: () => void;
+ }
 
 export interface MyAccountPanelRootDataProps {
     isPristine: boolean;
@@ -64,7 +76,7 @@ const RoleTypes = [
     { key: 'Other', value: 'Other' }
 ];
 
-type MyAccountPanelRootProps = InjectedFormProps<MyAccountPanelRootActionProps> & MyAccountPanelRootDataProps & WithStyles<CssRules>;
+type MyAccountPanelRootProps = InjectedFormProps & MyAccountPanelRootActionProps & MyAccountPanelRootDataProps & WithStyles<CssRules>;
 
 type LocalClusterProp = { localCluster: string };
 const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
@@ -72,12 +84,21 @@ const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = (
 );
 
 export const MyAccountPanelRoot = withStyles(styles)(
-    ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting, localCluster }: MyAccountPanelRootProps) => {
+    ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting, localCluster, openLinkAccount}: MyAccountPanelRootProps) => {
         return <Card className={classes.root}>
             <CardContent>
-                <Typography variant="title" className={classes.title}>
-                    Logged in as <Field name="uuid" component={renderField} localCluster={localCluster} />
-                </Typography>
+                <Grid container spacing={24}>
+                    <Grid item className={classes.gridItem}>
+                        <Typography variant="title" className={classes.title}>
+                            Logged in as <Field name="uuid" component={renderField} localCluster={localCluster} />
+                        </Typography>
+                    </Grid>
+                    <Grid item className={classes.gridItem}>
+                        <span onClick={() => openLinkAccount()}>
+                            <DetailsAttribute classLabel={classes.link} label='Link account' />
+                        </span>
+                    </Grid>
+                </Grid>
                 <form onSubmit={handleSubmit}>
                     <Grid container spacing={24}>
                         <Grid item className={classes.gridItem} sm={6} xs={12}>
diff --git a/src/views/my-account-panel/my-account-panel.tsx b/src/views/my-account-panel/my-account-panel.tsx
index bd1f5874..85f285d6 100644
--- a/src/views/my-account-panel/my-account-panel.tsx
+++ b/src/views/my-account-panel/my-account-panel.tsx
@@ -3,11 +3,12 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import { RootState } from '~/store/store';
+import { Dispatch } from 'redux';
 import { compose } from 'redux';
 import { reduxForm, isPristine, isValid } from 'redux-form';
 import { connect } from 'react-redux';
-import { saveEditedUser } from '~/store/my-account/my-account-panel-actions';
-import { MyAccountPanelRoot, MyAccountPanelRootDataProps } from '~/views/my-account-panel/my-account-panel-root';
+import { saveEditedUser, openLinkAccount } from '~/store/my-account/my-account-panel-actions';
+import { MyAccountPanelRoot, MyAccountPanelRootDataProps, MyAccountPanelRootActionProps } from '~/views/my-account-panel/my-account-panel-root';
 import { MY_ACCOUNT_FORM } from "~/store/my-account/my-account-panel-actions";
 
 const mapStateToProps = (state: RootState): MyAccountPanelRootDataProps => ({
@@ -17,8 +18,12 @@ const mapStateToProps = (state: RootState): MyAccountPanelRootDataProps => ({
     localCluster: state.auth.localCluster
 });
 
+const mapDispatchToProps = (dispatch: Dispatch): MyAccountPanelRootActionProps => ({
+    openLinkAccount: () => dispatch<any>(openLinkAccount())
+});
+
 export const MyAccountPanel = compose(
-    connect(mapStateToProps),
+    connect(mapStateToProps, mapDispatchToProps),
     reduxForm({
         form: MY_ACCOUNT_FORM,
         onSubmit: (data, dispatch) => {

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list