19969: Fix required condition on project input, make required prop required
[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 { PROFILE_EMAIL_VALIDATION, PROFILE_URL_VALIDATION } from "validators/validators";
28 import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
29 import { noop } from 'lodash';
30 import { DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
31 import { DataColumns } from 'components/data-table/data-table';
32 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible, UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
33 import { createTree } from 'models/tree';
34 import { getResource, ResourcesState } from 'store/resources/resources';
35 import { DefaultView } from 'components/default-view/default-view';
36 import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
37 import { PermissionResource } from 'models/permission';
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, PermissionResource> = [
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>
209                                     <Typography className={this.props.classes.title}>
210                                         {this.props.userUuid}
211                                         <CopyToClipboardSnackbar value={this.props.userUuid} />
212                                     </Typography>
213                                 </Grid>
214                                 <Grid item>
215                                     <Grid container alignItems="center">
216                                         <Grid item style={{marginRight: '10px'}}><UserResourceAccountStatus uuid={this.props.userUuid} /></Grid>
217                                         <Grid item>
218                                             <Tooltip title="Actions" disableFocusListener>
219                                                 <IconButton
220                                                     data-cy='user-profile-panel-options-btn'
221                                                     aria-label="Actions"
222                                                     onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
223                                                     <MoreOptionsIcon />
224                                                 </IconButton>
225                                             </Tooltip>
226                                         </Grid>
227                                     </Grid>
228                                 </Grid>
229                             </Grid>
230                             <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
231                                 <Grid container spacing={24}>
232                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
233                                         <Field
234                                             label="First name"
235                                             name="firstName"
236                                             component={TextField as any}
237                                             disabled={!this.props.isAdmin && !this.props.isSelf}
238                                         />
239                                     </Grid>
240                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
241                                         <Field
242                                             label="Last name"
243                                             name="lastName"
244                                             component={TextField as any}
245                                             disabled={!this.props.isAdmin && !this.props.isSelf}
246                                         />
247                                     </Grid>
248                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
249                                         <Field
250                                             label="E-mail"
251                                             name="email"
252                                             component={ReadOnlyField as any}
253                                             disabled
254                                         />
255                                     </Grid>
256                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
257                                         <Field
258                                             label="Username"
259                                             name="username"
260                                             component={ReadOnlyField as any}
261                                             disabled
262                                         />
263                                     </Grid>
264                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
265                                         <Field
266                                             label="Organization"
267                                             name="prefs.profile.organization"
268                                             component={TextField as any}
269                                             disabled={!this.props.isAdmin && !this.props.isSelf}
270                                         />
271                                     </Grid>
272                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
273                                         <Field
274                                             label="E-mail at Organization"
275                                             name="prefs.profile.organization_email"
276                                             component={TextField as any}
277                                             disabled={!this.props.isAdmin && !this.props.isSelf}
278                                             validate={PROFILE_EMAIL_VALIDATION}
279                                         />
280                                     </Grid>
281                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
282                                         <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
283                                         <Field
284                                             id="prefs.profile.role"
285                                             name="prefs.profile.role"
286                                             component={NativeSelectField as any}
287                                             items={RoleTypes}
288                                             disabled={!this.props.isAdmin && !this.props.isSelf}
289                                         />
290                                     </Grid>
291                                     <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
292                                         <Field
293                                             label="Website"
294                                             name="prefs.profile.website_url"
295                                             component={TextField as any}
296                                             disabled={!this.props.isAdmin && !this.props.isSelf}
297                                             validate={PROFILE_URL_VALIDATION}
298                                         />
299                                     </Grid>
300                                     <Grid item sm={12}>
301                                         <Grid container direction="row" justify="flex-end">
302                                             <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
303                                             <Button
304                                                 color="primary"
305                                                 variant="contained"
306                                                 type="submit"
307                                                 disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
308                                                 Save changes
309                                             </Button>
310                                         </Grid>
311                                     </Grid>
312                                 </Grid>
313                             </form >
314                         </CardContent>
315                     }
316                     {this.state.value === TABS.GROUPS &&
317                         <div className={this.props.classes.content}>
318                             <DataExplorer
319                                     id={USER_PROFILE_PANEL_ID}
320                                     data-cy="user-profile-groups-data-explorer"
321                                     onRowClick={noop}
322                                     onRowDoubleClick={noop}
323                                     onContextMenu={noop}
324                                     contextMenuColumn={false}
325                                     hideColumnSelector
326                                     hideSearchInput
327                                     paperProps={{
328                                         elevation: 0,
329                                     }}
330                                     defaultViewIcon={GroupsIcon}
331                                     defaultViewMessages={['Group list is empty.']} />
332                         </div>}
333                 </Paper >;
334             }
335         }
336
337         handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
338             this.setState({ value });
339         }
340
341         handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
342             event.stopPropagation();
343             const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
344             if (resource) {
345                 this.props.handleContextMenu(event, resource);
346             }
347         }
348
349     }
350 );