15088: Adds federated account linking
authorEric Biagiotti <ebiagiotti@veritasgenetics.com>
Wed, 15 May 2019 19:46:40 +0000 (15:46 -0400)
committerEric Biagiotti <ebiagiotti@veritasgenetics.com>
Wed, 15 May 2019 19:46:40 +0000 (15:46 -0400)
Arvados-DCO-1.1-Signed-off-by: Eric Biagiotti <ebiagiotti@veritasgenetics.com>

src/store/link-account-panel/link-account-panel-actions.ts
src/store/link-account-panel/link-account-panel-reducer.ts
src/views/link-account-panel/link-account-panel-root.tsx
src/views/link-account-panel/link-account-panel.tsx

index e63da1a42ac838f8d089771391ceb80dd94f8dac..b531b82da55e79adad547a4c37725e052775b569 100644 (file)
@@ -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 = () =>
index 0e61abdd2f55bf094cfe96716cd223fdafd92a65..3d20584219b90fdd2b48d10182aa01586242e19c 100644 (file)
@@ -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
         })
index 19c4b97af069edbd74b48462702beeac54f352f3..581e86dde429658256c85ed216c256ce69455de5 100644 (file)
@@ -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&nbsp;{hasRemoteHosts ? <label> from {selectedCluster} </label> : null}&nbsp;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&nbsp;{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)}.
index f620b5689e375f9552642d367cee85c98c5e5455..0635911129a6e66bd0ef5f58ed04958ff380ccb4 100644 (file)
@@ -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);