531d3bdbfbcd092bdeccdb3794e4d7571c8835a9
[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     CardContent,
17     Button,
18     Typography,
19     Grid,
20     InputLabel,
21     Tabs, Tab,
22     Paper,
23     Tooltip,
24     IconButton,
25 } from '@material-ui/core';
26 import { ArvadosTheme } from 'common/custom-theme';
27 import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
28 import { PROFILE_EMAIL_VALIDATION } from "validators/validators";
29 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
30 import { noop } from 'lodash';
31 import { DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
32 import { DataColumns } from 'components/data-table/data-table';
33 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
34 import { createTree } from 'models/tree';
35 import { getResource, ResourcesState } from 'store/resources/resources';
36 import { DefaultView } from 'components/default-view/default-view';
37 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
38
39 type CssRules = 'root' | 'emptyRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
40
41 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
42     root: {
43         width: '100%',
44         overflow: 'auto'
45     },
46     emptyRoot: {
47         width: '100%',
48         overflow: 'auto',
49         padding: theme.spacing.unit * 4,
50     },
51     gridItem: {
52         height: 45,
53         marginBottom: 20
54     },
55     label: {
56         fontSize: '0.675rem',
57         color: theme.palette.grey['600']
58     },
59     readOnlyValue: {
60         fontSize: '0.875rem',
61     },
62     title: {
63         fontSize: '1.1rem',
64     },
65     description: {
66         color: theme.palette.grey["600"]
67     },
68     actions: {
69         display: 'flex',
70         justifyContent: 'flex-end'
71     },
72     content: {
73         // reserve space for the tab bar
74         height: `calc(100% - ${theme.spacing.unit * 7}px)`,
75     },
76     copyIcon: {
77         marginLeft: theme.spacing.unit,
78         color: theme.palette.grey["500"],
79         cursor: 'pointer',
80         display: 'inline',
81         '& svg': {
82             fontSize: '1rem'
83         }
84     }
85 });
86
87 export interface UserProfilePanelRootActionProps {
88     handleContextMenu: (event, resource: UserResource) => void;
89 }
90
91 export interface UserProfilePanelRootDataProps {
92     isAdmin: boolean;
93     isSelf: boolean;
94     isPristine: boolean;
95     isValid: boolean;
96     isInaccessible: boolean;
97     userUuid: string;
98     resources: ResourcesState;
99     localCluster: string;
100 }
101
102 const RoleTypes = [
103     { key: '', value: '' },
104     { key: 'Bio-informatician', value: 'Bio-informatician' },
105     { key: 'Data Scientist', value: 'Data Scientist' },
106     { key: 'Analyst', value: 'Analyst' },
107     { key: 'Researcher', value: 'Researcher' },
108     { key: 'Software Developer', value: 'Software Developer' },
109     { key: 'System Administrator', value: 'System Administrator' },
110     { key: 'Other', value: 'Other' }
111 ];
112
113 type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & DispatchProp & WithStyles<CssRules>;
114
115 export enum UserProfileGroupsColumnNames {
116     NAME = "Name",
117     PERMISSION = "Permission",
118     VISIBLE = "Visible to other members",
119     UUID = "UUID",
120     REMOVE = "Remove",
121 }
122
123 enum TABS {
124     PROFILE = "PROFILE",
125     GROUPS = "GROUPS",
126
127 }
128
129 export const userProfileGroupsColumns: DataColumns<string> = [
130     {
131         name: UserProfileGroupsColumnNames.NAME,
132         selected: true,
133         configurable: true,
134         filters: createTree(),
135         render: uuid => <ResourceLinkHead uuid={uuid} />
136     },
137     {
138         name: UserProfileGroupsColumnNames.PERMISSION,
139         selected: true,
140         configurable: true,
141         filters: createTree(),
142         render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
143     },
144     {
145         name: UserProfileGroupsColumnNames.VISIBLE,
146         selected: true,
147         configurable: true,
148         filters: createTree(),
149         render: uuid => <ResourceLinkTailIsVisible uuid={uuid} />
150     },
151     {
152         name: UserProfileGroupsColumnNames.UUID,
153         selected: true,
154         configurable: true,
155         filters: createTree(),
156         render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
157     },
158     {
159         name: UserProfileGroupsColumnNames.REMOVE,
160         selected: true,
161         configurable: true,
162         filters: createTree(),
163         render: uuid => <ResourceLinkDelete uuid={uuid} />
164     },
165 ];
166
167 const ReadOnlyField = withStyles(styles)(
168     (props: ({ label: string, input: {value: string} }) & WithStyles<CssRules> ) => (
169         <Grid item xs={12} data-cy="field">
170             <Typography className={props.classes.label}>
171                 {props.label}
172             </Typography>
173             <Typography className={props.classes.readOnlyValue} data-cy="value">
174                 {props.input.value}
175             </Typography>
176         </Grid>
177     )
178 );
179
180 export const UserProfilePanelRoot = withStyles(styles)(
181     class extends React.Component<UserProfilePanelRootProps> {
182         state = {
183             value: TABS.PROFILE,
184         };
185
186         componentDidMount() {
187             this.setState({ value: TABS.PROFILE});
188         }
189
190         render() {
191             if (this.props.isInaccessible) {
192                 return (
193                     <Paper className={this.props.classes.emptyRoot}>
194                         <CardContent>
195                             <DefaultView icon={DetailsIcon} messages={['This user does not exist or your account does not have permission to view it']} />
196                         </CardContent>
197                     </Paper>
198                 );
199             } else {
200                 return <Paper className={this.props.classes.root}>
201                     <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
202                         <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
203                         <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
204                     </Tabs>
205                     {this.state.value === TABS.PROFILE &&
206                         <CardContent>
207                             <Grid container justify="space-between">
208                                 <Grid item xs={11}>
209                                     <Typography className={this.props.classes.title}>
210                                         {this.props.userUuid}
211                                         <CopyToClipboardSnackbar value={this.props.userUuid} />
212                                     </Typography>
213                                 </Grid>
214                                 <Grid item xs={1} style={{ textAlign: "right" }}>
215                                     <Tooltip title="Actions" disableFocusListener>
216                                         <IconButton
217                                             data-cy='collection-panel-options-btn'
218                                             aria-label="Actions"
219                                             onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
220                                             <MoreOptionsIcon />
221                                         </IconButton>
222                                     </Tooltip>
223                                 </Grid>
224                             </Grid>
225                             <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
226                                 <Grid container spacing={24}>
227                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
228                                         <Field
229                                             label="First name"
230                                             name="firstName"
231                                             component={TextField as any}
232                                             disabled={!this.props.isAdmin && !this.props.isSelf}
233                                         />
234                                     </Grid>
235                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
236                                         <Field
237                                             label="Last name"
238                                             name="lastName"
239                                             component={TextField as any}
240                                             disabled={!this.props.isAdmin && !this.props.isSelf}
241                                         />
242                                     </Grid>
243                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
244                                         <Field
245                                             label="E-mail"
246                                             name="email"
247                                             component={ReadOnlyField as any}
248                                             disabled
249                                         />
250                                     </Grid>
251                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
252                                         <Field
253                                             label="Username"
254                                             name="username"
255                                             component={ReadOnlyField as any}
256                                             disabled
257                                         />
258                                     </Grid>
259                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
260                                         <Field
261                                             label="Organization"
262                                             name="prefs.profile.organization"
263                                             component={TextField as any}
264                                             disabled={!this.props.isAdmin && !this.props.isSelf}
265                                         />
266                                     </Grid>
267                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
268                                         <Field
269                                             label="E-mail at Organization"
270                                             name="prefs.profile.organization_email"
271                                             component={TextField as any}
272                                             disabled={!this.props.isAdmin && !this.props.isSelf}
273                                             validate={PROFILE_EMAIL_VALIDATION}
274                                         />
275                                     </Grid>
276                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
277                                         <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
278                                         <Field
279                                             id="prefs.profile.role"
280                                             name="prefs.profile.role"
281                                             component={NativeSelectField as any}
282                                             items={RoleTypes}
283                                             disabled={!this.props.isAdmin && !this.props.isSelf}
284                                         />
285                                     </Grid>
286                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
287                                         <Field
288                                             label="Website"
289                                             name="prefs.profile.website_url"
290                                             component={TextField as any}
291                                             disabled={!this.props.isAdmin && !this.props.isSelf}
292                                         />
293                                     </Grid>
294                                     <Grid item sm={12}>
295                                         <Grid container direction="row" justify="flex-end">
296                                             <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
297                                             <Button
298                                                 color="primary"
299                                                 variant="contained"
300                                                 type="submit"
301                                                 disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
302                                                 Save changes
303                                             </Button>
304                                         </Grid>
305                                     </Grid>
306                                 </Grid>
307                             </form >
308                         </CardContent>
309                     }
310                     {this.state.value === TABS.GROUPS &&
311                         <div className={this.props.classes.content}>
312                             <DataExplorer
313                                     id={USER_PROFILE_PANEL_ID}
314                                     data-cy="user-profile-groups-data-explorer"
315                                     onRowClick={noop}
316                                     onRowDoubleClick={noop}
317                                     onContextMenu={noop}
318                                     contextMenuColumn={false}
319                                     hideColumnSelector
320                                     hideSearchInput
321                                     paperProps={{
322                                         elevation: 0,
323                                     }}
324                                     dataTableDefaultView={
325                                         <DataTableDefaultView
326                                             icon={GroupsIcon}
327                                             messages={['Group list is empty.']} />
328                                     } />
329                         </div>}
330                 </Paper >;
331             }
332         }
333
334         handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
335             this.setState({ value });
336         }
337
338         handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
339             event.stopPropagation();
340             const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
341             if (resource) {
342                 this.props.handleContextMenu(event, resource);
343             }
344         }
345
346     }
347 );