1 // Copyright (C) The Arvados Authors. All rights reserved.
3 // SPDX-License-Identifier: AGPL-3.0
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 { CustomStyleRulesCallback } from 'common/custom-theme';
24 } from '@mui/material';
25 import { WithStyles } from '@mui/styles';
26 import withStyles from '@mui/styles/withStyles';
27 import { ArvadosTheme } from 'common/custom-theme';
28 import { PROFILE_EMAIL_VALIDATION, PROFILE_URL_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, MoreVerticalIcon } from 'components/icon/icon';
32 import { DataColumns } from 'components/data-table/data-table';
33 import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible, UserResourceAccountStatus } 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 import { PermissionResource } from 'models/permission';
40 type CssRules = 'root' | 'emptyRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon' | 'userProfileFormMessage';
42 const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
50 padding: theme.spacing(4),
58 color: theme.palette.grey['600']
67 color: theme.palette.grey["600"]
71 justifyContent: 'flex-end'
74 // reserve space for the tab bar
75 height: `calc(100% - ${theme.spacing(7)})`,
78 marginLeft: theme.spacing(1),
79 color: theme.palette.grey["500"],
86 userProfileFormMessage: {
91 export interface UserProfilePanelRootActionProps {
92 handleContextMenu: (event, resource: UserResource) => void;
95 export interface UserProfilePanelRootDataProps {
100 isInaccessible: boolean;
102 resources: ResourcesState;
103 localCluster: string;
104 userProfileFormMessage: string;
108 { key: '', value: '' },
109 { key: 'Bio-informatician', value: 'Bio-informatician' },
110 { key: 'Data Scientist', value: 'Data Scientist' },
111 { key: 'Analyst', value: 'Analyst' },
112 { key: 'Researcher', value: 'Researcher' },
113 { key: 'Software Developer', value: 'Software Developer' },
114 { key: 'System Administrator', value: 'System Administrator' },
115 { key: 'Other', value: 'Other' }
118 type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & DispatchProp & WithStyles<CssRules>;
120 export enum UserProfileGroupsColumnNames {
122 PERMISSION = "Permission",
123 VISIBLE = "Visible to other members",
134 export const userProfileGroupsColumns: DataColumns<string, PermissionResource> = [
136 name: UserProfileGroupsColumnNames.NAME,
139 filters: createTree(),
140 render: uuid => <ResourceLinkHead uuid={uuid} />
143 name: UserProfileGroupsColumnNames.PERMISSION,
146 filters: createTree(),
147 render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
150 name: UserProfileGroupsColumnNames.VISIBLE,
153 filters: createTree(),
154 render: uuid => <ResourceLinkTailIsVisible uuid={uuid} />
157 name: UserProfileGroupsColumnNames.UUID,
160 filters: createTree(),
161 render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
164 name: UserProfileGroupsColumnNames.REMOVE,
167 filters: createTree(),
168 render: uuid => <ResourceLinkDelete uuid={uuid} />
172 const ReadOnlyField = withStyles(styles)(
173 (props: ({ label: string, input: { value: string } }) & WithStyles<CssRules>) => (
174 <Grid item xs={12} data-cy="field">
175 <Typography className={props.classes.label}>
178 <Typography className={props.classes.readOnlyValue} data-cy="value">
185 export const UserProfilePanelRoot = withStyles(styles)(
186 class extends React.Component<UserProfilePanelRootProps> {
191 componentDidMount() {
192 this.setState({ value: TABS.PROFILE });
196 if (this.props.isInaccessible) {
198 <Paper className={this.props.classes.emptyRoot}>
200 <DefaultView icon={DetailsIcon} messages={['This user does not exist or your account does not have permission to view it']} />
206 <Paper className={this.props.classes.root}>
207 <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
208 <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
209 <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
211 {this.state.value === TABS.PROFILE &&
213 <Grid container justifyContent="space-between">
215 <Typography className={this.props.classes.title}>
216 {this.props.userUuid}
217 <CopyToClipboardSnackbar value={this.props.userUuid} />
221 <Grid container alignItems="center">
222 <Grid item style={{ marginRight: '10px' }}><UserResourceAccountStatus uuid={this.props.userUuid} /></Grid>
224 <Tooltip title="Actions" disableFocusListener>
226 data-cy='user-profile-panel-options-btn'
228 onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}
237 <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
238 <Grid container spacing={3}>
239 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
243 component={TextField as any}
244 disabled={!this.props.isAdmin && !this.props.isSelf}
247 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
251 component={TextField as any}
252 disabled={!this.props.isAdmin && !this.props.isSelf}
255 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
259 component={ReadOnlyField as any}
263 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
267 component={ReadOnlyField as any}
271 <Grid item className={this.props.classes.gridItem} xs={12}>
272 <span className={this.props.classes.userProfileFormMessage}>{this.props.userProfileFormMessage}</span>
274 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
277 name="prefs.profile.organization"
278 component={TextField as any}
279 disabled={!this.props.isAdmin && !this.props.isSelf}
282 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
284 label="E-mail at Organization"
285 name="prefs.profile.organization_email"
286 component={TextField as any}
287 disabled={!this.props.isAdmin && !this.props.isSelf}
288 validate={PROFILE_EMAIL_VALIDATION}
291 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
292 <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
294 id="prefs.profile.role"
295 name="prefs.profile.role"
296 component={NativeSelectField as any}
298 disabled={!this.props.isAdmin && !this.props.isSelf}
301 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
304 name="prefs.profile.website_url"
305 component={TextField as any}
306 disabled={!this.props.isAdmin && !this.props.isSelf}
307 validate={PROFILE_URL_VALIDATION}
311 <Grid container direction="row" justifyContent="flex-end">
312 <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
317 disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
326 {this.state.value === TABS.GROUPS &&
327 <div className={this.props.classes.content}>
329 id={USER_PROFILE_PANEL_ID}
330 data-cy="user-profile-groups-data-explorer"
332 onRowDoubleClick={noop}
334 contextMenuColumn={false}
340 defaultViewIcon={GroupsIcon}
341 defaultViewMessages={['Group list is empty.']} />
348 handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
349 this.setState({ value });
352 handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
353 event.stopPropagation();
354 const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
356 this.props.handleContextMenu(event, resource);