18559: Add uuid with copy and action menu to user profile panel
[arvados-workbench2.git] / src / views / user-profile-panel / user-profile-panel-root.tsx
1 // Copyright (C) The Arvados Authors. All rights reserved.
2 //
3 // SPDX-License-Identifier: AGPL-3.0
4
5 import React from 'react';
6 import { Field, InjectedFormProps } from "redux-form";
7 import { DispatchProp } from 'react-redux';
8 import { UserResource } from 'models/user';
9 import { TextField } from "components/text-field/text-field";
10 import { DataExplorer } from "views-components/data-explorer/data-explorer";
11 import { NativeSelectField } from "components/select-field/select-field";
12 import {
13     StyleRulesCallback,
14     WithStyles,
15     withStyles,
16     Card,
17     CardContent,
18     Button,
19     Typography,
20     Grid,
21     InputLabel,
22     Tabs, Tab,
23     Paper,
24     Tooltip,
25     IconButton,
26 } from '@material-ui/core';
27 import { ArvadosTheme } from 'common/custom-theme';
28 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
29 import { PROFILE_EMAIL_VALIDATION } from "validators/validators";
30 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
31 import { noop } from 'lodash';
32 import { CopyIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
33 import { DataColumns } from 'components/data-table/data-table';
34 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
35 import { createTree } from 'models/tree';
36 import { getResource, ResourcesState } from 'store/resources/resources';
37 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
38 import CopyToClipboard from 'react-copy-to-clipboard';
39
40 type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
41
42 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
43     root: {
44         width: '100%',
45         overflow: 'auto'
46     },
47     adminRoot: {
48         // ...theme.mixins.gutters()
49     },
50     gridItem: {
51         height: 45,
52         marginBottom: 20
53     },
54     label: {
55         fontSize: '0.675rem',
56         color: theme.palette.grey['600']
57     },
58     readOnlyValue: {
59         fontSize: '0.875rem',
60     },
61     title: {
62         fontSize: '1.1rem',
63     },
64     description: {
65         color: theme.palette.grey["600"]
66     },
67     actions: {
68         display: 'flex',
69         justifyContent: 'flex-end'
70     },
71     content: {
72         // reserve space for the tab bar
73         height: `calc(100% - ${theme.spacing.unit * 7}px)`,
74     },
75     copyIcon: {
76         marginLeft: theme.spacing.unit,
77         color: theme.palette.grey["500"],
78         cursor: 'pointer',
79         display: 'inline',
80         '& svg': {
81             fontSize: '1rem'
82         }
83     }
84 });
85
86 export interface UserProfilePanelRootActionProps {
87     openSetupDialog: (uuid: string) => void;
88     loginAs: (uuid: string) => void;
89     openDeactivateDialog: (uuid: string) => void;
90     handleContextMenu: (event, resource: UserResource) => void;
91 }
92
93 export interface UserProfilePanelRootDataProps {
94     isAdmin: boolean;
95     isSelf: boolean;
96     isPristine: boolean;
97     isValid: boolean;
98     userUuid: string;
99     resources: ResourcesState
100     localCluster: string;
101 }
102
103 const RoleTypes = [
104     { key: '', value: '' },
105     { key: 'Bio-informatician', value: 'Bio-informatician' },
106     { key: 'Data Scientist', value: 'Data Scientist' },
107     { key: 'Analyst', value: 'Analyst' },
108     { key: 'Researcher', value: 'Researcher' },
109     { key: 'Software Developer', value: 'Software Developer' },
110     { key: 'System Administrator', value: 'System Administrator' },
111     { key: 'Other', value: 'Other' }
112 ];
113
114 type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & DispatchProp & WithStyles<CssRules>;
115
116 export enum UserProfileGroupsColumnNames {
117     NAME = "Name",
118     PERMISSION = "Permission",
119     VISIBLE = "Visible to other members",
120     UUID = "UUID",
121     REMOVE = "Remove",
122 }
123
124 enum TABS {
125     PROFILE = "PROFILE",
126     GROUPS = "GROUPS",
127     ADMIN = "ADMIN",
128
129 }
130
131 export const userProfileGroupsColumns: DataColumns<string> = [
132     {
133         name: UserProfileGroupsColumnNames.NAME,
134         selected: true,
135         configurable: true,
136         filters: createTree(),
137         render: uuid => <ResourceLinkHead uuid={uuid} />
138     },
139     {
140         name: UserProfileGroupsColumnNames.PERMISSION,
141         selected: true,
142         configurable: true,
143         filters: createTree(),
144         render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
145     },
146     {
147         name: UserProfileGroupsColumnNames.VISIBLE,
148         selected: true,
149         configurable: true,
150         filters: createTree(),
151         render: uuid => <ResourceLinkTailIsVisible uuid={uuid} />
152     },
153     {
154         name: UserProfileGroupsColumnNames.UUID,
155         selected: true,
156         configurable: true,
157         filters: createTree(),
158         render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
159     },
160     {
161         name: UserProfileGroupsColumnNames.REMOVE,
162         selected: true,
163         configurable: true,
164         filters: createTree(),
165         render: uuid => <ResourceLinkDelete uuid={uuid} />
166     },
167 ];
168
169 const ReadOnlyField = withStyles(styles)(
170     (props: ({ label: string, input: {value: string} }) & WithStyles<CssRules> ) => (
171         <Grid item xs={12} data-cy="field">
172             <Typography className={props.classes.label}>
173                 {props.label}
174             </Typography>
175             <Typography className={props.classes.readOnlyValue} data-cy="value">
176                 {props.input.value}
177             </Typography>
178         </Grid>
179     )
180 );
181
182 export const UserProfilePanelRoot = withStyles(styles)(
183     class extends React.Component<UserProfilePanelRootProps> {
184         state = {
185             value: TABS.PROFILE,
186         };
187
188         componentDidMount() {
189             this.setState({ value: TABS.PROFILE});
190         }
191
192         onCopy = (message: string) => {
193             this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
194                 message,
195                 hideDuration: 2000,
196                 kind: SnackbarKind.SUCCESS
197             }));
198         }
199
200         render() {
201             return <Paper className={this.props.classes.root}>
202                 <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
203                     <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
204                     <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
205                     {this.props.isAdmin && <Tab label={TABS.ADMIN} value={TABS.ADMIN} />}
206                 </Tabs>
207                 {this.state.value === TABS.PROFILE &&
208                     <CardContent>
209                         <Grid container justify="space-between">
210                             <Grid item xs={11}>
211                                 <Typography className={this.props.classes.title}>
212                                     {this.props.userUuid}
213                                     <Tooltip title="Copy to clipboard">
214                                         <span className={this.props.classes.copyIcon}>
215                                             <CopyToClipboard text={this.props.userUuid || ""} onCopy={() => this.onCopy!("Copied")}>
216                                                 <CopyIcon />
217                                             </CopyToClipboard>
218                                         </span>
219                                     </Tooltip>
220                                 </Typography>
221                             </Grid>
222                             <Grid item xs={1} style={{ textAlign: "right" }}>
223                                 <Tooltip title="Actions" disableFocusListener>
224                                     <IconButton
225                                         data-cy='collection-panel-options-btn'
226                                         aria-label="Actions"
227                                         onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
228                                         <MoreOptionsIcon />
229                                     </IconButton>
230                                 </Tooltip>
231                             </Grid>
232                         </Grid>
233                         <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
234                             <Grid container spacing={24}>
235                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
236                                     <Field
237                                         label="First name"
238                                         name="firstName"
239                                         component={ReadOnlyField as any}
240                                         disabled
241                                     />
242                                 </Grid>
243                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
244                                     <Field
245                                         label="Last name"
246                                         name="lastName"
247                                         component={ReadOnlyField as any}
248                                         disabled
249                                     />
250                                 </Grid>
251                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
252                                     <Field
253                                         label="E-mail"
254                                         name="email"
255                                         component={ReadOnlyField as any}
256                                         disabled
257                                     />
258                                 </Grid>
259                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
260                                     <Field
261                                         label="Username"
262                                         name="username"
263                                         component={ReadOnlyField as any}
264                                         disabled
265                                     />
266                                 </Grid>
267                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
268                                     <Field
269                                         label="Organization"
270                                         name="prefs.profile.organization"
271                                         component={TextField as any}
272                                         disabled={!this.props.isAdmin && !this.props.isSelf}
273                                     />
274                                 </Grid>
275                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
276                                     <Field
277                                         label="E-mail at Organization"
278                                         name="prefs.profile.organization_email"
279                                         component={TextField as any}
280                                         disabled={!this.props.isAdmin && !this.props.isSelf}
281                                         validate={PROFILE_EMAIL_VALIDATION}
282                                     />
283                                 </Grid>
284                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
285                                     <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
286                                     <Field
287                                         id="prefs.profile.role"
288                                         name="prefs.profile.role"
289                                         component={NativeSelectField as any}
290                                         items={RoleTypes}
291                                         disabled={!this.props.isAdmin && !this.props.isSelf}
292                                     />
293                                 </Grid>
294                                 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
295                                     <Field
296                                         label="Website"
297                                         name="prefs.profile.website_url"
298                                         component={TextField as any}
299                                         disabled={!this.props.isAdmin && !this.props.isSelf}
300                                     />
301                                 </Grid>
302                                 <Grid item sm={12}>
303                                     <Grid container direction="row" justify="flex-end">
304                                         <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
305                                         <Button
306                                             color="primary"
307                                             variant="contained"
308                                             type="submit"
309                                             disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
310                                             Save changes
311                                         </Button>
312                                     </Grid>
313                                 </Grid>
314                             </Grid>
315                         </form >
316                     </CardContent>
317                 }
318                 {this.state.value === TABS.GROUPS &&
319                     <div className={this.props.classes.content}>
320                         <DataExplorer
321                                 id={USER_PROFILE_PANEL_ID}
322                                 data-cy="user-profile-groups-data-explorer"
323                                 onRowClick={noop}
324                                 onRowDoubleClick={noop}
325                                 onContextMenu={noop}
326                                 contextMenuColumn={false}
327                                 hideColumnSelector
328                                 hideSearchInput
329                                 paperProps={{
330                                     elevation: 0,
331                                 }}
332                                 dataTableDefaultView={
333                                     <DataTableDefaultView
334                                         icon={GroupsIcon}
335                                         messages={['Group list is empty.']} />
336                                 } />
337                     </div>}
338                 {this.props.isAdmin && this.state.value === TABS.ADMIN &&
339                     <Paper elevation={0} className={this.props.classes.adminRoot}>
340                         <Card elevation={0}>
341                             <CardContent>
342                                 <Grid container
343                                     direction="row"
344                                     justify={'flex-end'}
345                                     alignItems={'center'}>
346                                     <Grid item xs>
347                                         <Typography variant="h6" className={this.props.classes.title}>
348                                             Setup Account
349                                         </Typography>
350                                         <Typography variant="body1" className={this.props.classes.description}>
351                                             This button sets up a user. After setup, they will be able use Arvados. This dialog box also allows you to optionally set up a shell account for this user. The login name is automatically generated from the user's e-mail address.
352                                         </Typography>
353                                     </Grid>
354                                     <Grid item sm={'auto'} xs={12}>
355                                         <Button variant="contained"
356                                             color="primary"
357                                             onClick={() => {this.props.openSetupDialog(this.props.userUuid)}}
358                                             disabled={false}>
359                                             Setup Account
360                                         </Button>
361                                     </Grid>
362                                 </Grid>
363                             </CardContent>
364                         </Card>
365                         <Card elevation={0}>
366                             <CardContent>
367                                 <Grid container
368                                     direction="row"
369                                     justify={'flex-end'}
370                                     alignItems={'center'}>
371                                     <Grid item xs>
372                                         <Typography variant="h6" className={this.props.classes.title}>
373                                             Deactivate
374                                         </Typography>
375                                         <Typography variant="body1" className={this.props.classes.description}>
376                                             As an admin, you can deactivate and reset this user. This will remove all repository/VM permissions for the user. If you "setup" the user again, the user will have to sign the user agreement again. You may also want to reassign data ownership.
377                                         </Typography>
378                                     </Grid>
379                                     <Grid item sm={'auto'} xs={12}>
380                                         <Button variant="contained"
381                                             color="primary"
382                                             onClick={() => {this.props.openDeactivateDialog(this.props.userUuid)}}
383                                             disabled={false}>
384                                             Deactivate
385                                         </Button>
386                                     </Grid>
387                                 </Grid>
388                             </CardContent>
389                         </Card>
390                         <Card elevation={0}>
391                             <CardContent>
392                                 <Grid container
393                                     direction="row"
394                                     justify={'flex-end'}
395                                     alignItems={'center'}>
396                                     <Grid item xs>
397                                         <Typography variant="h6" className={this.props.classes.title}>
398                                             Log In
399                                         </Typography>
400                                         <Typography variant="body1" className={this.props.classes.description}>
401                                             As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
402                                         </Typography>
403                                     </Grid>
404                                     <Grid item sm={'auto'} xs={12}>
405                                         <Button variant="contained"
406                                             color="primary"
407                                             onClick={() => {this.props.loginAs(this.props.userUuid)}}
408                                             disabled={false}>
409                                             Log In
410                                         </Button>
411                                     </Grid>
412                                 </Grid>
413                             </CardContent>
414                         </Card>
415                     </Paper>}
416             </Paper >;
417         }
418
419         handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
420             this.setState({ value });
421         }
422
423         handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
424             event.stopPropagation();
425             const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
426             if (resource) {
427                 this.props.handleContextMenu(event, resource);
428             }
429         }
430
431     }
432 );