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";
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';
40 type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
42 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
48 // ...theme.mixins.gutters()
56 color: theme.palette.grey['600']
65 color: theme.palette.grey["600"]
69 justifyContent: 'flex-end'
72 // reserve space for the tab bar
73 height: `calc(100% - ${theme.spacing.unit * 7}px)`,
76 marginLeft: theme.spacing.unit,
77 color: theme.palette.grey["500"],
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;
93 export interface UserProfilePanelRootDataProps {
99 resources: ResourcesState
100 localCluster: string;
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' }
114 type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & DispatchProp & WithStyles<CssRules>;
116 export enum UserProfileGroupsColumnNames {
118 PERMISSION = "Permission",
119 VISIBLE = "Visible to other members",
131 export const userProfileGroupsColumns: DataColumns<string> = [
133 name: UserProfileGroupsColumnNames.NAME,
136 filters: createTree(),
137 render: uuid => <ResourceLinkHead uuid={uuid} />
140 name: UserProfileGroupsColumnNames.PERMISSION,
143 filters: createTree(),
144 render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
147 name: UserProfileGroupsColumnNames.VISIBLE,
150 filters: createTree(),
151 render: uuid => <ResourceLinkTailIsVisible uuid={uuid} />
154 name: UserProfileGroupsColumnNames.UUID,
157 filters: createTree(),
158 render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
161 name: UserProfileGroupsColumnNames.REMOVE,
164 filters: createTree(),
165 render: uuid => <ResourceLinkDelete uuid={uuid} />
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}>
175 <Typography className={props.classes.readOnlyValue} data-cy="value">
182 export const UserProfilePanelRoot = withStyles(styles)(
183 class extends React.Component<UserProfilePanelRootProps> {
188 componentDidMount() {
189 this.setState({ value: TABS.PROFILE});
192 onCopy = (message: string) => {
193 this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
196 kind: SnackbarKind.SUCCESS
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} />}
207 {this.state.value === TABS.PROFILE &&
209 <Grid container justify="space-between">
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")}>
222 <Grid item xs={1} style={{ textAlign: "right" }}>
223 <Tooltip title="Actions" disableFocusListener>
225 data-cy='collection-panel-options-btn'
227 onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
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">
239 component={ReadOnlyField as any}
243 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
247 component={ReadOnlyField as any}
251 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
255 component={ReadOnlyField as any}
259 <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
263 component={ReadOnlyField as any}
267 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
270 name="prefs.profile.organization"
271 component={TextField as any}
272 disabled={!this.props.isAdmin && !this.props.isSelf}
275 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
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}
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>
287 id="prefs.profile.role"
288 name="prefs.profile.role"
289 component={NativeSelectField as any}
291 disabled={!this.props.isAdmin && !this.props.isSelf}
294 <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
297 name="prefs.profile.website_url"
298 component={TextField as any}
299 disabled={!this.props.isAdmin && !this.props.isSelf}
303 <Grid container direction="row" justify="flex-end">
304 <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
309 disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
318 {this.state.value === TABS.GROUPS &&
319 <div className={this.props.classes.content}>
321 id={USER_PROFILE_PANEL_ID}
322 data-cy="user-profile-groups-data-explorer"
324 onRowDoubleClick={noop}
326 contextMenuColumn={false}
332 dataTableDefaultView={
333 <DataTableDefaultView
335 messages={['Group list is empty.']} />
338 {this.props.isAdmin && this.state.value === TABS.ADMIN &&
339 <Paper elevation={0} className={this.props.classes.adminRoot}>
345 alignItems={'center'}>
347 <Typography variant="h6" className={this.props.classes.title}>
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.
354 <Grid item sm={'auto'} xs={12}>
355 <Button variant="contained"
357 onClick={() => {this.props.openSetupDialog(this.props.userUuid)}}
370 alignItems={'center'}>
372 <Typography variant="h6" className={this.props.classes.title}>
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.
379 <Grid item sm={'auto'} xs={12}>
380 <Button variant="contained"
382 onClick={() => {this.props.openDeactivateDialog(this.props.userUuid)}}
395 alignItems={'center'}>
397 <Typography variant="h6" className={this.props.classes.title}>
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.
404 <Grid item sm={'auto'} xs={12}>
405 <Button variant="contained"
407 onClick={() => {this.props.loginAs(this.props.userUuid)}}
419 handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
420 this.setState({ value });
423 handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
424 event.stopPropagation();
425 const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
427 this.props.handleContextMenu(event, resource);