[ARVADOS-WORKBENCH2] created: 1.3.0-17-gdc95b80

Git user git at public.curoverse.com
Tue Dec 4 10:44:28 EST 2018


        at  dc95b803fa84b3c9ef7c11a4f81dd0d86077d779 (commit)


commit dc95b803fa84b3c9ef7c11a4f81dd0d86077d779
Author: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>
Date:   Tue Dec 4 16:44:11 2018 +0100

    user-admin-panel-init
    
    Feature #14504
    
    Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk <pawel.kowalczyk at contractors.roche.com>

diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx
index cb979c7..3b09b5b 100644
--- a/src/components/data-explorer/data-explorer.tsx
+++ b/src/components/data-explorer/data-explorer.tsx
@@ -44,6 +44,7 @@ interface DataExplorerDataProps<T> {
     contextMenuColumn: boolean;
     dataTableDefaultView?: React.ReactNode;
     working?: boolean;
+    isColumnSelectorHidden?: boolean;
 }
 
 interface DataExplorerActionProps<T> {
@@ -74,7 +75,7 @@ export const DataExplorer = withStyles(styles)(
                 columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
                 rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
                 items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
-                dataTableDefaultView
+                dataTableDefaultView, isColumnSelectorHidden
             } = this.props;
             return <Paper className={classes.root}>
                 <Toolbar className={classes.toolbar}>
@@ -84,9 +85,9 @@ export const DataExplorer = withStyles(styles)(
                                 value={searchValue}
                                 onSearch={onSearch} />
                         </div>
-                        <ColumnSelector
+                        {!isColumnSelectorHidden && <ColumnSelector
                             columns={columns}
-                            onColumnToggle={onColumnToggle} />
+                            onColumnToggle={onColumnToggle} />}
                     </Grid>
                 </Toolbar>
                 <DataTable
diff --git a/src/models/user.ts b/src/models/user.ts
index 9f9c534..b0f004c 100644
--- a/src/models/user.ts
+++ b/src/models/user.ts
@@ -25,8 +25,18 @@ export interface UserResource extends Resource {
     lastName: string;
     identityUrl: string;
     isAdmin: boolean;
-    prefs: string;
+    prefs: UserPrefs;
     defaultOwnerUuid: string;
     isActive: boolean;
     writableBy: string[];
+}
+
+export interface UserPrefs {
+    profile: {
+        lab: string;
+        organization: string;
+        organizationEmail: string;
+        role: string;
+        websiteUrl: string;
+    };
 }
\ No newline at end of file
diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts
index fdc4211..1cea993 100644
--- a/src/routes/route-change-handlers.ts
+++ b/src/routes/route-change-handlers.ts
@@ -8,12 +8,12 @@ import {
     matchProcessRoute, matchProcessLogRoute, matchProjectRoute, matchCollectionRoute, matchFavoritesRoute,
     matchTrashRoute, matchRootRoute, matchSharedWithMeRoute, matchRunProcessRoute, matchWorkflowRoute,
     matchSearchResultsRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute,
-    matchKeepServicesRoute
+    matchKeepServicesRoute, matchUsersRoute
 } from './routes';
 import {
     loadSharedWithMe, loadRunProcess, loadWorkflow, loadSearchResults,
     loadProject, loadCollection, loadFavorites, loadTrash, loadProcess, loadProcessLog,
-    loadSshKeys, loadRepositories, loadVirtualMachines, loadKeepServices
+    loadSshKeys, loadRepositories, loadVirtualMachines, loadKeepServices, loadUsers
 } from '~/store/workbench/workbench-actions';
 import { navigateToRootProject } from '~/store/navigation/navigation-action';
 
@@ -39,6 +39,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const workflowMatch = matchWorkflowRoute(pathname);
     const sshKeysMatch = matchSshKeysRoute(pathname);
     const keepServicesMatch = matchKeepServicesRoute(pathname);
+    const userMatch = matchUsersRoute(pathname);
 
     if (projectMatch) {
         store.dispatch(loadProject(projectMatch.params.id));
@@ -70,5 +71,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(loadSshKeys);
     } else if (keepServicesMatch) {
         store.dispatch(loadKeepServices);
+    } else if (userMatch) {
+        store.dispatch(loadUsers);
     }
 };
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 5cd3e55..2c4337d 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -23,7 +23,8 @@ export const Routes = {
     WORKFLOWS: '/workflows',
     SEARCH_RESULTS: '/search-results',
     SSH_KEYS: `/ssh-keys`,
-    KEEP_SERVICES: `/keep-services`
+    KEEP_SERVICES: `/keep-services`,
+    USERS: '/users'
 };
 
 export const getResourceUrl = (uuid: string) => {
@@ -83,12 +84,15 @@ export const matchSearchResultsRoute = (route: string) =>
 
 export const matchVirtualMachineRoute = (route: string) =>
     matchPath<ResourceRouteParams>(route, { path: Routes.VIRTUAL_MACHINES });
-    
+
 export const matchRepositoriesRoute = (route: string) =>
     matchPath<ResourceRouteParams>(route, { path: Routes.REPOSITORIES });
-    
+
 export const matchSshKeysRoute = (route: string) =>
     matchPath(route, { path: Routes.SSH_KEYS });
 
 export const matchKeepServicesRoute = (route: string) =>
     matchPath(route, { path: Routes.KEEP_SERVICES });
+
+export const matchUsersRoute = (route: string) =>
+    matchPath(route, { path: Routes.USERS });
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index d452710..4ee2210 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -68,4 +68,6 @@ export const navigateToRepositories = push(Routes.REPOSITORIES);
 
 export const navigateToSshKeys= push(Routes.SSH_KEYS);
 
-export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
\ No newline at end of file
+export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
+
+export const navigateToUsers = push(Routes.USERS);
\ No newline at end of file
diff --git a/src/store/repositories/repositories-actions.ts b/src/store/repositories/repositories-actions.ts
index 61caa76..a8b75ac 100644
--- a/src/store/repositories/repositories-actions.ts
+++ b/src/store/repositories/repositories-actions.ts
@@ -91,7 +91,7 @@ export const removeRepository = (uuid: string) =>
 const repositoriesBindedActions = bindDataExplorerActions(REPOSITORIES_PANEL);
 
 export const openRepositoriesPanel = () =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch<any>(navigateToRepositories);
     };
 
diff --git a/src/store/store.ts b/src/store/store.ts
index f8bdcc2..d04775f 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -46,6 +46,8 @@ import { resourcesDataReducer } from "~/store/resources-data/resources-data-redu
 import { virtualMachinesReducer } from "~/store/virtual-machines/virtual-machines-reducer";
 import { repositoriesReducer } from '~/store/repositories/repositories-reducer';
 import { keepServicesReducer } from '~/store/keep-services/keep-services-reducer';
+import { UserMiddlewareService } from '~/store/users/user-panel-middleware-service';
+import { USERS_PANEL_ID } from '~/store/users/users-actions';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -77,6 +79,9 @@ export function configureStore(history: History, services: ServiceRepository): R
     const workflowPanelMiddleware = dataExplorerMiddleware(
         new WorkflowMiddlewareService(services, WORKFLOW_PANEL_ID)
     );
+    const userPanelMiddleware = dataExplorerMiddleware(
+        new UserMiddlewareService(services, USERS_PANEL_ID)
+    );
 
     const middlewares: Middleware[] = [
         routerMiddleware(history),
@@ -86,7 +91,8 @@ export function configureStore(history: History, services: ServiceRepository): R
         trashPanelMiddleware,
         searchResultsPanelMiddleware,
         sharedWithMePanelMiddleware,
-        workflowPanelMiddleware
+        workflowPanelMiddleware,
+        userPanelMiddleware
     ];
     const enhancer = composeEnhancers(applyMiddleware(...middlewares));
     return createStore(rootReducer, enhancer);
diff --git a/src/store/trash-panel/trash-panel-action.ts b/src/store/trash-panel/trash-panel-action.ts
index 6be9322..e17d9fa 100644
--- a/src/store/trash-panel/trash-panel-action.ts
+++ b/src/store/trash-panel/trash-panel-action.ts
@@ -2,8 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { bindDataExplorerActions } from "../data-explorer/data-explorer-action";
-import { favoritePanelActions } from "~/store/favorite-panel/favorite-panel-action";
+import { bindDataExplorerActions } from "~/store/data-explorer/data-explorer-action";
 
 export const TRASH_PANEL_ID = "trashPanel";
 export const trashPanelActions = bindDataExplorerActions(TRASH_PANEL_ID);
diff --git a/src/store/workflow-panel/workflow-middleware-service.ts b/src/store/users/user-panel-middleware-service.ts
similarity index 74%
copy from src/store/workflow-panel/workflow-middleware-service.ts
copy to src/store/users/user-panel-middleware-service.ts
index fefcb32..590e160 100644
--- a/src/store/workflow-panel/workflow-middleware-service.ts
+++ b/src/store/users/user-panel-middleware-service.ts
@@ -11,14 +11,14 @@ import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explor
 import { updateResources } from '~/store/resources/resources-actions';
 import { FilterBuilder } from '~/services/api/filter-builder';
 import { SortDirection } from '~/components/data-table/data-column';
-import { WorkflowPanelColumnNames } from '~/views/workflow-panel/workflow-panel-view';
 import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
-import { WorkflowResource } from '~/models/workflow';
 import { ListResults } from '~/services/common-service/common-resource-service';
-import { workflowPanelActions } from './workflow-panel-actions';
+import { userBindedActions } from '~/store/users/users-actions';
 import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
+import { UserResource } from '~/models/user';
+import { UserPanelColumnNames } from '~/views/user-panel/user-panel';
 
-export class WorkflowMiddlewareService extends DataExplorerMiddlewareService {
+export class UserMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
         super(id);
     }
@@ -27,11 +27,11 @@ export class WorkflowMiddlewareService extends DataExplorerMiddlewareService {
         const state = api.getState();
         const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
         try {
-            const response = await this.services.workflowService.list(getParams(dataExplorer));
+            const response = await this.services.userService.list(getParams(dataExplorer));
             api.dispatch(updateResources(response.items));
             api.dispatch(setItems(response));
         } catch {
-            api.dispatch(couldNotFetchWorkflows());
+            api.dispatch(couldNotFetchUsers());
         }
     }
 }
