From dc95b803fa84b3c9ef7c11a4f81dd0d86077d779 Mon Sep 17 00:00:00 2001 From: Pawel Kowalczyk Date: Tue, 4 Dec 2018 16:44:11 +0100 Subject: [PATCH] user-admin-panel-init Feature #14504 Arvados-DCO-1.1-Signed-off-by: Pawel Kowalczyk --- .../data-explorer/data-explorer.tsx | 7 +- src/models/user.ts | 12 +- src/routes/route-change-handlers.ts | 7 +- src/routes/routes.ts | 10 +- src/store/navigation/navigation-action.ts | 4 +- .../repositories/repositories-actions.ts | 2 +- src/store/store.ts | 8 +- src/store/trash-panel/trash-panel-action.ts | 3 +- .../users/user-panel-middleware-service.ts | 78 ++++++++ src/store/users/users-actions.ts | 101 ++++++++++ src/store/workbench/workbench-actions.ts | 12 +- .../workflow-middleware-service.ts | 2 +- .../data-explorer/renderers.tsx | 91 ++++++++- .../main-app-bar/account-menu.tsx | 4 +- .../main-content-bar/main-content-bar.tsx | 5 +- .../search-results-panel-view.tsx | 5 +- src/views/trash-panel/trash-panel.tsx | 6 +- src/views/user-panel/user-panel.tsx | 172 ++++++++++++++++++ src/views/workbench/workbench.tsx | 2 + 19 files changed, 494 insertions(+), 37 deletions(-) create mode 100644 src/store/users/user-panel-middleware-service.ts create mode 100644 src/store/users/users-actions.ts create mode 100644 src/views/user-panel/user-panel.tsx diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index cb979c7b..3b09b5ba 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -44,6 +44,7 @@ interface DataExplorerDataProps { contextMenuColumn: boolean; dataTableDefaultView?: React.ReactNode; working?: boolean; + isColumnSelectorHidden?: boolean; } interface DataExplorerActionProps { @@ -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 @@ -84,9 +85,9 @@ export const DataExplorer = withStyles(styles)( value={searchValue} onSearch={onSearch} /> - + onColumnToggle={onColumnToggle} />} ({ 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 5cd3e559..2c4337df 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(route, { path: Routes.VIRTUAL_MACHINES }); - + export const matchRepositoriesRoute = (route: string) => matchPath(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 d452710c..4ee22108 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 61caa769..a8b75ac1 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(navigateToRepositories); }; diff --git a/src/store/store.ts b/src/store/store.ts index f8bdcc24..d04775fb 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 6be93228..e17d9fae 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/users/user-panel-middleware-service.ts b/src/store/users/user-panel-middleware-service.ts new file mode 100644 index 00000000..590e160c --- /dev/null +++ b/src/store/users/user-panel-middleware-service.ts @@ -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) { + 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(); + 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) => + 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 index 00000000..8ec373a8 --- /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(), +}); + +export type UsersActions = UnionOf; + +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(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(loadUsersData()); + }; + +export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID); + +export const openUsersPanel = () => + (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + dispatch(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 667f1c80..0d857d09 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(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) => { await dispatch(loadRepositoriesPanel()); @@ -416,6 +418,12 @@ export const loadKeepServices = handleFirstTimeLoad( await dispatch(loadKeepServicesPanel()); }); +export const loadUsers = handleFirstTimeLoad( + async (dispatch: Dispatch) => { + await dispatch(loadUsersPanel()); + dispatch(setBreadcrumbs([{ label: 'Users' }])); + }); + const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch) => { 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 fefcb325..000e9f5c 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 a032b3ed..20b2f9ec 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 }) => {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 ; @@ -60,11 +61,11 @@ export const renderIcon = (item: { kind: string }) => { } }; -export const renderDate = (date?: string) => { +const renderDate = (date?: string) => { return {formatDate(date)}; }; -export const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ownerUuid: string }) => +const renderWorkflowName = (item: { name: string; uuid: string, kind: string, ownerUuid: string }) => {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 (
@@ -113,7 +114,77 @@ export const ResourceShare = connect( })((props: { ownerUuid?: string, uuidPrefix: string, uuid?: string } & DispatchProp) => resourceShare(props.dispatch, props.uuidPrefix, props.ownerUuid, props.uuid)); -export const resourceRunProcess = (dispatch: Dispatch, uuid: string) => { +const renderFirstName = (item: { firstName: string }) => { + return {item.firstName}; +}; + +export const ResourceFirstName = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { firstName: '' }; + })(renderFirstName); + +const renderLastName = (item: { lastName: string }) => + {item.lastName}; + +export const ResourceLastName = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { lastName: '' }; + })(renderLastName); + +const renderUuid = (item: { uuid: string }) => + {item.uuid}; + +export const ResourceUuid = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { uuid: '' }; + })(renderUuid); + +const renderEmail = (item: { email: string }) => + {item.email}; + +export const ResourceEmail = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { email: '' }; + })(renderEmail); + +const renderIsActive = (item: { isActive: boolean }) => + ; + +export const ResourceIsActive = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { isActive: false }; + })(renderIsActive); + +const renderIsAdmin = (item: { isAdmin: boolean }) => + ; + +export const ResourceIsAdmin = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { isAdmin: false }; + })(renderIsAdmin); + +const renderUsername = (item: { username: string }) => + {item.username}; + +export const ResourceUsername = connect( + (state: RootState, props: { uuid: string }) => { + const resource = getResource(props.uuid)(state.resources); + return resource || { username: '' }; + })(renderUsername); + +const resourceRunProcess = (dispatch: Dispatch, uuid: string) => { return (
{uuid && @@ -135,7 +206,7 @@ export const ResourceRunProcess = connect( })((props: { uuid: string } & DispatchProp) => 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) => {owner} ; @@ -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) => {resourceLabel(type)} ; diff --git a/src/views-components/main-app-bar/account-menu.tsx b/src/views-components/main-app-bar/account-menu.tsx index 075aa69a..889b51df 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)( dispatch(openRepositoriesPanel())}>Repositories dispatch(openCurrentTokenDialog)}>Current token dispatch(navigateToSshKeys)}>Ssh Keys - { user.isAdmin && dispatch(navigateToKeepServices)}>Keep Services } + dispatch(navigateToUsers)}>Users + {user.isAdmin && dispatch(navigateToKeepServices)}>Keep Services} My account dispatch(logout())}>Logout 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 66d7cabc..fe681451 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 ea658ee7..7bfc2bfe 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 ae12425e..bcc66114 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 = (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 00000000..4b9a3396 --- /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(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 = [ + { + name: UserPanelColumnNames.FIRST_NAME, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: UserPanelColumnNames.LAST_NAME, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: UserPanelColumnNames.UUID, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: UserPanelColumnNames.EMAIL, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: UserPanelColumnNames.ACTIVE, + selected: true, + configurable: true, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: UserPanelColumnNames.ADMIN, + selected: true, + configurable: false, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + }, + { + name: UserPanelColumnNames.REDIRECT_TO_USER, + selected: true, + configurable: false, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: () => (none) + }, + { + name: UserPanelColumnNames.USERNAME, + selected: true, + configurable: false, + sortDirection: SortDirection.NONE, + filters: createTree(), + render: uuid => + } +]; + +interface UserPanelDataProps { + resources: ResourcesState; +} + +type UserPanelProps = UserPanelDataProps & DispatchProp & WithStyles; + +export const UserPanel = compose( + styles, + connect((state: RootState) => ({ + resources: state.resources + })))( + class extends React.Component { + render() { + console.log(this.props.resources); + return + } />; + } + + handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { + const resource = getResource(resourceUuid)(this.props.resources); + if (resource) { + this.props.dispatch(openContextMenu(event, { + name: '', + uuid: resource.uuid, + ownerUuid: resource.ownerUuid, + kind: resource.kind, + menuKind: ContextMenuKind.TRASH + })); + } + } + + handleRowDoubleClick = (uuid: string) => { + this.props.dispatch(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 2d17fad9..16032c4e 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 = + -- 2.30.2