Merge branch 'master' into 15530-wb2-logincluster
authorPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 28 Oct 2019 15:52:17 +0000 (11:52 -0400)
committerPeter Amstutz <pamstutz@veritasgenetics.com>
Mon, 28 Oct 2019 15:52:17 +0000 (11:52 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <pamstutz@veritasgenetics.com>

17 files changed:
src/common/config.ts
src/services/auth-service/auth-service.ts
src/store/auth/auth-action-session.ts
src/store/auth/auth-action.test.ts
src/store/auth/auth-action.ts
src/store/auth/auth-reducer.test.ts
src/store/auth/auth-reducer.ts
src/store/link-account-panel/link-account-panel-actions.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/main-app-bar/anonymous-menu.tsx
src/views/link-account-panel/link-account-panel-root.tsx
src/views/link-account-panel/link-account-panel.tsx
src/views/login-panel/login-panel.tsx
src/views/search-results-panel/search-results-panel-view.tsx
src/views/site-manager-panel/site-manager-panel-root.tsx
src/views/site-manager-panel/site-manager-panel.tsx
src/views/workbench/fed-login.tsx

index 1f668292c32c3a5abfbdefee99dd2b3a7ecee441..758a77ba91ed688c5c81ea2dd8d2be3dd6dff23e 100644 (file)
@@ -46,6 +46,9 @@ export interface ClusterConfigJSON {
         FileViewersConfigURL: string;
         WelcomePageHTML: string;
     };
+    Login: {
+        LoginCluster: string;
+    };
 }
 
 export class Config {
@@ -61,6 +64,7 @@ export class Config {
     workbench2Url: string;
     vocabularyUrl: string;
     fileViewersConfigUrl: string;
+    loginCluster: string;
     clusterConfig: ClusterConfigJSON;
 }
 
@@ -113,6 +117,7 @@ remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`);
                 config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
                 config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
                 config.keepWebServiceUrl = clusterConfigJSON.Services.WebDAV.ExternalURL;
+                config.loginCluster = clusterConfigJSON.Login.LoginCluster;
                 config.clusterConfig = clusterConfigJSON;
                 mapRemoteHosts(clusterConfigJSON, config);
 
@@ -144,6 +149,9 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
         FileViewersConfigURL: "",
         WelcomePageHTML: "",
     },
+    Login: {
+        LoginCluster: "",
+    },
     ...config
 });
 
@@ -158,6 +166,7 @@ export const mockConfig = (config: Partial<Config>): Config => ({
     workbench2Url: "",
     vocabularyUrl: "",
     fileViewersConfigUrl: "",
+    loginCluster: "",
     clusterConfig: mockClusterConfigJSON({}),
     ...config
 });
index a80d89ba146f374f329ff62f179217997fd34e6f..da96f1629b2fc9e3cf76ba91c3215df43bd17666 100644 (file)
@@ -114,10 +114,10 @@ export class AuthService {
         localStorage.removeItem(USER_PREFS);
     }
 
-    public login(uuidPrefix: string, homeCluster: string, remoteHosts: { [key: string]: string }) {
+    public login(uuidPrefix: string, homeCluster: string, loginCluster: string, remoteHosts: { [key: string]: string }) {
         const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
         const homeClusterHost = remoteHosts[homeCluster];
-        window.location.assign(`https://${homeClusterHost}/login?${uuidPrefix !== homeCluster ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
+        window.location.assign(`https://${homeClusterHost}/login?${(uuidPrefix !== homeCluster && homeCluster !== loginCluster) ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
     }
 
     public logout() {
@@ -183,7 +183,12 @@ export class AuthService {
             active: true,
             status: SessionStatus.VALIDATED
         } as Session;
-        const localSessions = this.getSessions();
+        const localSessions = this.getSessions().map(s => ({
+            ...s,
+            active: false,
+            status: SessionStatus.INVALIDATED
+        }));
+
         const cfgSessions = Object.keys(cfg.remoteHosts).map(clusterId => {
             const remoteHost = cfg.remoteHosts[clusterId];
             return {
@@ -199,8 +204,9 @@ export class AuthService {
             } as Session;
         });
         const sessions = [currentSession]
+            .concat(cfgSessions)
             .concat(localSessions)
-            .concat(cfgSessions);
+            .filter((r: Session) => r.clusterId !== "*");
 
         const uniqSessions = uniqBy(sessions, 'clusterId');
 
index ca2e23269aff5a82578db3731bfb36d846069848..6af72e0c7e413bf5f5be384fe9e605a93e62016a 100644 (file)
@@ -203,6 +203,7 @@ export const initSessions = (authService: AuthService, config: Config, user: Use
         const sessions = authService.buildSessions(config, user);
         authService.saveSessions(sessions);
         dispatch(authActions.SET_SESSIONS(sessions));
+        dispatch(validateSessions());
     };
 
 export const loadSiteManagerPanel = () =>
index 1ebc8db993be3fc3d82125f24a59ac54dce8bba4..801d9e33ee9cb9d3e8a266298712fc7bdfe1b956 100644 (file)
@@ -23,7 +23,7 @@ import { configureStore, RootStore } from "../store";
 import createBrowserHistory from "history/createBrowserHistory";
 import { mockConfig } from '~/common/config';
 import { ApiActions } from "~/services/api/api-actions";
-import { ACCOUNT_LINK_STATUS_KEY} from '~/services/link-account-service/link-account-service';
+import { ACCOUNT_LINK_STATUS_KEY } from '~/services/link-account-service/link-account-service';
 
 describe('auth-actions', () => {
     let reducer: (state: AuthState | undefined, action: AuthAction) => any;
@@ -67,7 +67,16 @@ describe('auth-actions', () => {
             sshKeys: [],
             homeCluster: "zzzzz",
             localCluster: "zzzzz",
-            remoteHostsConfig: {},
+            loginCluster: undefined,
+            remoteHostsConfig: {
+                "zzzzz": {
+                    "remoteHosts": {
+                        "xc59z": "xc59z.arvadosapi.com",
+                    },
+                    "rootUrl": "https://zzzzz.arvadosapi.com",
+                    "uuidPrefix": "zzzzz",
+                },
+            },
             remoteHosts: {
                 zzzzz: "zzzzz.arvadosapi.com",
                 xc59z: "xc59z.arvadosapi.com"
@@ -89,7 +98,7 @@ describe('auth-actions', () => {
                 "email": "",
                 "loggedIn": false,
                 "remoteHost": "xc59z.arvadosapi.com",
-                "status": 0,
+                "status": 1,
                 "token": "",
                 "username": ""
             }],
index b889adf571113494f32fee49843ebeabe527f5a9..e273d18c1b4327bb82c2c8ec4e4b170b9ed9c8d5 100644 (file)
@@ -72,7 +72,7 @@ const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState,
         setAuthorizationHeader(services, token);
     }
     dispatch(authActions.CONFIG({ config }));
-    dispatch(authActions.SET_HOME_CLUSTER(homeCluster || config.uuidPrefix));
+    dispatch(authActions.SET_HOME_CLUSTER(config.loginCluster || homeCluster || config.uuidPrefix));
     if (token && user) {
         dispatch(authActions.INIT({ user, token }));
         dispatch<any>(initSessions(services.authService, config, user));
@@ -93,8 +93,9 @@ const init = (config: Config) => (dispatch: Dispatch, getState: () => RootState,
                 const remoteConfig = new Config();
                 remoteConfig.uuidPrefix = response.data.ClusterID;
                 remoteConfig.workbench2Url = response.data.Services.Workbench2.ExternalURL;
+                remoteConfig.loginCluster = response.data.Login.LoginCluster;
                 mapRemoteHosts(response.data, remoteConfig);
-                dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config: remoteConfig}));
+                dispatch(authActions.REMOTE_CLUSTER_CONFIG({ config: remoteConfig }));
             });
     });
 };
@@ -110,10 +111,11 @@ export const saveUser = (user: UserResource) => (dispatch: Dispatch, getState: (
     dispatch(authActions.SAVE_USER(user));
 };
 
-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());
-};
+export const login = (uuidPrefix: string, homeCluster: string, loginCluster: string,
+    remoteHosts: { [key: string]: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        services.authService.login(uuidPrefix, homeCluster, loginCluster, remoteHosts);
+        dispatch(authActions.LOGIN());
+    };
 
 export const logout = (deleteLinkData: boolean = false) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
     if (deleteLinkData) {
index 14d92803cc1fc21750d662d723ded60c2d3221ad..8311e861ed67e647879a5f2ebf1b09b98a305b13 100644 (file)
@@ -43,6 +43,7 @@ describe('auth-reducer', () => {
             sessions: [],
             homeCluster: "zzzzz",
             localCluster: "",
+            loginCluster: "",
             remoteHosts: {},
             remoteHostsConfig: {}
         });
@@ -59,6 +60,7 @@ describe('auth-reducer', () => {
             sessions: [],
             homeCluster: "",
             localCluster: "",
+            loginCluster: "",
             remoteHosts: {},
             remoteHostsConfig: {}
         });
@@ -86,6 +88,7 @@ describe('auth-reducer', () => {
             sessions: [],
             homeCluster: "",
             localCluster: "",
+            loginCluster: "",
             remoteHosts: {},
             remoteHostsConfig: {},
             user: {
index cded9f0e71816636ce1876d51c83d3976abb589c..e932b97dd1920f3f6f75a2fd0fb81707e1208956 100644 (file)
@@ -16,6 +16,7 @@ export interface AuthState {
     sessions: Session[];
     localCluster: string;
     homeCluster: string;
+    loginCluster: string;
     remoteHosts: { [key: string]: string };
     remoteHostsConfig: { [key: string]: Config };
 }
@@ -27,6 +28,7 @@ const initialState: AuthState = {
     sessions: [],
     localCluster: "",
     homeCluster: "",
+    loginCluster: "",
     remoteHosts: {},
     remoteHostsConfig: {}
 };
@@ -37,14 +39,16 @@ export const authReducer = (services: ServiceRepository) => (state = initialStat
             return { ...state, apiToken: token };
         },
         SAVE_USER: (user: UserResource) => {
-            return { ...state, user};
+            return { ...state, user };
         },
         CONFIG: ({ config }) => {
             return {
                 ...state,
                 localCluster: config.uuidPrefix,
                 remoteHosts: { ...config.remoteHosts, [config.uuidPrefix]: new URL(config.rootUrl).host },
-                homeCluster: config.uuidPrefix
+                homeCluster: config.loginCluster || config.uuidPrefix,
+                loginCluster: config.loginCluster,
+                remoteHostsConfig: { ...state.remoteHostsConfig, [config.uuidPrefix]: config }
             };
         },
         REMOTE_CLUSTER_CONFIG: ({ config }) => {
index cdc99660b7a3be268c66ee79351fa99ed75208e3..88d2a4ec2911ed10ab7876de7b823acb2964bcd0 100644 (file)
@@ -19,22 +19,27 @@ import { WORKBENCH_LOADING_SCREEN } from '~/store/workbench/workbench-actions';
 
 export const linkAccountPanelActions = unionize({
     LINK_INIT: ofType<{
-        targetUser: UserResource | undefined }>(),
+        targetUser: UserResource | undefined
+    }>(),
     LINK_LOAD: ofType<{
         originatingUser: OriginatingUser | undefined,
         targetUser: UserResource | undefined,
         targetUserToken: string | undefined,
         userToLink: UserResource | undefined,
-        userToLinkToken: string | undefined }>(),
+        userToLinkToken: string | undefined
+    }>(),
     LINK_INVALID: ofType<{
         originatingUser: OriginatingUser | undefined,
         targetUser: UserResource | undefined,
         userToLink: UserResource | undefined,
-        error: LinkAccountPanelError }>(),
+        error: LinkAccountPanelError
+    }>(),
     SET_SELECTED_CLUSTER: ofType<{
-        selectedCluster: string }>(),
+        selectedCluster: string
+    }>(),
     SET_IS_PROCESSING: ofType<{
-        isProcessing: boolean}>(),
+        isProcessing: boolean
+    }>(),
     HAS_SESSION_DATA: {}
 });
 
@@ -119,7 +124,7 @@ export const loadLinkAccountPanel = () =>
             dispatch(checkForLinkStatus());
 
             // Continue loading the link account panel
-            dispatch(setBreadcrumbs([{ label: 'Link account'}]));
+            dispatch(setBreadcrumbs([{ label: 'Link account' }]));
             const curUser = getState().auth.user;
             const curToken = getState().auth.apiToken;
             if (curUser && curToken) {
@@ -170,7 +175,8 @@ export const loadLinkAccountPanel = () =>
                             originatingUser: params.originatingUser,
                             targetUser: params.targetUser,
                             userToLink: params.userToLink,
-                            error}));
+                            error
+                        }));
                         return;
                     }
                 }
@@ -192,18 +198,18 @@ 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;
+        const accountToLink = { type: t, userUuid: services.authService.getUuid(), token: services.authService.getApiToken() } as AccountToLink;
         services.linkAccountService.saveAccountToLink(accountToLink);
 
         const auth = getState().auth;
-        const isLocalUser = auth.user!.uuid.substring(0,5) === auth.localCluster;
+        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));
+        dispatch(login(auth.localCluster, homeCluster, auth.loginCluster, auth.remoteHosts));
     };
 
 export const getAccountLinkData = () =>
@@ -265,7 +271,7 @@ export const linkAccount = () =>
                 services.linkAccountService.saveLinkOpStatus(LinkAccountStatus.SUCCESS);
                 location.reload();
             }
-            catch(e) {
+            catch (e) {
                 // If the link operation fails, delete the previously made project
                 try {
                     setAuthorizationHeader(services, linkState.targetUserToken);
@@ -277,4 +283,4 @@ export const linkAccount = () =>
                 throw e;
             }
         }
-    };
\ No newline at end of file
+    };
index cc311248cb1d9b0d3f208831604faf75fddd774c..32e14c5a08cd04e202251ab33681be9bdbdae4b0 100644 (file)
@@ -235,17 +235,19 @@ const clusterColors = [
 
 export const ResourceCluster = (props: { uuid: string }) => {
     const CLUSTER_ID_LENGTH = 5;
-    const pos = props.uuid.indexOf('-');
+    const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
     const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substr(0, pos) : '';
-    const ci = pos >= CLUSTER_ID_LENGTH ? (props.uuid.charCodeAt(0) + props.uuid.charCodeAt(1)) % clusterColors.length : 0;
-    return <Typography>
-        <span style={{
-            backgroundColor: clusterColors[ci][0],
-            color: clusterColors[ci][1],
-            padding: "2px 7px",
-            borderRadius: 3
-        }}>{clusterId}</span>
-    </Typography>;
+    const ci = pos >= CLUSTER_ID_LENGTH ? (((((
+        (props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1))
+        + props.uuid.charCodeAt(2))
+        * props.uuid.charCodeAt(3))
+        + props.uuid.charCodeAt(4))) % clusterColors.length) : 0;
+    return <span style={{
+        backgroundColor: clusterColors[ci][0],
+        color: clusterColors[ci][1],
+        padding: "2px 7px",
+        borderRadius: 3
+    }}>{clusterId}</span>;
 };
 
 export const ComputeNodeInfo = withResourceData('info', renderNodeInfo);
index 15329a43e9119850a8fce69b759319c7f5f69015..be280675c44dda97f5a612ccbe775bc38d2b5f66 100644 (file)
@@ -11,6 +11,6 @@ export const AnonymousMenu = connect()(
     ({ dispatch }: DispatchProp<any>) =>
         <Button
             color="inherit"
-            onClick={() => dispatch(login("", "", {}))}>
+            onClick={() => dispatch(login("", "", "", {}))}>
             Sign in
         </Button>);
index 0eb494e67c0b721de9b15f574f5e57ab353c9d2d..98d19acedf24bb9c303151d3436ba00053b500dc 100644 (file)
@@ -19,6 +19,7 @@ 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";
+import { Config } from '~/common/config';
 
 type CssRules = 'root';
 
@@ -32,10 +33,11 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 export interface LinkAccountPanelRootDataProps {
     targetUser?: UserResource;
     userToLink?: UserResource;
-    remoteHosts:  { [key: string]: string };
+    remoteHostsConfig: { [key: string]: Config };
     hasRemoteHosts: boolean;
     localCluster: string;
-    status : LinkAccountPanelStatus;
+    loginCluster: string;
+    status: LinkAccountPanelStatus;
     error: LinkAccountPanelError;
     selectedCluster?: string;
     isProcessing: boolean;
@@ -52,7 +54,7 @@ function displayUser(user: UserResource, showCreatedAt: boolean = false, showClu
     const disp = [];
     disp.push(<span><b>{user.email}</b> ({user.username}, {user.uuid})</span>);
     if (showCluster) {
-        const homeCluster = user.uuid.substr(0,5);
+        const homeCluster = user.uuid.substr(0, 5);
         disp.push(<span> hosted on cluster <b>{homeCluster}</b> and </span>);
     }
     if (showCreatedAt) {
@@ -67,116 +69,128 @@ function isLocalUser(uuid: string, localCluster: string) {
 
 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
 
-export const LinkAccountPanelRoot = withStyles(styles) (
-    ({classes, targetUser, userToLink, status, isProcessing, error, startLinking, cancelLinking, linkAccount,
-      remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster}: LinkAccountPanelRootProps) => {
+export const LinkAccountPanelRoot = withStyles(styles)(
+    ({ classes, targetUser, userToLink, status, isProcessing, error, startLinking, cancelLinking, linkAccount,
+        remoteHostsConfig, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster, loginCluster }: 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> }
-            { !isProcessing && 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 >
+                {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 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>}
+                {!isProcessing && 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>
-                    { 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 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>
-                            Please select the cluster that hosts the account you want to link with:
-                                <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
+                        {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)}
+                                    {Object.keys(remoteHostsConfig).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&nbsp;{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 item>
+                                <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT)}>
+                                    Link with an account on&nbsp;{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>
+                                {targetUser.isActive ?
+                                    (loginCluster === "" ?
+                                        <> <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 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)}>
+                                                Link an account from {localCluster} to this account
+                                           </Button>
+                                        </Grid> </>
+                                   : <Grid item>Please visit cluster
+                                      <a href={remoteHostsConfig[loginCluster].workbench2Url + "/link_account"}>{loginCluster}</a>
+                                       to perform account linking.</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>}
+                {!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>
+                                Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
+                   </Grid>
+                            {(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 item>
+                                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)}.
+               </Grid>}
+                        {error === LinkAccountPanelError.SAME_USER && <Grid item>
+                            Cannot link {displayUser(targetUser)} to the same account.
+               </Grid>}
+                        {error === LinkAccountPanelError.INACTIVE && <Grid item>
+                            Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
+               </Grid>}
+                        <Grid container item direction="row" spacing={24}>
+                            <Grid item>
+                                <Button variant="contained" onClick={() => cancelLinking()}>
+                                    Cancel
+                       </Button>
+                            </Grid>
+                            <Grid item>
+                                <Button disabled={status === LinkAccountPanelStatus.ERROR} color="primary" variant="contained" onClick={() => linkAccount()}>
+                                    Link accounts
+                       </Button>
+                            </Grid>
                         </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 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)}>
-                                Link an account from {localCluster} to this account
-                            </Button>
-                        </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> }
-            { !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>
-                        Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
-                    </Grid>
-                    { (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 item>
-                        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)}.
-                </Grid> }
-                { error === LinkAccountPanelError.SAME_USER && <Grid item>
-                    Cannot link {displayUser(targetUser)} to the same account.
-                </Grid> }
-                { error === LinkAccountPanelError.INACTIVE && <Grid item>
-                    Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
-                </Grid> }
-                <Grid container item direction="row" spacing={24}>
-                    <Grid item>
-                        <Button variant="contained" onClick={() => cancelLinking()}>
-                            Cancel
-                        </Button>
-                    </Grid>
-                    <Grid item>
-                        <Button disabled={status === LinkAccountPanelStatus.ERROR} color="primary" variant="contained" onClick={() => linkAccount()}>
-                            Link accounts
-                        </Button>
-                    </Grid>
-                </Grid>
-            </Grid> }
-        </CardContent>
-    </Card>;
-});
\ No newline at end of file
+                    </Grid>}
+            </CardContent>
+        </Card>;
+    });
index c3ad51cf61bfa94251f2641a6331afcc09a48e5d..60166cda853d7b196b902ad62c34a88fe2f8d7c7 100644 (file)
@@ -15,10 +15,11 @@ import {
 
 const mapStateToProps = (state: RootState): LinkAccountPanelRootDataProps => {
     return {
-        remoteHosts: state.auth.remoteHosts,
-        hasRemoteHosts: Object.keys(state.auth.remoteHosts).length > 1,
+        remoteHostsConfig: state.auth.remoteHostsConfig,
+        hasRemoteHosts: Object.keys(state.auth.remoteHosts).length > 1 && state.auth.loginCluster === "",
         selectedCluster: state.linkAccountPanel.selectedCluster,
         localCluster: state.auth.localCluster,
+        loginCluster: state.auth.loginCluster,
         targetUser: state.linkAccountPanel.targetUser,
         userToLink: state.linkAccountPanel.userToLink,
         status: state.linkAccountPanel.status,
@@ -31,7 +32,7 @@ const mapDispatchToProps = (dispatch: Dispatch): LinkAccountPanelRootActionProps
     startLinking: (type: LinkAccountType) => dispatch<any>(startLinking(type)),
     cancelLinking: () => dispatch<any>(cancelLinking(true)),
     linkAccount: () => dispatch<any>(linkAccount()),
-    setSelectedCluster: (selectedCluster: string) => dispatch<any>(linkAccountPanelActions.SET_SELECTED_CLUSTER({selectedCluster}))
+    setSelectedCluster: (selectedCluster: string) => dispatch<any>(linkAccountPanelActions.SET_SELECTED_CLUSTER({ selectedCluster }))
 });
 
 export const LinkAccountPanel = connect(mapStateToProps, mapDispatchToProps)(LinkAccountPanelRoot);
index afc35f7156b24022cbc8aa4e1f34d10f61b09799..293026c3d6733f19ad86f84b00006d8c143b03f9 100644 (file)
@@ -51,6 +51,7 @@ type LoginPanelProps = DispatchProp<any> & WithStyles<CssRules> & {
     remoteHosts: { [key: string]: string },
     homeCluster: string,
     uuidPrefix: string,
+    loginCluster: string,
     welcomePage: string
 };
 
@@ -59,8 +60,9 @@ export const LoginPanel = withStyles(styles)(
         remoteHosts: state.auth.remoteHosts,
         homeCluster: state.auth.homeCluster,
         uuidPrefix: state.auth.localCluster,
+        loginCluster: state.auth.loginCluster,
         welcomePage: state.config.clusterConfig.Workbench.WelcomePageHTML
-    }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix, welcomePage }: LoginPanelProps) =>
+    }))(({ classes, dispatch, remoteHosts, homeCluster, uuidPrefix, loginCluster, welcomePage }: LoginPanelProps) =>
         <Grid container justify="center" alignItems="center"
             className={classes.root}
             style={{ marginTop: 56, overflowY: "auto", height: "100%" }}>
@@ -68,7 +70,8 @@ export const LoginPanel = withStyles(styles)(
                 <Typography>
                     <div dangerouslySetInnerHTML={{ __html: welcomePage }} style={{ margin: "1em" }} />
                 </Typography>
-                {Object.keys(remoteHosts).length > 1 &&
+                {Object.keys(remoteHosts).length > 1 && loginCluster === "" &&
+
                     <Typography component="div" align="right">
                         <label>Please select the cluster that hosts your user account:</label>
                         <Select native value={homeCluster} style={{ margin: "1em" }}
@@ -79,10 +82,10 @@ export const LoginPanel = withStyles(styles)(
 
                 <Typography component="div" align="right">
                     <Button variant="contained" color="primary" style={{ margin: "1em" }} className={classes.button}
-                        onClick={() => dispatch(login(uuidPrefix, homeCluster, remoteHosts))}>
-                        Log in to {uuidPrefix}
-                        {uuidPrefix !== homeCluster &&
-                            <span>&nbsp;with user from {homeCluster}</span>}
+                        onClick={() => dispatch(login(uuidPrefix, homeCluster, loginCluster, remoteHosts))}>
+                        Log in
+                       {uuidPrefix !== homeCluster && loginCluster !== homeCluster &&
+                            <span>&nbsp;to {uuidPrefix} with user from {homeCluster}</span>}
                     </Button>
                 </Typography>
             </Grid>
index b82b174520ced7cf922d4fec423f3c0c0372051d..6eac09fa0b08bd1d8191dafaa5ef9ec35c65b651 100644 (file)
@@ -21,6 +21,10 @@ import {
 import { createTree } from '~/models/tree';
 import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
 import { SearchResultsPanelProps } from "./search-results-panel";
+import { Routes } from '~/routes/routes';
+import { Link } from 'react-router-dom';
+import { StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
+import { ArvadosTheme } from '~/common/custom-theme';
 
 export enum SearchResultsPanelColumnNames {
     CLUSTER = "Cluster",
@@ -33,6 +37,15 @@ export enum SearchResultsPanelColumnNames {
     LAST_MODIFIED = "Last modified"
 }
 
+export type CssRules = 'siteManagerLink';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    siteManagerLink: {
+        marginRight: theme.spacing.unit * 2,
+        float: 'right'
+    }
+});
+
 export interface WorkflowPanelFilter extends DataTableFilterItem {
     type: ResourceKind | ContainerRequestState;
 }
@@ -98,19 +111,27 @@ export const searchResultsPanelColumns: DataColumns<string> = [
     }
 ];
 
-export const SearchResultsPanelView = (props: SearchResultsPanelProps) => {
-    const homeCluster = props.user.uuid.substr(0, 5);
-    return <DataExplorer
-        id={SEARCH_RESULTS_PANEL_ID}
-        onRowClick={props.onItemClick}
-        onRowDoubleClick={props.onItemDoubleClick}
-        onContextMenu={props.onContextMenu}
-        contextMenuColumn={false}
-        hideSearchInput
-        title={
-            props.localCluster === homeCluster ?
-                <div>Searching clusters: {props.sessions.filter((ss) => ss.loggedIn).map((ss) => <span key={ss.clusterId}> {ss.clusterId}</span>)}</div> :
-                <div>Searching local cluster {props.localCluster} only.  To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></div>
-        }
-    />;
-};
+export const SearchResultsPanelView = withStyles(styles, { withTheme: true })(
+    (props: SearchResultsPanelProps & WithStyles<CssRules, true>) => {
+        const homeCluster = props.user.uuid.substr(0, 5);
+        const loggedIn = props.sessions.filter((ss) => ss.loggedIn);
+        return <DataExplorer
+            id={SEARCH_RESULTS_PANEL_ID}
+            onRowClick={props.onItemClick}
+            onRowDoubleClick={props.onItemDoubleClick}
+            onContextMenu={props.onContextMenu}
+            contextMenuColumn={false}
+            hideSearchInput
+            title={
+                <div>
+                    {loggedIn.length === 1 ?
+                        <span>Searching local cluster <ResourceCluster uuid={props.localCluster} /></span>
+                        : <span>Searching clusters: {loggedIn.map((ss) => <span key={ss.clusterId}>
+                            <a href={props.remoteHostsConfig[ss.clusterId].workbench2Url} style={{ textDecoration: 'none' }}> <ResourceCluster uuid={ss.clusterId} /></a></span>)}</span>}
+                    {loggedIn.length === 1 && props.localCluster !== homeCluster ?
+                        <span>To search multiple clusters, <a href={props.remoteHostsConfig[homeCluster] && props.remoteHostsConfig[homeCluster].workbench2Url}> start from your home Workbench.</a></span>
+                        : <span style={{ marginLeft: "2em" }}>Use <Link to={Routes.SITE_MANAGER} >Site Manager</Link> to manage which clusters will be searched.</span>}
+                </div >
+            }
+        />;
+    });
index 684e35b4d31680a630c846d506cb05fb6d38241f..e75aa1f95578a091ef24f975ae4bb360c5961c5e 100644 (file)
@@ -26,6 +26,8 @@ import { Field, FormErrors, InjectedFormProps, reduxForm, reset, stopSubmit } fr
 import { TextField } from "~/components/text-field/text-field";
 import { addSession } from "~/store/auth/auth-action-session";
 import { SITE_MANAGER_REMOTE_HOST_VALIDATION } from "~/validators/validators";
+import { Config } from '~/common/config';
+import { ResourceCluster } from '~/views-components/data-explorer/renderers';
 
 type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' |
     'remoteSiteInfo' | 'buttonAdd' | 'buttonLoggedIn' | 'buttonLoggedOut' |
@@ -33,8 +35,8 @@ type CssRules = 'root' | 'link' | 'buttonContainer' | 'table' | 'tableRow' |
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
-       width: '100%',
-       overflow: 'auto'
+        width: '100%',
+        overflow: 'auto'
     },
     link: {
         color: theme.palette.primary.main,
@@ -88,6 +90,7 @@ export interface SiteManagerPanelRootActionProps {
 
 export interface SiteManagerPanelRootDataProps {
     sessions: Session[];
+    remoteHostsConfig: { [key: string]: Config };
 }
 
 type SiteManagerPanelRootProps = SiteManagerPanelRootDataProps & SiteManagerPanelRootActionProps & WithStyles<CssRules> & InjectedFormProps;
@@ -106,7 +109,7 @@ const submitSession = (remoteHost: string) =>
     };
 
 export const SiteManagerPanelRoot = compose(
-    reduxForm<{remoteHost: string}>({
+    reduxForm<{ remoteHost: string }>({
         form: SITE_MANAGER_FORM_NAME,
         touchOnBlur: false,
         onSubmit: (data, dispatch) => {
@@ -114,14 +117,14 @@ export const SiteManagerPanelRoot = compose(
         }
     }),
     withStyles(styles))
-    (({ classes, sessions, handleSubmit, toggleSession }: SiteManagerPanelRootProps) =>
+    (({ classes, sessions, handleSubmit, toggleSession, remoteHostsConfig }: SiteManagerPanelRootProps) =>
         <Card className={classes.root}>
             <CardContent>
                 <Grid container direction="row">
                     <Grid item xs={12}>
-                        <Typography  paragraph={true} >
+                        <Typography paragraph={true} >
                             You can log in to multiple Arvados sites here, then use the multi-site search page to search collections and projects on all sites at once.
-                        </Typography>
+                   </Typography>
                     </Grid>
                 </Grid>
                 <Grid item xs={12}>
@@ -129,6 +132,7 @@ export const SiteManagerPanelRoot = compose(
                         <TableHead>
                             <TableRow className={classes.tableRow}>
                                 <TableCell>Cluster ID</TableCell>
+                                <TableCell>Host</TableCell>
                                 <TableCell>Username</TableCell>
                                 <TableCell>Email</TableCell>
                                 <TableCell>Status</TableCell>
@@ -138,9 +142,12 @@ export const SiteManagerPanelRoot = compose(
                             {sessions.map((session, index) => {
                                 const validating = session.status === SessionStatus.BEING_VALIDATED;
                                 return <TableRow key={index} className={classes.tableRow}>
-                                    <TableCell>{session.clusterId}</TableCell>
-                                    <TableCell>{validating ? <CircularProgress size={20}/> : session.username}</TableCell>
-                                    <TableCell>{validating ? <CircularProgress size={20}/> : session.email}</TableCell>
+                                    <TableCell>{remoteHostsConfig[session.clusterId] ?
+                                        <a href={remoteHostsConfig[session.clusterId].workbench2Url} style={{ textDecoration: 'none' }}> <ResourceCluster uuid={session.clusterId} /></a>
+                                        : session.clusterId}</TableCell>
+                                    <TableCell>{session.remoteHost}</TableCell>
+                                    <TableCell>{validating ? <CircularProgress size={20} /> : session.username}</TableCell>
+                                    <TableCell>{validating ? <CircularProgress size={20} /> : session.email}</TableCell>
                                     <TableCell className={classes.statusCell}>
                                         <Button fullWidth
                                             disabled={validating || session.status === SessionStatus.INVALIDATED || session.active}
@@ -157,9 +164,9 @@ export const SiteManagerPanelRoot = compose(
                 <form onSubmit={handleSubmit}>
                     <Grid container direction="row">
                         <Grid item xs={12}>
-                            <Typography  paragraph={true} className={classes.remoteSiteInfo}>
+                            <Typography paragraph={true} className={classes.remoteSiteInfo}>
                                 To add a remote Arvados site, paste the remote site's host here (see "ARVADOS_API_HOST" on the "current token" page).
-                            </Typography>
+                        </Typography>
                         </Grid>
                         <Grid item xs={8}>
                             <Field
@@ -169,7 +176,7 @@ export const SiteManagerPanelRoot = compose(
                                 placeholder="zzzz.arvadosapi.com"
                                 margin="normal"
                                 label="New cluster"
-                                autoFocus/>
+                                autoFocus />
                         </Grid>
                         <Grid item xs={3}>
                             <Button type="submit" variant="contained" color="primary"
index 4532e856deffa2d7075f369eb15a5a1d6a849fe2..0f48565d40ddd2d145cedc1351bf5214632d016e 100644 (file)
@@ -14,7 +14,8 @@ import { toggleSession } from "~/store/auth/auth-action-session";
 
 const mapStateToProps = (state: RootState): SiteManagerPanelRootDataProps => {
     return {
-        sessions: state.auth.sessions
+        sessions: state.auth.sessions,
+        remoteHostsConfig: state.auth.remoteHostsConfig
     };
 };
 
index 829fe17a4de48b3f535036d6c34f3c1fc5ff2497..be543a64a5bee708cdecfd0bb65f4853008c848a 100644 (file)
@@ -41,7 +41,9 @@ export const FedLogin = connect(mapStateToProps)(
                             console.log(`Cluster ${k} does not define workbench2Url.  Federated login / cross-site linking to ${k} is unavailable.  Tell the admin of ${k} to set Services->Workbench2->ExternalURL in config.yml.`);
                             return;
                         }
-                        return <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${getSaltedToken(k, tokenUuid, token)}`} style={{
+                        const fedtoken = (remoteHostsConfig[k].loginCluster === localCluster)
+                            ? apiToken : getSaltedToken(k, tokenUuid, token);
+                        return <iframe key={k} src={`${remoteHostsConfig[k].workbench2Url}/fedtoken?api_token=${fedtoken}`} style={{
                             height: 0,
                             width: 0,
                             visibility: "hidden"