15088: Improves federated linking logic and UI
[arvados.git] / src / views / link-account-panel / link-account-panel-root.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import * as React from 'react';
6 import {
7     StyleRulesCallback,
8     WithStyles,
9     withStyles,
10     Card,
11     CardContent,
12     Button,
13     Grid,
14     Select
15 } from '@material-ui/core';
16 import { ArvadosTheme } from '~/common/custom-theme';
17 import { UserResource } from "~/models/user";
18 import { LinkAccountType } from "~/models/link-account";
19 import { formatDate } from "~/common/formatters";
20 import { LinkAccountPanelStatus, LinkAccountPanelError } from "~/store/link-account-panel/link-account-panel-reducer";
21
22 type CssRules = 'root';// | 'gridItem' | 'label' | 'title' | 'actions';
23
24 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
25     root: {
26         width: '100%',
27         overflow: 'auto'
28     }
29 });
30
31 export interface LinkAccountPanelRootDataProps {
32     targetUser?: UserResource;
33     userToLink?: UserResource;
34     remoteHosts:  { [key: string]: string };
35     hasRemoteHosts: boolean;
36     localCluster: string;
37     status : LinkAccountPanelStatus;
38     error: LinkAccountPanelError;
39     selectedCluster?: string;
40 }
41
42 export interface LinkAccountPanelRootActionProps {
43     startLinking: (type: LinkAccountType) => void;
44     cancelLinking: () => void;
45     linkAccount: () => void;
46     setSelectedCluster: (cluster: string) => void;
47 }
48
49 function displayUser(user: UserResource, showCreatedAt: boolean = false, showCluster: boolean = false) {
50     const disp = [];
51     disp.push(<span><b>{user.email}</b> ({user.username}, {user.uuid})</span>);
52     if (showCluster) {
53         const homeCluster = user.uuid.substr(0,5);
54         disp.push(<span> hosted on cluster <b>{homeCluster}</b> and </span>);
55     }
56     if (showCreatedAt) {
57         disp.push(<span> created on <b>{formatDate(user.createdAt, true)}</b></span>);
58     }
59     return disp;
60 }
61
62 function isLocalUser(uuid: string, localCluster: string) {
63     return uuid.substring(0, 5) === localCluster;
64 }
65
66 type LinkAccountPanelRootProps = LinkAccountPanelRootDataProps & LinkAccountPanelRootActionProps & WithStyles<CssRules>;
67
68 export const LinkAccountPanelRoot = withStyles(styles) (
69     ({classes, targetUser, userToLink, status, error, startLinking, cancelLinking, linkAccount,
70       remoteHosts, hasRemoteHosts, selectedCluster, setSelectedCluster, localCluster}: LinkAccountPanelRootProps) => {
71         return <Card className={classes.root}>
72             <CardContent>
73             { status === LinkAccountPanelStatus.INITIAL && targetUser && <div>
74                 { isLocalUser(targetUser.uuid, localCluster) ? <Grid container spacing={24}>
75                     <Grid container item direction="column" spacing={24}>
76                         <Grid item>
77                             You are currently logged in as {displayUser(targetUser, true)}
78                         </Grid>
79                         <Grid item>
80                             You can link Arvados accounts. After linking, either login will take you to the same account.
81                         </Grid >
82                     </Grid>
83                     <Grid container item direction="row" spacing={24}>
84                         <Grid item>
85                             <Button disabled={!targetUser.isActive} color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_OTHER_LOGIN)}>
86                                 Add another login to this account
87                             </Button>
88                         </Grid>
89                         <Grid item>
90                             <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_ACCOUNT)}>
91                                 Use this login to access another account
92                             </Button>
93                         </Grid>
94                     </Grid>
95                     { hasRemoteHosts && selectedCluster && <Grid container item direction="column" spacing={24}>
96                         <Grid item>
97                             You can also link {displayUser(targetUser, false)} with an account from a remote cluster.
98                         </Grid>
99                         <Grid item>
100                             Please select the cluster that hosts the account you want to link with:
101                                 <Select id="remoteHostsDropdown" native defaultValue={selectedCluster} style={{ marginLeft: "1em" }}
102                                     onChange={(event) => setSelectedCluster(event.target.value)}>
103                                     {Object.keys(remoteHosts).map((k) => k !== localCluster ? <option key={k} value={k}>{k}</option> : null)}
104                                 </Select>
105                             </Grid>
106                         <Grid item>
107                             <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ACCESS_OTHER_REMOTE_ACCOUNT)}>
108                                 Link with an account on&nbsp;{hasRemoteHosts ? <label>{selectedCluster} </label> : null}
109                             </Button>
110                         </Grid>
111                     </Grid> }
112                 </Grid> :
113                 <Grid container spacing={24}>
114                     <Grid container item direction="column" spacing={24}>
115                         <Grid item>
116                             You are currently logged in as {displayUser(targetUser, true, true)}
117                         </Grid>
118                         <Grid item>
119                             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.
120                         </Grid >
121                         <Grid item>
122                             <Button color="primary" variant="contained" onClick={() => startLinking(LinkAccountType.ADD_LOCAL_TO_REMOTE)}>
123                                 Link an account from {localCluster} to this account
124                             </Button>
125                         </Grid>
126                     </Grid>
127                 </Grid>}
128             </div> }
129             { (status === LinkAccountPanelStatus.LINKING || status === LinkAccountPanelStatus.ERROR) && userToLink && targetUser &&
130             <Grid container spacing={24}>
131                 { status === LinkAccountPanelStatus.LINKING && <Grid container item direction="column" spacing={24}>
132                     <Grid item>
133                         Clicking 'Link accounts' will link {displayUser(userToLink, true, !isLocalUser(targetUser.uuid, localCluster))} to {displayUser(targetUser, true, !isLocalUser(targetUser.uuid, localCluster))}.
134                     </Grid>
135                     { (isLocalUser(targetUser.uuid, localCluster)) && <Grid item>
136                         After linking, logging in as {displayUser(userToLink)} will log you into the same account as {displayUser(targetUser)}.
137                     </Grid> }
138                     <Grid item>
139                         Any object owned by {displayUser(userToLink)} will be transfered to {displayUser(targetUser)}.
140                     </Grid>
141                     { !isLocalUser(targetUser.uuid, localCluster) && <Grid item>
142                         You can access <b>{userToLink.email}</b> data by logging into <b>{localCluster}</b> with the <b>{targetUser.email}</b> account.
143                     </Grid> }
144                 </Grid> }
145                 { error === LinkAccountPanelError.NON_ADMIN && <Grid item>
146                     Cannot link admin account {displayUser(userToLink)} to non-admin account {displayUser(targetUser)}.
147                 </Grid> }
148                 { error === LinkAccountPanelError.SAME_USER && <Grid item>
149                     Cannot link {displayUser(targetUser)} to the same account.
150                 </Grid> }
151                 { error === LinkAccountPanelError.INACTIVE && <Grid item>
152                     Cannot link account {displayUser(userToLink)} to inactive account {displayUser(targetUser)}.
153                 </Grid> }
154                 <Grid container item direction="row" spacing={24}>
155                     <Grid item>
156                         <Button variant="contained" onClick={() => cancelLinking()}>
157                             Cancel
158                         </Button>
159                     </Grid>
160                     <Grid item>
161                         <Button disabled={status === LinkAccountPanelStatus.ERROR} color="primary" variant="contained" onClick={() => linkAccount()}>
162                             Link accounts
163                         </Button>
164                     </Grid>
165                 </Grid>
166             </Grid> }
167             </CardContent>
168         </Card> ;
169 });