creating-user
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 5 Dec 2018 16:05:29 +0000 (17:05 +0100)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Wed, 5 Dec 2018 16:05:29 +0000 (17:05 +0100)
Feature #14504

Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>

12 files changed:
src/components/data-explorer/data-explorer.tsx
src/store/users/users-actions.ts
src/validators/validators.tsx
src/views-components/context-menu/action-sets/user-action-set.ts
src/views-components/dialog-create/dialog-user-create.tsx [new file with mode: 0644]
src/views-components/dialog-forms/create-user-dialog.ts [new file with mode: 0644]
src/views-components/form-fields/collection-form-fields.tsx
src/views-components/form-fields/process-form-fields.tsx
src/views-components/form-fields/user-form-fields.tsx [new file with mode: 0644]
src/views-components/user-dialog/attributes-dialog.tsx
src/views/user-panel/user-panel.tsx
src/views/workbench/workbench.tsx

index 69f93ddace807f9012c6bba21ecad1f8483d2271..d906a32cbf7b5bab7f41dbed0062de63fa2d252c 100644 (file)
@@ -13,7 +13,7 @@ import { createTree } from '~/models/tree';
 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: {
@@ -28,6 +28,10 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
         height: '100%'
     },
+    rootUserPanel: {
+        height: '100%',
+        boxShadow: 'none'
+    },
     moreOptionsButton: {
         padding: 0
     }
@@ -44,7 +48,7 @@ interface DataExplorerDataProps<T> {
     contextMenuColumn: boolean;
     dataTableDefaultView?: React.ReactNode;
     working?: boolean;
-    isColumnSelectorHidden?: boolean;
+    isUserPanel?: boolean;
 }
 
 interface DataExplorerActionProps<T> {
@@ -75,17 +79,17 @@ export const DataExplorer = withStyles(styles)(
                 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>
index 43d9e6f32ad1416d3a06f9bfdff176adb0946fc4..ca6edd62ab3f7003138909db3216e13eb4f709f3 100644 (file)
@@ -24,7 +24,15 @@ export type UsersActions = UnionOf<typeof usersPanelActions>;
 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) => {
@@ -41,19 +49,17 @@ export const openUserCreateDialog = () =>
         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) {
@@ -63,27 +69,6 @@ export const createUser = (user: UserResource) =>
         }
     };
 
-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 = () =>
index c601df17416d8711be048d51144684703fa4fe8c..464f190072b06544be1c4b00702f2f12b89a0a77 100644 (file)
@@ -24,5 +24,8 @@ export const PROCESS_NAME_VALIDATION = [require, maxLength(255)];
 
 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)];
index e06a7c219832caa19f4194a19a58c74f8e56d1b9..68098cff1d50b6f5823850a08c0470cec4ac5f8c 100644 (file)
@@ -3,7 +3,7 @@
 // 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";
 
@@ -28,7 +28,7 @@ export const userActionSet: ContextMenuActionSet = [[{
 },
 {
     name: "Manage",
-    icon: ShareIcon,
+    icon: UserPanelIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
     }
diff --git a/src/views-components/dialog-create/dialog-user-create.tsx b/src/views-components/dialog-create/dialog-user-create.tsx
new file mode 100644 (file)
index 0000000..bf135e8
--- /dev/null
@@ -0,0 +1,28 @@
+// 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>;
diff --git a/src/views-components/dialog-forms/create-user-dialog.ts b/src/views-components/dialog-forms/create-user-dialog.ts
new file mode 100644 (file)
index 0000000..cb46204
--- /dev/null
@@ -0,0 +1,19 @@
+// 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
index 2d2a7c80880b0fef31e428c46150fd504d506f95..c02996d81cb21b148708d0cac1a2a9a398a1ee65 100644 (file)
@@ -3,11 +3,11 @@
 // 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
index cf471b67d1f16323abd551f69342782c11e068f0..8f55e08456258fa3a0f87f259cfd8afa66e8c5d5 100644 (file)
@@ -3,7 +3,7 @@
 // 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";
 
diff --git a/src/views-components/form-fields/user-form-fields.tsx b/src/views-components/form-fields/user-form-fields.tsx
new file mode 100644 (file)
index 0000000..6dd635b
--- /dev/null
@@ -0,0 +1,54 @@
+// 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
index 5f711de79b475b3ac2e57ff2e549b35bcca27d6a..66a488156d41a0a53d94adf70726c4283db3772a 100644 (file)
@@ -66,17 +66,17 @@ const attributes = (user: UserResource, classes: any) => {
         <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>
index ceaba8cbf76e31e6725caa900d153130fa857359..db44d508295f1c34bbd91fbe58f9e7a7287b6951 100644 (file)
@@ -3,7 +3,7 @@
 // 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';
@@ -21,24 +21,22 @@ import {
     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'
     },
 }));
 
@@ -124,48 +122,91 @@ interface UserPanelDataProps {
     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;
             }
         }
     );
index 11285ba90863321534235c9aefc8690fc2c36da8..5a6ba97de4a28569a577526182044025b320e2e3 100644 (file)
@@ -66,6 +66,7 @@ import { VirtualMachineAttributesDialog } from '~/views-components/virtual-machi
 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';
 
@@ -159,6 +160,7 @@ export const WorkbenchPanel =
             <CreateProjectDialog />
             <CreateRepositoryDialog />
             <CreateSshKeyDialog />
+            <CreateUserDialog />
             <CurrentTokenDialog />
             <FileRemoveDialog />
             <FilesUploadCollectionDialog />