@@ -44,19 +44,19 @@ export const getParams = (dataExplorer: DataExplorer) => ({
 
 export const getFilters = (dataExplorer: DataExplorer) => {
     const filters = new FilterBuilder()
-        .addILike("name", dataExplorer.searchValue)
+        .addILike("firstName", dataExplorer.searchValue)
         .getFilters();
     return filters;
 };
 
 export const getOrder = (dataExplorer: DataExplorer) => {
     const sortColumn = getSortColumn(dataExplorer);
-    const order = new OrderBuilder<WorkflowResource>();
+    const order = new OrderBuilder<UserResource>();
     if (sortColumn) {
         const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
             ? OrderDirection.ASC
             : OrderDirection.DESC;
-        const columnName = sortColumn && sortColumn.name === WorkflowPanelColumnNames.NAME ? "name" : "modifiedAt";
+        const columnName = sortColumn && sortColumn.name === UserPanelColumnNames.LAST_NAME ? "lastName" : "firstName";
         return order
             .addOrder(sortDirection, columnName)
             .getOrder();
@@ -65,14 +65,14 @@ export const getOrder = (dataExplorer: DataExplorer) => {
     }
 };
 
-export const setItems = (listResults: ListResults<WorkflowResource>) =>
-    workflowPanelActions.SET_ITEMS({
+export const setItems = (listResults: ListResults<UserResource>) =>
+    userBindedActions.SET_ITEMS({
         ...listResultsToDataExplorerItemsMeta(listResults),
         items: listResults.items.map(resource => resource.uuid),
     });
 
-const couldNotFetchWorkflows = () =>
+const couldNotFetchUsers = () =>
     snackbarActions.OPEN_SNACKBAR({
-        message: 'Could not fetch workflows.',
+        message: 'Could not fetch users.',
         kind: SnackbarKind.ERROR
     });
\ No newline at end of file
diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
new file mode 100644
index 0000000..8ec373a
--- /dev/null
+++ b/src/store/users/users-actions.ts
@@ -0,0 +1,101 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { bindDataExplorerActions } from '~/store/data-explorer/data-explorer-action';
+import { RootState } from '~/store/store';
+import { ServiceRepository } from "~/services/services";
+import { navigateToUsers } from "~/store/navigation/navigation-action";
+import { unionize, ofType, UnionOf } from "~/common/unionize";
+import { dialogActions } from '~/store/dialog/dialog-actions';
+import { startSubmit, reset, stopSubmit } from "redux-form";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "~/services/common-service/common-resource-service";
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { UserResource } from "~/models/user";
+
+export const usersPanelActions = unionize({
+    SET_USERS: ofType<any>(),
+});
+
+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 const openUserAttributes = (uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const repositoryData = getState().repositories.items.find(it => it.uuid === uuid);
+        dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data: { repositoryData } }));
+    };
+
+export const openUserCreateDialog = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const userUuid = await services.authService.getUuid();
+        const user = await services.userService.get(userUuid!);
+        dispatch(reset(USER_CREATE_FORM_NAME));
+        dispatch(dialogActions.OPEN_DIALOG({ id: USER_CREATE_FORM_NAME, data: { user } }));
+    };
+
+export const createUser = (user: UserResource) =>
+    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}` });
+            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;
+        } catch (e) {
+            const error = getCommonResourceServiceError(e);
+            if (error === CommonResourceServiceError.NAME_HAS_ALREADY_BEEN_TAKEN) {
+                dispatch(stopSubmit(USER_CREATE_FORM_NAME, { name: 'User with the same name already exists.' }));
+            }
+            return undefined;
+        }
+    };
+
+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 = () =>
+    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        dispatch<any>(navigateToUsers);
+    };
+
+export const loadUsersData = () =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        const users = await services.userService.list();
+        dispatch(usersPanelActions.SET_USERS(users.items));
+    };
+
+export const loadUsersPanel = () =>
+    (dispatch: Dispatch) => {
+        dispatch(userBindedActions.REQUEST_ITEMS());
+    };
\ No newline at end of file
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 667f1c8..0d857d0 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -57,6 +57,8 @@ import { searchResultsPanelColumns } from '~/views/search-results-panel/search-r
 import { loadVirtualMachinesPanel } from '~/store/virtual-machines/virtual-machines-actions';
 import { loadRepositoriesPanel } from '~/store/repositories/repositories-actions';
 import { loadKeepServicesPanel } from '~/store/keep-services/keep-services-actions';
+import { loadUsersPanel, userBindedActions } from '~/store/users/users-actions';
+import { userPanelColumns } from '~/views/user-panel/user-panel';
 
 export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
 
@@ -76,7 +78,6 @@ const handleFirstTimeLoad = (action: any) =>
         }
     };
 
-
 export const loadWorkbench = () =>
     async (dispatch: Dispatch, getState: () => RootState) => {
         dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
@@ -91,6 +92,7 @@ export const loadWorkbench = () =>
                 dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
                 dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
                 dispatch(searchResultsPanelActions.SET_COLUMNS({ columns: searchResultsPanelColumns }));
+                dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
                 dispatch<any>(initSidePanelTree());
                 if (router.location) {
                     const match = matchRootRoute(router.location.pathname);
@@ -399,7 +401,7 @@ export const loadVirtualMachines = handleFirstTimeLoad(
         await dispatch(loadVirtualMachinesPanel());
         dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
     });
-    
+
 export const loadRepositories = handleFirstTimeLoad(
     async (dispatch: Dispatch<any>) => {
         await dispatch(loadRepositoriesPanel());
@@ -416,6 +418,12 @@ export const loadKeepServices = handleFirstTimeLoad(
         await dispatch(loadKeepServicesPanel());
     });
 
+export const loadUsers = handleFirstTimeLoad(
+    async (dispatch: Dispatch<any>) => {
+        await dispatch(loadUsersPanel());
+        dispatch(setBreadcrumbs([{ label: 'Users' }]));
+    });
+
 const finishLoadingProject = (project: GroupContentsResource | string) =>
     async (dispatch: Dispatch<any>) => {
         const uuid = typeof project === 'string' ? project : project.uuid;
diff --git a/src/store/workflow-panel/workflow-middleware-service.ts b/src/store/workflow-panel/workflow-middleware-service.ts
index fefcb32..000e9f5 100644
--- a/src/store/workflow-panel/workflow-middleware-service.ts
+++ b/src/store/workflow-panel/workflow-middleware-service.ts
@@ -15,7 +15,7 @@ import { WorkflowPanelColumnNames } from '~/views/workflow-panel/workflow-panel-
 import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
 import { WorkflowResource } from '~/models/workflow';
 import { ListResults } from '~/services/common-service/common-resource-service';
-import { workflowPanelActions } from './workflow-panel-actions';
+import { workflowPanelActions } from '~/store/workflow-panel/workflow-panel-actions';
 import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
 
 export class WorkflowMiddlewareService extends DataExplorerMiddlewareService {
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index a032b3e..20b2f9e 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -3,7 +3,7 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from 'react';
-import { Grid, Typography, withStyles, Tooltip, IconButton } from '@material-ui/core';
+import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
 import { FavoriteStar } from '../favorite-star/favorite-star';
 import { ResourceKind, TrashableResource } from '~/models/resource';
 import { ProjectIcon, CollectionIcon, ProcessIcon, DefaultIcon, WorkflowIcon, ShareIcon } from '~/components/icon/icon';
@@ -21,8 +21,9 @@ import { ResourceStatus } from '~/views/workflow-panel/workflow-panel-view';
 import { getUuidPrefix, openRunProcess } from '~/store/workflow-panel/workflow-panel-actions';
 import { getResourceData } from "~/store/resources-data/resources-data";
 import { openSharingDialog } from '~/store/sharing-dialog/sharing-dialog-actions';
+import { UserResource } from '~/models/user';
 
-export const renderName = (item: { name: string; uuid: string, kind: string }) =>
+const renderName = (item: { name: string; uuid: string, kind: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item)}
@@ -45,7 +46,7 @@ export const ResourceName = connect(
         return resource || { name: '', uuid: '', kind: '' };
     })(renderName);
 
-export const renderIcon = (item: { kind: string }) => {
+const renderIcon = (item: { kind: string }) => {
     switch (item.kind) {
         case ResourceKind.PROJECT:
             return <ProjectIcon />;
@@ -60,11 +61,11 @@ export const renderIcon = (item: { kind: string }) => {
     }
 };
 
-export const renderDate = (date?: string) => {
+const renderDate = (date?: string) => {
     return <Typography noWrap style={{ minWidth: '100px' }}>{formatDate(date)}</Typography>;
 };
 
-export const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ownerUuid: string }) =>
+const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ownerUuid: string }) =>
     <Grid container alignItems="center" wrap="nowrap" spacing={16}>
         <Grid item>
             {renderIcon(item)}
@@ -86,7 +87,7 @@ const getPublicUuid = (uuidPrefix: string) => {
     return `${uuidPrefix}-tpzed-anonymouspublic`;
 };
 
-export const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
+const resourceShare = (dispatch: Dispatch, uuidPrefix: string, ownerUuid?: string, uuid?: string) => {
     const isPublic = ownerUuid === getPublicUuid(uuidPrefix);
     return (
         <div>
@@ -113,7 +114,77 @@ export const ResourceShare = connect(
     })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp<any>) =>
         resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid));
 
-export const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
+const renderFirstName = (item: { firstName: string }) => {
+    return <Typography noWrap>{item.firstName}</Typography>;
+};
+
+export const ResourceFirstName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { firstName: '' };
+    })(renderFirstName);
+
+const renderLastName = (item: { lastName: string }) =>
+    <Typography noWrap>{item.lastName}</Typography>;
+
+export const ResourceLastName = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { lastName: '' };
+    })(renderLastName);
+
+const renderUuid = (item: { uuid: string }) =>
+    <Typography noWrap>{item.uuid}</Typography>;
+
+export const ResourceUuid = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { uuid: '' };
+    })(renderUuid);
+
+const renderEmail = (item: { email: string }) =>
+    <Typography noWrap>{item.email}</Typography>;
+
+export const ResourceEmail = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { email: '' };
+    })(renderEmail);
+
+const renderIsActive = (item: { isActive: boolean }) =>
+    <Checkbox
+        disableRipple
+        color="primary"
+        checked={item.isActive} />;
+
+export const ResourceIsActive = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { isActive: false };
+    })(renderIsActive);
+
+const renderIsAdmin = (item: { isAdmin: boolean }) =>
+    <Checkbox
+        disableRipple
+        color="primary"
+        checked={item.isAdmin} />;
+
+export const ResourceIsAdmin = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { isAdmin: false };
+    })(renderIsAdmin);
+
+const renderUsername = (item: { username: string }) =>
+    <Typography noWrap>{item.username}</Typography>;
+
+export const ResourceUsername = connect(
+    (state: RootState, props: { uuid: string }) => {
+        const resource = getResource<UserResource>(props.uuid)(state.resources);
+        return resource || { username: '' };
+    })(renderUsername);
+
+const resourceRunProcess = (dispatch: Dispatch, uuid: string) => {
     return (
         <div>
             {uuid &&
@@ -135,7 +206,7 @@ export const ResourceRunProcess = connect(
     })((props: { uuid: string } & DispatchProp<any>) =>
         resourceRunProcess(props.dispatch, props.uuid));
 
-export const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
+const renderWorkflowStatus = (uuidPrefix: string, ownerUuid?: string) => {
     if (ownerUuid === getPublicUuid(uuidPrefix)) {
         return renderStatus(ResourceStatus.PUBLIC);
     } else {
@@ -185,7 +256,7 @@ export const ResourceFileSize = connect(
         return { fileSize: resource ? resource.fileSize : 0 };
     })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
 
-export const renderOwner = (owner: string) =>
+const renderOwner = (owner: string) =>
     <Typography noWrap color="primary" >
         {owner}
     </Typography>;
@@ -196,7 +267,7 @@ export const ResourceOwner = connect(
         return { owner: resource ? resource.ownerUuid : '' };
     })((props: { owner: string }) => renderOwner(props.owner));
 
-export const renderType = (type: string) =>
+const renderType = (type: string) =>
     <Typography noWrap>
         {resourceLabel(type)}
     </Typography>;
diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx
index 075aa69..889b51d 100644
--- a/src/views-components/main-app-bar/account-menu.tsx
+++ b/src/views-components/main-app-bar/account-menu.tsx
@@ -14,6 +14,7 @@ import { openCurrentTokenDialog } from '~/store/current-token-dialog/current-tok
 import { openRepositoriesPanel } from "~/store/repositories/repositories-actions";
 import { navigateToSshKeys, navigateToKeepServices } from '~/store/navigation/navigation-action';
 import { openVirtualMachines } from "~/store/virtual-machines/virtual-machines-actions";
+import { navigateToUsers } from '~/store/navigation/navigation-action';
 
 interface AccountMenuProps {
     user?: User;
@@ -37,7 +38,8 @@ export const AccountMenu = connect(mapStateToProps)(
                 <MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
                 <MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
                 <MenuItem onClick={() => dispatch(navigateToSshKeys)}>Ssh Keys</MenuItem>
-                { user.isAdmin && <MenuItem onClick={() => dispatch(navigateToKeepServices)}>Keep Services</MenuItem> }
+                <MenuItem onClick={() => dispatch(navigateToUsers)}>Users</MenuItem>
+                {user.isAdmin && <MenuItem onClick={() => dispatch(navigateToKeepServices)}>Keep Services</MenuItem>}
                 <MenuItem>My account</MenuItem>
                 <MenuItem onClick={() => dispatch(logout())}>Logout</MenuItem>
             </DropdownMenu>
diff --git a/src/views-components/main-content-bar/main-content-bar.tsx b/src/views-components/main-content-bar/main-content-bar.tsx
index 66d7cab..fe68145 100644
--- a/src/views-components/main-content-bar/main-content-bar.tsx
+++ b/src/views-components/main-content-bar/main-content-bar.tsx
@@ -8,7 +8,7 @@ import { DetailsIcon } from "~/components/icon/icon";
 import { Breadcrumbs } from "~/views-components/breadcrumbs/breadcrumbs";
 import { connect } from 'react-redux';
 import { RootState } from '~/store/store';
-import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute, matchKeepServicesRoute } from '~/routes/routes';
+import { matchWorkflowRoute, matchSshKeysRoute, matchRepositoriesRoute, matchVirtualMachineRoute, matchKeepServicesRoute, matchUsersRoute } from '~/routes/routes';
 import { toggleDetailsPanel } from '~/store/details-panel/details-panel-action';
 
 interface MainContentBarProps {
@@ -19,7 +19,8 @@ interface MainContentBarProps {
 const isButtonVisible = ({ router }: RootState) => {
     const pathname = router.location ? router.location.pathname : '';
     return !matchWorkflowRoute(pathname) && !matchVirtualMachineRoute(pathname) &&
-        !matchRepositoriesRoute(pathname) && !matchSshKeysRoute(pathname) && !matchKeepServicesRoute(pathname);
+        !matchRepositoriesRoute(pathname) && !matchSshKeysRoute(pathname) && !matchKeepServicesRoute(pathname) &&
+        !matchUsersRoute(pathname);
 };
 
 export const MainContentBar = connect((state: RootState) => ({
diff --git a/src/views/search-results-panel/search-results-panel-view.tsx b/src/views/search-results-panel/search-results-panel-view.tsx
index ea658ee..7bfc2bf 100644
--- a/src/views/search-results-panel/search-results-panel-view.tsx
+++ b/src/views/search-results-panel/search-results-panel-view.tsx
@@ -8,7 +8,6 @@ import { DataColumns } from '~/components/data-table/data-table';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { ResourceKind } from '~/models/resource';
 import { ContainerRequestState } from '~/models/container-request';
-import { resourceLabel } from '~/common/labels';
 import { SearchBarAdvanceFormData } from '~/models/search-bar';
 import { SEARCH_RESULTS_PANEL_ID } from '~/store/search-results-panel/search-results-panel-actions';
 import { DataExplorer } from '~/views-components/data-explorer/data-explorer';
@@ -21,8 +20,8 @@ import {
     ResourceType
 } from '~/views-components/data-explorer/renderers';
 import { createTree } from '~/models/tree';
-import { getInitialResourceTypeFilters } from '../../store/resource-type-filters/resource-type-filters';
-// TODO: code clean up
+import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
+
 export enum SearchResultsPanelColumnNames {
     NAME = "Name",
     PROJECT = "Project",
diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx
index ae12425..bcc6611 100644
--- a/src/views/trash-panel/trash-panel.tsx
+++ b/src/views/trash-panel/trash-panel.tsx
@@ -11,7 +11,6 @@ import { RootState } from '~/store/store';
 import { DataTableFilterItem } from '~/components/data-table-filters/data-table-filters';
 import { SortDirection } from '~/components/data-table/data-column';
 import { ResourceKind, TrashableResource } from '~/models/resource';
-import { resourceLabel } from '~/common/labels';
 import { ArvadosTheme } from '~/common/custom-theme';
 import { RestoreFromTrashIcon, TrashIcon } from '~/components/icon/icon';
 import { TRASH_PANEL_ID } from "~/store/trash-panel/trash-panel-action";
@@ -31,11 +30,10 @@ import { loadDetailsPanel } from "~/store/details-panel/details-panel-action";
 import { toggleTrashed } from "~/store/trash/trash-actions";
 import { ContextMenuKind } from "~/views-components/context-menu/context-menu";
 import { Dispatch } from "redux";
-import { PanelDefaultView } from '~/components/panel-default-view/panel-default-view';
 import { DataTableDefaultView } from '~/components/data-table-default-view/data-table-default-view';
 import { createTree } from '~/models/tree';
-import { getInitialResourceTypeFilters } from '../../store/resource-type-filters/resource-type-filters';
-// TODO: code clean up
+import { getInitialResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
+
 type CssRules = "toolbar" | "button";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
new file mode 100644
index 0000000..4b9a339
--- /dev/null
+++ b/src/views/user-panel/user-panel.tsx
@@ -0,0 +1,172 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WithStyles, withStyles, Typography } 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';
+import { RootState } from '~/store/store';
+import { SortDirection } from '~/components/data-table/data-column';
+import { openContextMenu } from "~/store/context-menu/context-menu-actions";
+import { getResource, ResourcesState } from "~/store/resources/resources";
+import {
+    ResourceFirstName,
+    ResourceLastName,
+    ResourceUuid,
+    ResourceEmail,
+    ResourceIsActive,
+    ResourceIsAdmin,
+    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 { UserResource } from '~/models/user';
+import { ShareMeIcon } from '~/components/icon/icon';
+import { USERS_PANEL_ID } from '~/store/users/users-actions';
+
+type UserPanelRules = "toolbar" | "button";
+
+const styles = withStyles<UserPanelRules>(theme => ({
+    toolbar: {
+        paddingBottom: theme.spacing.unit * 3,
+        textAlign: "right"
+    },
+    button: {
+        marginLeft: theme.spacing.unit
+    },
+}));
+
+export enum UserPanelColumnNames {
+    FIRST_NAME = "First Name",
+    LAST_NAME = "Last Name",
+    UUID = "Uuid",
+    EMAIL = "Email",
+    ACTIVE = "Active",
+    ADMIN = "Admin",
+    REDIRECT_TO_USER = "Redirect to user",
+    USERNAME = "Username"
+}
+
+export const userPanelColumns: DataColumns<string> = [
+    {
+        name: UserPanelColumnNames.FIRST_NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceFirstName uuid={uuid} />
+    },
+    {
+        name: UserPanelColumnNames.LAST_NAME,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceLastName uuid={uuid} />
+    },
+    {
+        name: UserPanelColumnNames.UUID,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceUuid uuid={uuid} />
+    },
+    {
+        name: UserPanelColumnNames.EMAIL,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceEmail uuid={uuid} />
+    },
+    {
+        name: UserPanelColumnNames.ACTIVE,
+        selected: true,
+        configurable: true,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceIsActive uuid={uuid} />
+    },
+    {
+        name: UserPanelColumnNames.ADMIN,
+        selected: true,
+        configurable: false,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceIsAdmin uuid={uuid} />
+    },
+    {
+        name: UserPanelColumnNames.REDIRECT_TO_USER,
+        selected: true,
+        configurable: false,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: () => <Typography noWrap>(none)</Typography>
+    },
+    {
+        name: UserPanelColumnNames.USERNAME,
+        selected: true,
+        configurable: false,
+        sortDirection: SortDirection.NONE,
+        filters: createTree(),
+        render: uuid => <ResourceUsername uuid={uuid} />
+    }
+];
+
+interface UserPanelDataProps {
+    resources: ResourcesState;
+}
+
+type UserPanelProps = UserPanelDataProps & DispatchProp & WithStyles<UserPanelRules>;
+
+export const UserPanel = compose(
+    styles,
+    connect((state: RootState) => ({
+        resources: state.resources
+    })))(
+        class extends React.Component<UserPanelProps> {
+            render() {
+                console.log(this.props.resources);
+                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.']} />
+                    } />;
+            }
+
+            handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+                const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
+                if (resource) {
+                    this.props.dispatch<any>(openContextMenu(event, {
+                        name: '',
+                        uuid: resource.uuid,
+                        ownerUuid: resource.ownerUuid,
+                        kind: resource.kind,
+                        menuKind: ContextMenuKind.TRASH
+                    }));
+                }
+            }
+
+            handleRowDoubleClick = (uuid: string) => {
+                this.props.dispatch<any>(navigateTo(uuid));
+            }
+
+            handleRowClick = (uuid: string) => {
+                this.props.dispatch(loadDetailsPanel(uuid));
+            }
+        }
+    );
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 2d17fad..16032c4 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -64,6 +64,7 @@ import { AttributesKeepServiceDialog } from '~/views-components/keep-services-di
 import { AttributesSshKeyDialog } from '~/views-components/ssh-keys-dialog/attributes-dialog';
 import { VirtualMachineAttributesDialog } from '~/views-components/virtual-machines-dialog/attributes-dialog';
 import { RemoveVirtualMachineDialog } from '~/views-components/virtual-machines-dialog/remove-dialog';
+import { UserPanel } from '~/views/user-panel/user-panel';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -137,6 +138,7 @@ export const WorkbenchPanel =
                                 <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
                                 <Route path={Routes.SSH_KEYS} component={SshKeyPanel} />
                                 <Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
+                                <Route path={Routes.USERS} component={UserPanel} />
                             </Switch>
                         </Grid>
                     </Grid>

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list