user-admin-panel-init
authorPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 4 Dec 2018 15:44:11 +0000 (16:44 +0100)
committerPawel Kowalczyk <pawel.kowalczyk@contractors.roche.com>
Tue, 4 Dec 2018 15:44:11 +0000 (16:44 +0100)
Feature #14504

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

19 files changed:
src/components/data-explorer/data-explorer.tsx
src/models/user.ts
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/store/navigation/navigation-action.ts
src/store/repositories/repositories-actions.ts
src/store/store.ts
src/store/trash-panel/trash-panel-action.ts
src/store/users/user-panel-middleware-service.ts [new file with mode: 0644]
src/store/users/users-actions.ts [new file with mode: 0644]
src/store/workbench/workbench-actions.ts
src/store/workflow-panel/workflow-middleware-service.ts
src/views-components/data-explorer/renderers.tsx
src/views-components/main-app-bar/account-menu.tsx
src/views-components/main-content-bar/main-content-bar.tsx
src/views/search-results-panel/search-results-panel-view.tsx
src/views/trash-panel/trash-panel.tsx
src/views/user-panel/user-panel.tsx [new file with mode: 0644]
src/views/workbench/workbench.tsx

index cb979c7bd216b31d7e7d6760c08da9471a159472..3b09b5baff4b98ecab042e80a1e998992674bf5a 100644 (file)
@@ -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
index 9f9c534763ca86ee40190c2361c89b4e81da2f95..b0f004c31e2ad4d450628b3c96c59d7ff27d3545 100644 (file)
@@ -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
index fdc4211fd60bacd52020ad84bc3258a1c02defe0..1cea993f59761d36aed45939b491dc7b800e9300 100644 (file)
@@ -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);
     }
 };
index 5cd3e559965202a8e8d87fd0d6d2acc66e702d94..2c4337df95a5c2a41d919a2c9cb2669e42b4ae2d 100644 (file)
@@ -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 });
index d452710cf561f298e65a8bea2a1c1395392c3738..4ee22108eb9fdb0e9f16217345144b0b73f2b461 100644 (file)
@@ -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
index 61caa769f1ea32746608f3a4f570a86ddbb4db38..a8b75ac1ee6ce7fcb4c4803e8c3c27ae8c83f40c 100644 (file)
@@ -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);
     };
 
index f8bdcc24c6efee4f3b6811a05fb48b86c3cb1ee1..d04775fba6ab0af6a6fe5e82bd3f1c4f7c8bfa81 100644 (file)
@@ -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);
index 6be93228be73d438d5b2c8c0f00dd0ba5c46c1ec..e17d9faea5d0e35febddca034fd39b8d950e3afa 100644 (file)
@@ -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/users/user-panel-middleware-service.ts b/src/store/users/user-panel-middleware-service.ts
new file mode 100644 (file)
index 0000000..590e160
--- /dev/null
@@ -0,0 +1,78 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository } from '~/services/services';
+import { MiddlewareAPI, Dispatch } from 'redux';
+import { DataExplorerMiddlewareService, dataExplorerToListParams, listResultsToDataExplorerItemsMeta } from '~/store/data-explorer/data-explorer-middleware-service';
+import { RootState } from '~/store/store';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
+import { updateResources } from '~/store/resources/resources-actions';
+import { FilterBuilder } from '~/services/api/filter-builder';
+import { SortDirection } from '~/components/data-table/data-column';
+import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
+import { ListResults } from '~/services/common-service/common-resource-service';
+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 UserMiddlewareService extends DataExplorerMiddlewareService {
+    constructor(private services: ServiceRepository, id: string) {
+        super(id);
+    }
+
+    async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+        const state = api.getState();
+        const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
+        try {
+            const response = await this.services.userService.list(getParams(dataExplorer));
+            api.dispatch(updateResources(response.items));
+            api.dispatch(setItems(response));
+        } catch {
+            api.dispatch(couldNotFetchUsers());
+        }
+    }
+}
+
+export const getParams = (dataExplorer: DataExplorer) => ({
+    ...dataExplorerToListParams(dataExplorer),
+    order: getOrder(dataExplorer),
+    filters: getFilters(dataExplorer)
+});
+
+export const getFilters = (dataExplorer: DataExplorer) => {
+    const filters = new FilterBuilder()
+        .addILike("firstName", dataExplorer.searchValue)
+        .getFilters();
+    return filters;
+};
+
+export const getOrder = (dataExplorer: DataExplorer) => {
+    const sortColumn = getSortColumn(dataExplorer);
+    const order = new OrderBuilder<UserResource>();
+    if (sortColumn) {
+        const sortDirection = sortColumn && sortColumn.sortDirection === SortDirection.ASC
+            ? OrderDirection.ASC
+            : OrderDirection.DESC;
+        const columnName = sortColumn && sortColumn.name === UserPanelColumnNames.LAST_NAME ? "lastName" : "firstName";
+        return order
+            .addOrder(sortDirection, columnName)
+            .getOrder();
+    } else {
+        return order.getOrder();
+    }
+};
+
+export const setItems = (listResults: ListResults<UserResource>) =>
+    userBindedActions.SET_ITEMS({
+        ...listResultsToDataExplorerItemsMeta(listResults),
+        items: listResults.items.map(resource => resource.uuid),
+    });
+
+const couldNotFetchUsers = () =>
+    snackbarActions.OPEN_SNACKBAR({
+        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 (file)
index 0000000..8ec373a
--- /dev/null
@@ -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
index 667f1c8047451853334f508d0737c9bf019fbe4d..0d857d0988c1d13a00c78095148fea08945177ce 100644 (file)
@@ -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;
index fefcb325e24dc8fe406c2b92545309e5e33db11d..000e9f5cb15a7e01fec16920fd01942259a015bd 100644 (file)
@@ -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 {
index a032b3ed5a22945e952d20767afd924fb931d4bd..20b2f9ec11ef08497c1f44b255b91a25651cc0ef 100644 (file)
@@ -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>;
index 075aa69a472347f2abc63c4b491a54fcaecaea85..889b51df068d524e658cfba0a46fe16f32d83ade 100644 (file)
@@ -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>
index 66d7cabc88bd3dfe6bf637cf5a4713d9a81bd759..fe681451a7353da2042a635db364bcfca54dd567 100644 (file)
@@ -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) => ({
index ea658ee72573c1e6554a6053b4e75ae96c474295..7bfc2bfef6caa70aa952f654a8d15423dd234d8a 100644 (file)
@@ -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",
index ae12425e1516a1dde5e05b31624a2537b97c5704..bcc661144fe64f0f0aee9faf0499a7d2c3ff06b4 100644 (file)
@@ -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 (file)
index 0000000..4b9a339
--- /dev/null
@@ -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));
+            }
+        }
+    );
index 2d17fad9629d80aabdf7c493155012c33bc38302..16032c4ec58409447401e70d742f805e6da381a4 100644 (file)
@@ -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>