// Copyright (C) The Arvados Authors. All rights reserved. // // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; import { Field, InjectedFormProps } from "redux-form"; import { DispatchProp } from 'react-redux'; import { UserResource } from 'models/user'; import { TextField } from "components/text-field/text-field"; import { DataExplorer } from "views-components/data-explorer/data-explorer"; import { NativeSelectField } from "components/select-field/select-field"; import { CustomStyleRulesCallback } from 'common/custom-theme'; import { WithStyles, withStyles, CardContent, Button, Typography, Grid, InputLabel, Tabs, Tab, Paper, Tooltip, IconButton, } from '@material-ui/core'; import { ArvadosTheme } from 'common/custom-theme'; import { PROFILE_EMAIL_VALIDATION, PROFILE_URL_VALIDATION } from "validators/validators"; import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions'; import { noop } from 'lodash'; import { DetailsIcon, GroupsIcon, MoreVerticalIcon } from 'components/icon/icon'; import { DataColumns } from 'components/data-table/data-table'; import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible, UserResourceAccountStatus } from 'views-components/data-explorer/renderers'; import { createTree } from 'models/tree'; import { getResource, ResourcesState } from 'store/resources/resources'; import { DefaultView } from 'components/default-view/default-view'; import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar'; import { PermissionResource } from 'models/permission'; type CssRules = 'root' | 'emptyRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon' | 'userProfileFormMessage'; const styles: CustomStyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({ root: { width: '100%', overflow: 'auto' }, emptyRoot: { width: '100%', overflow: 'auto', padding: theme.spacing(4), }, gridItem: { height: 45, marginBottom: 20 }, label: { fontSize: '0.675rem', color: theme.palette.grey['600'] }, readOnlyValue: { fontSize: '0.875rem', }, title: { fontSize: '1.1rem', }, description: { color: theme.palette.grey["600"] }, actions: { display: 'flex', justifyContent: 'flex-end' }, content: { // reserve space for the tab bar height: `calc(100% - ${theme.spacing(7)}px)`, }, copyIcon: { marginLeft: theme.spacing(1), color: theme.palette.grey["500"], cursor: 'pointer', display: 'inline', '& svg': { fontSize: '1rem' } }, userProfileFormMessage: { fontSize: '1.1rem', } }); export interface UserProfilePanelRootActionProps { handleContextMenu: (event, resource: UserResource) => void; } export interface UserProfilePanelRootDataProps { isAdmin: boolean; isSelf: boolean; isPristine: boolean; isValid: boolean; isInaccessible: boolean; userUuid: string; resources: ResourcesState; localCluster: string; userProfileFormMessage: string; } const RoleTypes = [ { key: '', value: '' }, { key: 'Bio-informatician', value: 'Bio-informatician' }, { key: 'Data Scientist', value: 'Data Scientist' }, { key: 'Analyst', value: 'Analyst' }, { key: 'Researcher', value: 'Researcher' }, { key: 'Software Developer', value: 'Software Developer' }, { key: 'System Administrator', value: 'System Administrator' }, { key: 'Other', value: 'Other' } ]; type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & DispatchProp & WithStyles<CssRules>; export enum UserProfileGroupsColumnNames { NAME = "Name", PERMISSION = "Permission", VISIBLE = "Visible to other members", UUID = "UUID", REMOVE = "Remove", } enum TABS { PROFILE = "PROFILE", GROUPS = "GROUPS", } export const userProfileGroupsColumns: DataColumns<string, PermissionResource> = [ { name: UserProfileGroupsColumnNames.NAME, selected: true, configurable: true, filters: createTree(), render: uuid => <ResourceLinkHead uuid={uuid} /> }, { name: UserProfileGroupsColumnNames.PERMISSION, selected: true, configurable: true, filters: createTree(), render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} /> }, { name: UserProfileGroupsColumnNames.VISIBLE, selected: true, configurable: true, filters: createTree(), render: uuid => <ResourceLinkTailIsVisible uuid={uuid} /> }, { name: UserProfileGroupsColumnNames.UUID, selected: true, configurable: true, filters: createTree(), render: uuid => <ResourceLinkHeadUuid uuid={uuid} /> }, { name: UserProfileGroupsColumnNames.REMOVE, selected: true, configurable: true, filters: createTree(), render: uuid => <ResourceLinkDelete uuid={uuid} /> }, ]; const ReadOnlyField = withStyles(styles)( (props: ({ label: string, input: { value: string } }) & WithStyles<CssRules>) => ( <Grid item xs={12} data-cy="field"> <Typography className={props.classes.label}> {props.label} </Typography> <Typography className={props.classes.readOnlyValue} data-cy="value"> {props.input.value} </Typography> </Grid> ) ); export const UserProfilePanelRoot = withStyles(styles)( class extends React.Component<UserProfilePanelRootProps> { state = { value: TABS.PROFILE, }; componentDidMount() { this.setState({ value: TABS.PROFILE }); } render() { if (this.props.isInaccessible) { return ( <Paper className={this.props.classes.emptyRoot}> <CardContent> <DefaultView icon={DetailsIcon} messages={['This user does not exist or your account does not have permission to view it']} /> </CardContent> </Paper> ); } else { return <Paper className={this.props.classes.root}> <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}> <Tab label={TABS.PROFILE} value={TABS.PROFILE} /> <Tab label={TABS.GROUPS} value={TABS.GROUPS} /> </Tabs> {this.state.value === TABS.PROFILE && <CardContent> <Grid container justify="space-between"> <Grid item> <Typography className={this.props.classes.title}> {this.props.userUuid} <CopyToClipboardSnackbar value={this.props.userUuid} /> </Typography> </Grid> <Grid item> <Grid container alignItems="center"> <Grid item style={{ marginRight: '10px' }}><UserResourceAccountStatus uuid={this.props.userUuid} /></Grid> <Grid item> <Tooltip title="Actions" disableFocusListener> <IconButton data-cy='user-profile-panel-options-btn' aria-label="Actions" onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}> <MoreVerticalIcon /> </IconButton> </Tooltip> </Grid> </Grid> </Grid> </Grid> <form onSubmit={this.props.handleSubmit} data-cy="profile-form"> <Grid container spacing={3}> <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName"> <Field label="First name" name="firstName" component={TextField as any} disabled={!this.props.isAdmin && !this.props.isSelf} /> </Grid> <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName"> <Field label="Last name" name="lastName" component={TextField as any} disabled={!this.props.isAdmin && !this.props.isSelf} /> </Grid> <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email"> <Field label="E-mail" name="email" component={ReadOnlyField as any} disabled /> </Grid> <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username"> <Field label="Username" name="username" component={ReadOnlyField as any} disabled /> </Grid> <Grid item className={this.props.classes.gridItem} xs={12}> <span className={this.props.classes.userProfileFormMessage}>{this.props.userProfileFormMessage}</span> </Grid> <Grid item className={this.props.classes.gridItem} sm={6} xs={12}> <Field label="Organization" name="prefs.profile.organization" component={TextField as any} disabled={!this.props.isAdmin && !this.props.isSelf} /> </Grid> <Grid item className={this.props.classes.gridItem} sm={6} xs={12}> <Field label="E-mail at Organization" name="prefs.profile.organization_email" component={TextField as any} disabled={!this.props.isAdmin && !this.props.isSelf} validate={PROFILE_EMAIL_VALIDATION} /> </Grid> <Grid item className={this.props.classes.gridItem} sm={6} xs={12}> <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel> <Field id="prefs.profile.role" name="prefs.profile.role" component={NativeSelectField as any} items={RoleTypes} disabled={!this.props.isAdmin && !this.props.isSelf} /> </Grid> <Grid item className={this.props.classes.gridItem} sm={6} xs={12}> <Field label="Website" name="prefs.profile.website_url" component={TextField as any} disabled={!this.props.isAdmin && !this.props.isSelf} validate={PROFILE_URL_VALIDATION} /> </Grid> <Grid item sm={12}> <Grid container direction="row" justify="flex-end"> <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button> <Button color="primary" variant="contained" type="submit" disabled={this.props.isPristine || this.props.invalid || this.props.submitting}> Save changes </Button> </Grid> </Grid> </Grid> </form > </CardContent> } {this.state.value === TABS.GROUPS && <div className={this.props.classes.content}> <DataExplorer id={USER_PROFILE_PANEL_ID} data-cy="user-profile-groups-data-explorer" onRowClick={noop} onRowDoubleClick={noop} onContextMenu={noop} contextMenuColumn={false} hideColumnSelector hideSearchInput paperProps={{ elevation: 0, }} defaultViewIcon={GroupsIcon} defaultViewMessages={['Group list is empty.']} /> </div>} </Paper >; } } handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => { this.setState({ value }); } handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => { event.stopPropagation(); const resource = getResource<UserResource>(resourceUuid)(this.props.resources); if (resource) { this.props.handleContextMenu(event, resource); } } } );