import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
import { MoreOptionsIcon } from '~/components/icon/icon';
-type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton';
+type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton' | 'rootUserPanel';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
searchBox: {
root: {
height: '100%'
},
+ rootUserPanel: {
+ height: '100%',
+ boxShadow: 'none'
+ },
moreOptionsButton: {
padding: 0
}
contextMenuColumn: boolean;
dataTableDefaultView?: React.ReactNode;
working?: boolean;
- isColumnSelectorHidden?: boolean;
+ isUserPanel?: boolean;
}
interface DataExplorerActionProps<T> {
columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
- dataTableDefaultView, isColumnSelectorHidden
+ dataTableDefaultView, isUserPanel
} = this.props;
- return <Paper className={classes.root}>
- <Toolbar className={classes.toolbar}>
+ return <Paper className={!isUserPanel ? classes.root : classes.rootUserPanel}>
+ <Toolbar className={!isUserPanel ? classes.toolbar : ''}>
<Grid container justify="space-between" wrap="nowrap" alignItems="center">
<div className={classes.searchBox}>
<SearchInput
value={searchValue}
onSearch={onSearch} />
</div>
- {!isColumnSelectorHidden && <ColumnSelector
+ {!isUserPanel && <ColumnSelector
columns={columns}
onColumnToggle={onColumnToggle} />}
</Grid>
export const USERS_PANEL_ID = 'usersPanel';
export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
export const USER_CREATE_FORM_NAME = 'repositoryCreateFormName';
-export const USER_REMOVE_DIALOG = 'repositoryRemoveDialog';
+
+export interface UserCreateFormDialogData {
+ firstName: string;
+ lastName: string;
+ email: string;
+ identityUrl: string;
+ virtualMachineName: string;
+ groupVirtualMachine: string;
+}
export const openUserAttributes = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(dialogActions.OPEN_DIALOG({ id: USER_CREATE_FORM_NAME, data: { user } }));
};
-export const createUser = (user: UserResource) =>
+export const createUser = (user: UserCreateFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = await services.authService.getUuid();
- const user = await services.userService.get(userUuid!);
dispatch(startSubmit(USER_CREATE_FORM_NAME));
try {
- // const newUser = await services.repositoriesService.create({ name: `${user.username}/${repository.name}` });
+ const newUser = await services.userService.create({ ...user });
dispatch(dialogActions.CLOSE_DIALOG({ id: USER_CREATE_FORM_NAME }));
dispatch(reset(USER_CREATE_FORM_NAME));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been successfully created.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
dispatch<any>(loadUsersData());
- // return newUser;
- return;
+ dispatch(userBindedActions.REQUEST_ITEMS());
+ return newUser;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.NAME_HAS_ALREADY_BEEN_TAKEN) {
}
};
-export const openRemoveUsersDialog = (uuid: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(dialogActions.OPEN_DIALOG({
- id: USER_REMOVE_DIALOG,
- data: {
- title: 'Remove user',
- text: 'Are you sure you want to remove this user?',
- confirmButtonLabel: 'Remove',
- uuid
- }
- }));
- };
-
-export const removeUser = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...' }));
- await services.userService.delete(uuid);
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
- dispatch<any>(loadUsersData());
- };
-
export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
export const openUsersPanel = () =>
export const REPOSITORY_NAME_VALIDATION = [require, maxLength(255)];
+export const USER_EMAIL_VALIDATION = [require, maxLength(255)];
+export const USER_LENGTH_VALIDATION = [maxLength(255)];
+
export const SSH_KEY_PUBLIC_VALIDATION = [require, isRsaKey, maxLength(1024)];
export const SSH_KEY_NAME_VALIDATION = [require, maxLength(255)];
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, ProjectIcon, AttributesIcon, ShareIcon } from "~/components/icon/icon";
+import { AdvancedIcon, ProjectIcon, AttributesIcon, UserPanelIcon } from "~/components/icon/icon";
import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
import { openUserAttributes } from "~/store/users/users-actions";
},
{
name: "Manage",
- icon: ShareIcon,
+ icon: UserPanelIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { UserFirstNameField, UserLastNameField, UserEmailField, UserIdentityUrlField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
+
+type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<any>;
+
+export const UserRepositoryCreate = (props: DialogUserProps) =>
+ <FormDialog
+ dialogTitle='New user'
+ formFields={UserAddFields}
+ submitLabel='ADD NEW USER'
+ {...props}
+ />;
+
+const UserAddFields = () => <span>
+ <UserFirstNameField />
+ <UserLastNameField />
+ <UserEmailField />
+ <UserIdentityUrlField />
+ <UserVirtualMachineField />
+ <UserGroupsVirtualMachineField />
+</span>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { USER_CREATE_FORM_NAME, createUser, UserCreateFormDialogData } from "~/store/users/users-actions";
+import { UserRepositoryCreate } from "~/views-components/dialog-create/dialog-user-create";
+
+export const CreateUserDialog = compose(
+ withDialog(USER_CREATE_FORM_NAME),
+ reduxForm<UserCreateFormDialogData>({
+ form: USER_CREATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(createUser(data));
+ }
+ })
+)(UserRepositoryCreate);
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { Field, WrappedFieldProps } from "redux-form";
+import { Field } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
-import { ProjectTreePicker, ProjectTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
-import { PickerIdProp } from '../../store/tree-picker/picker-id';
+import { ProjectTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
export const CollectionNameField = () =>
<Field
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { Field, WrappedFieldProps } from "redux-form";
+import { Field } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { PROCESS_NAME_VALIDATION } from "~/validators/validators";
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Field } from "redux-form";
+import { TextField } from "~/components/text-field/text-field";
+import { USER_EMAIL_VALIDATION, USER_LENGTH_VALIDATION } from "~/validators/validators";
+import { NativeSelectField } from "~/components/select-field/select-field";
+
+export const UserFirstNameField = () =>
+ <Field
+ name='firstName'
+ component={TextField}
+ validate={USER_LENGTH_VALIDATION}
+ autoFocus={true}
+ label="First name" />;
+
+export const UserLastNameField = () =>
+ <Field
+ name='lastName'
+ component={TextField}
+ validate={USER_LENGTH_VALIDATION}
+ autoFocus={true}
+ label="Last name" />;
+
+export const UserEmailField = () =>
+ <Field
+ name='email'
+ component={TextField}
+ validate={USER_EMAIL_VALIDATION}
+ autoFocus={true}
+ label="Email" />;
+
+export const UserIdentityUrlField = () =>
+ <Field
+ name='identityUrl'
+ component={TextField}
+ validate={USER_LENGTH_VALIDATION}
+ label="Identity URL Prefix" />;
+
+export const UserVirtualMachineField = () =>
+ <Field
+ name='virtualMachine'
+ component={NativeSelectField}
+ validate={USER_LENGTH_VALIDATION}
+ items={['shell']} />;
+
+export const UserGroupsVirtualMachineField = () =>
+ <Field
+ name='virtualMachine'
+ component={TextField}
+ validate={USER_LENGTH_VALIDATION}
+ label="Groups for virtual machine (comma separated list)" />;
\ No newline at end of file
<span>
<Grid container direction="row">
<Grid item xs={5} className={classes.rightContainer}>
- <Grid item>First name</Grid>
- <Grid item>Last name</Grid>
- <Grid item>Owner uuid</Grid>
- <Grid item>Created at</Grid>
- <Grid item>Modified at</Grid>
- <Grid item>Modified by user uuid</Grid>
- <Grid item>Modified by client uuid</Grid>
- <Grid item>uuid</Grid>
- <Grid item>Href</Grid>
- <Grid item>Identity url</Grid>
- <Grid item>Username</Grid>
+ {firstName && <Grid item>First name</Grid>}
+ {lastName && <Grid item>Last name</Grid>}
+ {ownerUuid && <Grid item>Owner uuid</Grid>}
+ {createdAt && <Grid item>Created at</Grid>}
+ {modifiedAt && <Grid item>Modified at</Grid>}
+ {modifiedByUserUuid && <Grid item>Modified by user uuid</Grid>}
+ {modifiedByClientUuid && <Grid item>Modified by client uuid</Grid>}
+ {uuid && <Grid item>uuid</Grid>}
+ {href && <Grid item>Href</Grid>}
+ {identityUrl && <Grid item>Identity url</Grid>}
+ {username && <Grid item>Username</Grid>}
</Grid>
<Grid item xs={7} className={classes.leftContainer}>
<Grid item>{firstName}</Grid>
// SPDX-License-Identifier: AGPL-3.0
import * as React from 'react';
-import { WithStyles, withStyles, Typography } from '@material-ui/core';
+import { WithStyles, withStyles, Typography, Tabs, Tab, Paper, Button } from '@material-ui/core';
import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
import { connect, DispatchProp } from 'react-redux';
import { DataColumns } from '~/components/data-table/data-table';
ResourceUsername
} from "~/views-components/data-explorer/renderers";
import { navigateTo } from "~/store/navigation/navigation-action";
-import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
import { ContextMenuKind } from "~/views-components/context-menu/context-menu";
import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
import { createTree } from '~/models/tree';
-import { compose } from 'redux';
+import { compose, Dispatch } from 'redux';
import { UserResource } from '~/models/user';
-import { ShareMeIcon } from '~/components/icon/icon';
-import { USERS_PANEL_ID } from '~/store/users/users-actions';
+import { ShareMeIcon, AddIcon } from '~/components/icon/icon';
+import { USERS_PANEL_ID, openUserCreateDialog } from '~/store/users/users-actions';
-type UserPanelRules = "toolbar" | "button";
+type UserPanelRules = "button";
const styles = withStyles<UserPanelRules>(theme => ({
- toolbar: {
- paddingBottom: theme.spacing.unit * 3,
- textAlign: "right"
- },
button: {
- marginLeft: theme.spacing.unit
+ marginTop: theme.spacing.unit,
+ marginRight: theme.spacing.unit * 2,
+ textAlign: 'right',
+ alignSelf: 'center'
},
}));
resources: ResourcesState;
}
-type UserPanelProps = UserPanelDataProps & DispatchProp & WithStyles<UserPanelRules>;
+interface UserPanelActionProps {
+ openUserCreateDialog: () => void;
+ handleRowDoubleClick: (uuid: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
+}
+
+const mapStateToProps = (state: RootState) => {
+ return {
+ resources: state.resources
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ openUserCreateDialog: () => dispatch<any>(openUserCreateDialog()),
+ handleRowDoubleClick: (uuid: string) => dispatch<any>(navigateTo(uuid)),
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => dispatch<any>(openContextMenu(event, item))
+});
+
+type UserPanelProps = UserPanelDataProps & UserPanelActionProps & DispatchProp & WithStyles<UserPanelRules>;
export const UserPanel = compose(
styles,
- connect((state: RootState) => ({
- resources: state.resources
- })))(
+ connect(mapStateToProps, mapDispatchToProps))(
class extends React.Component<UserPanelProps> {
+ state = {
+ value: 0,
+ };
+
+ componentDidMount() {
+ this.setState({ value: 0 });
+ }
+
render() {
- return <DataExplorer
- id={USERS_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
- onContextMenu={this.handleContextMenu}
- contextMenuColumn={true}
- isColumnSelectorHidden={true}
- dataTableDefaultView={
- <DataTableDefaultView
- icon={ShareMeIcon}
- messages={['Your user list is empty.']} />
- } />;
+ const { value } = this.state;
+ return <Paper>
+ <Tabs value={value} onChange={this.handleChange} fullWidth>
+ <Tab label="USERS" />
+ <Tab label="ACTIVITY" />
+ </Tabs>
+ {value === 0 &&
+ <span>
+ <div className={this.props.classes.button}>
+ <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
+ <AddIcon /> NEW USER
+ </Button>
+ </div>
+ <DataExplorer
+ id={USERS_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={true}
+ isUserPanel={true}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={ShareMeIcon}
+ messages={['Your user list is empty.']} />
+ } />
+ </span>}
+ </Paper>;
+ }
+
+ handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ this.setState({ value });
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
if (resource) {
- this.props.dispatch<any>(openContextMenu(event, {
+ this.props.onContextMenu(event, {
name: '',
uuid: resource.uuid,
ownerUuid: resource.ownerUuid,
kind: resource.kind,
menuKind: ContextMenuKind.USER
- }));
+ });
}
}
handleRowDoubleClick = (uuid: string) => {
- this.props.dispatch<any>(navigateTo(uuid));
+ this.props.handleRowDoubleClick(uuid);
}
- handleRowClick = (uuid: string) => {
- this.props.dispatch(loadDetailsPanel(uuid));
+ handleRowClick = () => {
+ return;
}
}
);
import { RemoveVirtualMachineDialog } from '~/views-components/virtual-machines-dialog/remove-dialog';
import { UserPanel } from '~/views/user-panel/user-panel';
import { UserAttributesDialog } from '~/views-components/user-dialog/attributes-dialog';
+import { CreateUserDialog } from '~/views-components/dialog-forms/create-user-dialog';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<CreateProjectDialog />
<CreateRepositoryDialog />
<CreateSshKeyDialog />
+ <CreateUserDialog />
<CurrentTokenDialog />
<FileRemoveDialog />
<FilesUploadCollectionDialog />