[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