import * as React from 'react';
import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip } from '@material-ui/core';
-import { ColumnSelector } from "../column-selector/column-selector";
-import { DataTable, DataColumns } from "../data-table/data-table";
-import { DataColumn, SortDirection } from "../data-table/data-column";
-import { SearchInput } from '../search-input/search-input';
+import { ColumnSelector } from "~/components/column-selector/column-selector";
+import { DataTable, DataColumns } from "~/components/data-table/data-table";
+import { DataColumn, SortDirection } from "~/components/data-table/data-column";
+import { SearchInput } from '~/components/search-input/search-input';
import { ArvadosTheme } from "~/common/custom-theme";
import { createTree } from '~/models/tree';
-import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
+import { DataTableFilters } from '~/components/data-table-filters/data-table-filters-tree';
import { MoreOptionsIcon } from '~/components/icon/icon';
-type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton';
+type CssRules = 'searchBox' | "toolbar" | "footer" | "root" | 'moreOptionsButton' | 'rootUserPanel';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
searchBox: {
root: {
height: '100%'
},
+ rootUserPanel: {
+ height: '100%',
+ boxShadow: 'none'
+ },
moreOptionsButton: {
padding: 0
}
contextMenuColumn: boolean;
dataTableDefaultView?: React.ReactNode;
working?: boolean;
+ isUserPanel?: boolean;
}
interface DataExplorerActionProps<T> {
columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey,
rowsPerPage, rowsPerPageOptions, onColumnToggle, searchValue, onSearch,
items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
- dataTableDefaultView
+ dataTableDefaultView, isUserPanel
} = this.props;
- return <Paper className={classes.root}>
- <Toolbar className={classes.toolbar}>
+ return <Paper className={!isUserPanel ? classes.root : classes.rootUserPanel}>
+ <Toolbar className={!isUserPanel ? classes.toolbar : ''}>
<Grid container justify="space-between" wrap="nowrap" alignItems="center">
<div className={classes.searchBox}>
<SearchInput
value={searchValue}
onSearch={onSearch} />
</div>
- <ColumnSelector
+ {!isUserPanel && <ColumnSelector
columns={columns}
- onColumnToggle={onColumnToggle} />
+ onColumnToggle={onColumnToggle} />}
</Grid>
</Toolbar>
<DataTable
import * as React from 'react';
import * as ReactDOM from 'react-dom';
import { Provider } from "react-redux";
-import { MainPanel } from './views/main-panel/main-panel';
-import './index.css';
+import { MainPanel } from '~/views/main-panel/main-panel';
+import '~/index.css';
import { Route, Switch } from 'react-router';
import createBrowserHistory from "history/createBrowserHistory";
import { History } from "history";
-import { configureStore, RootStore } from './store/store';
+import { configureStore, RootStore } from '~/store/store';
import { ConnectedRouter } from "react-router-redux";
-import { ApiToken } from "./views-components/api-token/api-token";
-import { initAuth } from "./store/auth/auth-action";
-import { createServices } from "./services/services";
+import { ApiToken } from "~/views-components/api-token/api-token";
+import { initAuth } from "~/store/auth/auth-action";
+import { createServices } from "~/services/services";
import { MuiThemeProvider } from '@material-ui/core/styles';
-import { CustomTheme } from './common/custom-theme';
-import { fetchConfig } from './common/config';
-import { addMenuActionSet, ContextMenuKind } from './views-components/context-menu/context-menu';
-import { rootProjectActionSet } from "./views-components/context-menu/action-sets/root-project-action-set";
-import { projectActionSet } from "./views-components/context-menu/action-sets/project-action-set";
-import { resourceActionSet } from './views-components/context-menu/action-sets/resource-action-set';
-import { favoriteActionSet } from "./views-components/context-menu/action-sets/favorite-action-set";
-import { collectionFilesActionSet } from './views-components/context-menu/action-sets/collection-files-action-set';
-import { collectionFilesItemActionSet } from './views-components/context-menu/action-sets/collection-files-item-action-set';
-import { collectionFilesNotSelectedActionSet } from './views-components/context-menu/action-sets/collection-files-not-selected-action-set';
-import { collectionActionSet } from './views-components/context-menu/action-sets/collection-action-set';
-import { collectionResourceActionSet } from './views-components/context-menu/action-sets/collection-resource-action-set';
-import { processActionSet } from './views-components/context-menu/action-sets/process-action-set';
-import { loadWorkbench } from './store/workbench/workbench-actions';
+import { CustomTheme } from '~/common/custom-theme';
+import { fetchConfig } from '~/common/config';
+import { addMenuActionSet, ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { rootProjectActionSet } from "~/views-components/context-menu/action-sets/root-project-action-set";
+import { projectActionSet } from "~/views-components/context-menu/action-sets/project-action-set";
+import { resourceActionSet } from '~/views-components/context-menu/action-sets/resource-action-set';
+import { favoriteActionSet } from "~/views-components/context-menu/action-sets/favorite-action-set";
+import { collectionFilesActionSet } from '~/views-components/context-menu/action-sets/collection-files-action-set';
+import { collectionFilesItemActionSet } from '~/views-components/context-menu/action-sets/collection-files-item-action-set';
+import { collectionFilesNotSelectedActionSet } from '~/views-components/context-menu/action-sets/collection-files-not-selected-action-set';
+import { collectionActionSet } from '~/views-components/context-menu/action-sets/collection-action-set';
+import { collectionResourceActionSet } from '~/views-components/context-menu/action-sets/collection-resource-action-set';
+import { processActionSet } from '~/views-components/context-menu/action-sets/process-action-set';
+import { loadWorkbench } from '~/store/workbench/workbench-actions';
import { Routes } from '~/routes/routes';
import { trashActionSet } from "~/views-components/context-menu/action-sets/trash-action-set";
import { ServiceRepository } from '~/services/services';
import { keepServiceActionSet } from '~/views-components/context-menu/action-sets/keep-service-action-set';
import { loadVocabulary } from '~/store/vocabulary/vocabulary-actions';
import { virtualMachineActionSet } from '~/views-components/context-menu/action-sets/virtual-machine-action-set';
+import { userActionSet } from '~/views-components/context-menu/action-sets/user-action-set';
import { computeNodeActionSet } from '~/views-components/context-menu/action-sets/compute-node-action-set';
import { apiClientAuthorizationActionSet } from '~/views-components/context-menu/action-sets/api-client-authorization-action-set';
addMenuActionSet(ContextMenuKind.SSH_KEY, sshKeyActionSet);
addMenuActionSet(ContextMenuKind.VIRTUAL_MACHINE, virtualMachineActionSet);
addMenuActionSet(ContextMenuKind.KEEP_SERVICE, keepServiceActionSet);
+addMenuActionSet(ContextMenuKind.USER, userActionSet);
addMenuActionSet(ContextMenuKind.NODE, computeNodeActionSet);
addMenuActionSet(ContextMenuKind.API_CLIENT_AUTHORIZATION, apiClientAuthorizationActionSet);
lastName: string;
identityUrl: string;
isAdmin: boolean;
- prefs: string;
+ prefs: UserPrefs;
defaultOwnerUuid: string;
isActive: boolean;
writableBy: string[];
const computeNodesMatch = Routes.matchComputeNodesRoute(pathname);
const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
const myAccountMatch = Routes.matchMyAccountRoute(pathname);
+ const userMatch = Routes.matchUsersRoute(pathname);
if (projectMatch) {
store.dispatch(WorkbenchActions.loadProject(projectMatch.params.id));
store.dispatch(WorkbenchActions.loadApiClientAuthorizations);
} else if (myAccountMatch) {
store.dispatch(WorkbenchActions.loadMyAccount);
+ }else if (userMatch) {
+ store.dispatch(WorkbenchActions.loadUsers);
}
};
MY_ACCOUNT: '/my-account',
KEEP_SERVICES: `/keep-services`,
COMPUTE_NODES: `/nodes`,
+ USERS: '/users',
API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations`
};
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 });
+
export const matchComputeNodesRoute = (route: string) =>
matchPath(route, { path: Routes.COMPUTE_NODES });
export const matchApiClientAuthorizationsRoute = (route: string) =>
- matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS });
\ No newline at end of file
+ matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS });
VIRTUAL_MACHINES = 'virtual_machines',
KEEP_SERVICES = 'keep_services',
COMPUTE_NODES = 'nodes',
+ USERS = 'users',
API_CLIENT_AUTHORIZATIONS = 'api_client_authorizations'
}
CREATED_AT = 'created_at'
}
+enum UserData {
+ USER = 'user',
+ USERNAME = 'username'
+}
+
enum ComputeNodeData {
COMPUTE_NODE = 'node',
PROPERTIES = 'properties'
DEFAULT_OWNER_UUID = 'default_owner_uuid'
}
-type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData | ApiClientAuthorizationsData;
+type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ComputeNodeData | ApiClientAuthorizationsData | UserData;
type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix;
-type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | ApiClientAuthorization | undefined;
+type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | NodeResource | ApiClientAuthorization | UserResource | undefined;
export const openAdvancedTabDialog = (uuid: string) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
});
dispatch<any>(initAdvancedTabDialog(advanceDataKeepService));
break;
+ case ResourceKind.USER:
+ const { resources } = getState();
+ const data = getResource<UserResource>(uuid)(resources);
+ const metadata = await services.linkService.list({
+ filters: new FilterBuilder()
+ .addEqual('headUuid', uuid)
+ .getFilters()
+ });
+ const advanceDataUser = advancedTabData({
+ uuid,
+ metadata,
+ user: '',
+ apiResponseKind: userApiResponse,
+ data,
+ resourceKind: UserData.USER,
+ resourcePrefix: ResourcePrefix.USERS,
+ resourceKindProperty: UserData.USERNAME,
+ property: data!.username
+ });
+ dispatch<any>(initAdvancedTabDialog(advanceDataUser));
+ break;
case ResourceKind.NODE:
const dataComputeNode = getState().computeNodes.find(node => node.uuid === uuid);
const advanceDataComputeNode = advancedTabData({
return response;
};
+const userApiResponse = (apiResponse: UserResource) => {
+ const {
+ uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid,
+ email, firstName, lastName, identityUrl, isActive, isAdmin, prefs, defaultOwnerUuid, username
+ } = apiResponse;
+ const response = `"uuid": "${uuid}",
+"owner_uuid": "${ownerUuid}",
+"created_at": "${createdAt}",
+"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
+"modified_by_user_uuid": ${stringify(modifiedByUserUuid)},
+"modified_at": ${stringify(modifiedAt)},
+"email": "${email}",
+"first_name": "${firstName}",
+"last_name": "${stringify(lastName)}",
+"identity_url": "${identityUrl}",
+"is_active": "${isActive},
+"is_admin": "${isAdmin},
+"prefs": "${stringifyObject(prefs)},
+"default_owner_uuid": "${defaultOwnerUuid},
+"username": "${username}"`;
+
+ return response;
+};
+
const computeNodeApiResponse = (apiResponse: NodeResource) => {
const {
uuid, slotNumber, hostname, domain, ipAddress, firstPingAt, lastPingAt, jobUuid,
const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization) => {
const {
- uuid, ownerUuid, apiToken, apiClientId, userId, createdByIpAddress, lastUsedByIpAddress,
+ uuid, ownerUuid, apiToken, apiClientId, userId, createdByIpAddress, lastUsedByIpAddress,
lastUsedAt, expiresAt, defaultOwnerUuid, scopes, updatedAt, createdAt
} = apiResponse;
const response = `"uuid": "${uuid}",
export const navigateToComputeNodes = push(Routes.COMPUTE_NODES);
-export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS);
\ No newline at end of file
+export const navigateToUsers = push(Routes.USERS);
+
+export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS);
dataExplorerToListParams,
getDataExplorerColumnFilters,
listResultsToDataExplorerItemsMeta
-} from '../data-explorer/data-explorer-middleware-service';
-import { ProjectPanelColumnNames, ProjectPanelFilter } from "~/views/project-panel/project-panel";
-import { RootState } from "../store";
+} from '~/store/data-explorer/data-explorer-middleware-service';
+import { ProjectPanelColumnNames } from "~/views/project-panel/project-panel";
+import { RootState } from "~/store/store";
import { DataColumns } from "~/components/data-table/data-table";
import { ServiceRepository } from "~/services/services";
import { SortDirection } from "~/components/data-table/data-column";
import { OrderBuilder, OrderDirection } from "~/services/api/order-builder";
import { FilterBuilder, joinFilters } from "~/services/api/filter-builder";
import { GroupContentsResource, GroupContentsResourcePrefix } from "~/services/groups-service/groups-service";
-import { updateFavorites } from "../favorites/favorites-actions";
-import { PROJECT_PANEL_CURRENT_UUID, IS_PROJECT_PANEL_TRASHED, projectPanelActions } from './project-panel-action';
+import { updateFavorites } from "~/store/favorites/favorites-actions";
+import { PROJECT_PANEL_CURRENT_UUID, IS_PROJECT_PANEL_TRASHED, projectPanelActions } from '~/store/project-panel/project-panel-action';
import { Dispatch, MiddlewareAPI } from "redux";
import { ProjectResource } from "~/models/project";
import { updateResources } from "~/store/resources/resources-actions";
import { getProperty } from "~/store/properties/properties";
-import { snackbarActions, SnackbarKind } from '../snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from '~/store/snackbar/snackbar-actions';
import { progressIndicatorActions } from '~/store/progress-indicator/progress-indicator-actions.ts';
-import { DataExplorer, getDataExplorer } from '../data-explorer/data-explorer-reducer';
+import { DataExplorer, getDataExplorer } from '~/store/data-explorer/data-explorer-reducer';
import { ListResults } from '~/services/common-service/common-service';
-import { loadContainers } from '../processes/processes-actions';
+import { loadContainers } from '~/store/processes/processes-actions';
import { ResourceKind } from '~/models/resource';
import { getResource } from "~/store/resources/resources";
import { CollectionResource } from "~/models/collection";
import { resourcesDataActions } from "~/store/resources-data/resources-data-actions";
import { getSortColumn } from "~/store/data-explorer/data-explorer-reducer";
-import { serializeResourceTypeFilters } from '../resource-type-filters/resource-type-filters';
+import { serializeResourceTypeFilters } from '~/store/resource-type-filters/resource-type-filters';
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: 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);
};
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';
import { computeNodesReducer } from '~/store/compute-nodes/compute-nodes-reducer';
import { apiClientAuthorizationsReducer } from '~/store/api-client-authorizations/api-client-authorizations-reducer';
const workflowPanelMiddleware = dataExplorerMiddleware(
new WorkflowMiddlewareService(services, WORKFLOW_PANEL_ID)
);
+ const userPanelMiddleware = dataExplorerMiddleware(
+ new UserMiddlewareService(services, USERS_PANEL_ID)
+ );
const middlewares: Middleware[] = [
routerMiddleware(history),
trashPanelMiddleware,
searchResultsPanelMiddleware,
sharedWithMePanelMiddleware,
- workflowPanelMiddleware
+ workflowPanelMiddleware,
+ userPanelMiddleware
];
const enhancer = composeEnhancers(applyMiddleware(...middlewares));
return createStore(rootReducer, enhancer);
//
// 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);
--- /dev/null
+// 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-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("username", 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
--- /dev/null
+// 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 { 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";
+import { getResource } from '~/store/resources/resources';
+import { navigateToProject } from "~/store/navigation/navigation-action";
+
+export const USERS_PANEL_ID = 'usersPanel';
+export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
+export const USER_CREATE_FORM_NAME = 'repositoryCreateFormName';
+
+export interface UserCreateFormDialogData {
+ email: string;
+ identityUrl: string;
+ virtualMachineName: string;
+ groupVirtualMachine: string;
+}
+
+export const openUserAttributes = (uuid: string) =>
+ (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const data = getResource<UserResource>(uuid)(resources);
+ dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data }));
+ };
+
+export const openUserCreateDialog = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = await services.authService.getUuid();
+ const user = await services.userService.get(userUuid!);
+ const virtualMachines = await services.virtualMachineService.list();
+ dispatch(reset(USER_CREATE_FORM_NAME));
+ dispatch(dialogActions.OPEN_DIALOG({ id: USER_CREATE_FORM_NAME, data: { user, ...virtualMachines } }));
+ };
+
+export const openUserProjects = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch<any>(navigateToProject(uuid));
+ };
+
+
+export const createUser = (user: UserCreateFormDialogData) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(USER_CREATE_FORM_NAME));
+ try {
+ const newUser = await services.userService.create({ ...user });
+ dispatch(dialogActions.CLOSE_DIALOG({ id: USER_CREATE_FORM_NAME }));
+ dispatch(reset(USER_CREATE_FORM_NAME));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been successfully created.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+ dispatch<any>(loadUsersPanel());
+ dispatch(userBindedActions.REQUEST_ITEMS());
+ return newUser;
+ } catch (e) {
+ return ;
+ }
+ };
+
+export const toggleIsActive = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const data = getResource<UserResource>(uuid)(resources);
+ const isActive = data!.isActive;
+ const newActivity = await services.userService.update(uuid, { ...data, isActive: !isActive });
+ dispatch<any>(loadUsersPanel());
+ return newActivity;
+ };
+
+export const toggleIsAdmin = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const { resources } = getState();
+ const data = getResource<UserResource>(uuid)(resources);
+ const isAdmin = data!.isAdmin;
+ const newActivity = await services.userService.update(uuid, { ...data, isAdmin: !isAdmin });
+ dispatch<any>(loadUsersPanel());
+ return newActivity;
+ };
+
+export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
+
+export const loadUsersData = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ await services.userService.list();
+ };
+
+export const loadUsersPanel = () =>
+ (dispatch: Dispatch) => {
+ dispatch(userBindedActions.REQUEST_ITEMS());
+ };
\ No newline at end of file
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';
import { loadComputeNodesPanel } from '~/store/compute-nodes/compute-nodes-actions';
import { loadApiClientAuthorizationsPanel } from '~/store/api-client-authorizations/api-client-authorizations-actions';
}
};
-
export const loadWorkbench = () =>
async (dispatch: Dispatch, getState: () => RootState) => {
dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
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);
const userUuid = services.authService.getUuid();
dispatch(setIsProjectPanelTrashed(false));
if (userUuid) {
- if (userUuid !== uuid) {
+ if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
+ // Load another users home projects
+ dispatch(finishLoadingProject(uuid));
+ } else if (userUuid !== uuid) {
const match = await loadGroupContentsResource({ uuid, userUuid, services });
match({
OWNED: async project => {
await dispatch(loadVirtualMachinesPanel());
dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
});
-
+
export const loadRepositories = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
await dispatch(loadRepositoriesPanel());
await dispatch(loadKeepServicesPanel());
});
+export const loadUsers = handleFirstTimeLoad(
+ async (dispatch: Dispatch<any>) => {
+ await dispatch(loadUsersPanel());
+ dispatch(setBreadcrumbs([{ label: 'Users' }]));
+ });
+
export const loadComputeNodes = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
await dispatch(loadComputeNodesPanel());
import { OrderDirection, OrderBuilder } from '~/services/api/order-builder';
import { WorkflowResource } from '~/models/workflow';
import { ListResults } from '~/services/common-service/common-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 {
export const REPOSITORY_NAME_VALIDATION = [require, maxLength(255)];
+export const USER_EMAIL_VALIDATION = [require, maxLength(255)];
+export const USER_LENGTH_VALIDATION = [maxLength(255)];
+
export const SSH_KEY_PUBLIC_VALIDATION = [require, isRsaKey, maxLength(1024)];
export const SSH_KEY_NAME_VALIDATION = [require, maxLength(255)];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "~/views-components/context-menu/context-menu-action-set";
+import { AdvancedIcon, ProjectIcon, AttributesIcon, UserPanelIcon } from "~/components/icon/icon";
+import { openAdvancedTabDialog } from '~/store/advanced-tab/advanced-tab';
+import { openUserAttributes, openUserProjects } from "~/store/users/users-actions";
+
+export const userActionSet: ContextMenuActionSet = [[{
+ name: "Attributes",
+ icon: AttributesIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openUserAttributes(uuid));
+ }
+}, {
+ name: "Project",
+ icon: ProjectIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openUserProjects(uuid));
+ }
+}, {
+ name: "Advanced",
+ icon: AdvancedIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openAdvancedTabDialog(uuid));
+ }
+},
+{
+ name: "Manage",
+ icon: UserPanelIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openAdvancedTabDialog(uuid));
+ }
+}]];
SSH_KEY = "SshKey",
VIRTUAL_MACHINE = "VirtualMachine",
KEEP_SERVICE = "KeepService",
+ USER = "User",
NODE = "Node"
}
// 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';
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';
+import { toggleIsActive, toggleIsAdmin } from '~/store/users/users-actions';
-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)}
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 />;
}
};
-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)}
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>
})((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 = (props: { uuid: string, isActive: boolean, toggleIsActive: (uuid: string) => void }) =>
+ <Checkbox
+ color="primary"
+ checked={props.isActive}
+ onClick={() => props.toggleIsActive(props.uuid)} />;
+
+export const ResourceIsActive = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<UserResource>(props.uuid)(state.resources);
+ return resource || { isActive: false };
+ }, { toggleIsActive }
+)(renderIsActive);
+
+const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (uuid: string) => void }) =>
+ <Checkbox
+ color="primary"
+ checked={props.isAdmin}
+ onClick={() => props.toggleIsAdmin(props.uuid)} />;
+
+export const ResourceIsAdmin = connect(
+ (state: RootState, props: { uuid: string }) => {
+ const resource = getResource<UserResource>(props.uuid)(state.resources);
+ return resource || { isAdmin: false };
+ }, { toggleIsAdmin }
+)(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 &&
})((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 {
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>;
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>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { InjectedFormProps } from 'redux-form';
+import { WithDialogProps } from '~/store/dialog/with-dialog';
+import { FormDialog } from '~/components/form-dialog/form-dialog';
+import { UserEmailField, UserIdentityUrlField, UserVirtualMachineField, UserGroupsVirtualMachineField } from '~/views-components/form-fields/user-form-fields';
+
+export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<any>;
+
+export const UserRepositoryCreate = (props: DialogUserProps) =>
+ <FormDialog
+ dialogTitle='New user'
+ formFields={UserAddFields}
+ submitLabel='ADD NEW USER'
+ {...props}
+ />;
+
+const UserAddFields = (props: DialogUserProps) => <span>
+ <UserEmailField />
+ <UserIdentityUrlField />
+ <UserVirtualMachineField data={props.data}/>
+ <UserGroupsVirtualMachineField />
+</span>;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { compose } from "redux";
+import { reduxForm } from 'redux-form';
+import { withDialog } from "~/store/dialog/with-dialog";
+import { USER_CREATE_FORM_NAME, createUser, UserCreateFormDialogData } from "~/store/users/users-actions";
+import { UserRepositoryCreate } from "~/views-components/dialog-create/dialog-user-create";
+
+export const CreateUserDialog = compose(
+ withDialog(USER_CREATE_FORM_NAME),
+ reduxForm<UserCreateFormDialogData>({
+ form: USER_CREATE_FORM_NAME,
+ onSubmit: (data, dispatch) => {
+ dispatch(createUser(data));
+ }
+ })
+)(UserRepositoryCreate);
\ No newline at end of file
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { Field, WrappedFieldProps } from "redux-form";
+import { Field } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { COLLECTION_NAME_VALIDATION, COLLECTION_DESCRIPTION_VALIDATION, COLLECTION_PROJECT_VALIDATION } from "~/validators/validators";
-import { ProjectTreePicker, ProjectTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
-import { PickerIdProp } from '../../store/tree-picker/picker-id';
+import { ProjectTreePickerField } from "~/views-components/project-tree-picker/project-tree-picker";
+import { PickerIdProp } from '~/store/tree-picker/picker-id';
export const CollectionNameField = () =>
<Field
// SPDX-License-Identifier: AGPL-3.0
import * as React from "react";
-import { Field, WrappedFieldProps } from "redux-form";
+import { Field } from "redux-form";
import { TextField } from "~/components/text-field/text-field";
import { PROCESS_NAME_VALIDATION } from "~/validators/validators";
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Field } from "redux-form";
+import { TextField } from "~/components/text-field/text-field";
+import { USER_EMAIL_VALIDATION, USER_LENGTH_VALIDATION } from "~/validators/validators";
+import { NativeSelectField } from "~/components/select-field/select-field";
+import { InputLabel } from "@material-ui/core";
+import { VirtualMachinesResource } from "~/models/virtual-machines";
+
+export const UserEmailField = () =>
+ <Field
+ name='email'
+ component={TextField}
+ validate={USER_EMAIL_VALIDATION}
+ autoFocus={true}
+ label="Email" />;
+
+export const UserIdentityUrlField = () =>
+ <Field
+ name='identityUrl'
+ component={TextField}
+ validate={USER_LENGTH_VALIDATION}
+ label="Identity URL Prefix" />;
+
+export const UserVirtualMachineField = ({ data }: any) =>
+ <div style={{ marginBottom: '21px' }}>
+ <InputLabel>Virtual Machine</InputLabel>
+ <Field
+ name='virtualMachine'
+ component={NativeSelectField}
+ validate={USER_LENGTH_VALIDATION}
+ items={getVirtualMachinesList(data.items)} />
+ </div>;
+
+export const UserGroupsVirtualMachineField = () =>
+ <Field
+ name='groups'
+ component={TextField}
+ validate={USER_LENGTH_VALIDATION}
+ label="Groups for virtual machine (comma separated list)" />;
+
+const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) => {
+ const mappedVirtualMachines = virtualMachines.map(it => ({ key: it.hostname, value: it.hostname }));
+ return mappedVirtualMachines;
+};
navigateToApiClientAuthorizations, navigateToMyAccount
} 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;
<MenuItem onClick={() => dispatch(openRepositoriesPanel())}>Repositories</MenuItem>
<MenuItem onClick={() => dispatch(openCurrentTokenDialog)}>Current token</MenuItem>
<MenuItem onClick={() => dispatch(navigateToSshKeys)}>Ssh Keys</MenuItem>
+ <MenuItem onClick={() => dispatch(navigateToUsers)}>Users</MenuItem>
{ user.isAdmin && <MenuItem onClick={() => dispatch(navigateToApiClientAuthorizations)}>Api Tokens</MenuItem> }
{ user.isAdmin && <MenuItem onClick={() => dispatch(navigateToKeepServices)}>Keep Services</MenuItem> }
{ user.isAdmin && <MenuItem onClick={() => dispatch(navigateToComputeNodes)}>Compute Nodes</MenuItem> }
return !Routes.matchWorkflowRoute(pathname) && !Routes.matchVirtualMachineRoute(pathname) &&
!Routes.matchRepositoriesRoute(pathname) && !Routes.matchSshKeysRoute(pathname) &&
!Routes.matchKeepServicesRoute(pathname) && !Routes.matchComputeNodesRoute(pathname) &&
- !Routes.matchApiClientAuthorizationsRoute(pathname);
+ !Routes.matchApiClientAuthorizationsRoute(pathname) && !Routes.matchUsersRoute(pathname);
};
export const MainContentBar = connect((state: RootState) => ({
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography, Grid } from "@material-ui/core";
+import { WithDialogProps } from "~/store/dialog/with-dialog";
+import { withDialog } from '~/store/dialog/with-dialog';
+import { WithStyles, withStyles } from '@material-ui/core/styles';
+import { ArvadosTheme } from '~/common/custom-theme';
+import { compose } from "redux";
+import { USER_ATTRIBUTES_DIALOG } from "~/store/users/users-actions";
+import { UserResource } from "~/models/user";
+
+type CssRules = 'rightContainer' | 'leftContainer' | 'spacing';
+
+const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
+ rightContainer: {
+ textAlign: 'right',
+ paddingRight: theme.spacing.unit * 2,
+ color: theme.palette.grey["500"]
+ },
+ leftContainer: {
+ textAlign: 'left',
+ paddingLeft: theme.spacing.unit * 2
+ },
+ spacing: {
+ paddingTop: theme.spacing.unit * 2
+ },
+}));
+
+interface UserAttributesDataProps {
+ data: UserResource;
+}
+
+type UserAttributesProps = UserAttributesDataProps & WithStyles<CssRules>;
+
+export const UserAttributesDialog = compose(
+ withDialog(USER_ATTRIBUTES_DIALOG),
+ styles)(
+ (props: WithDialogProps<UserAttributesProps> & UserAttributesProps) =>
+ <Dialog open={props.open}
+ onClose={props.closeDialog}
+ fullWidth
+ maxWidth="sm">
+ <DialogTitle>Attributes</DialogTitle>
+ <DialogContent>
+ <Typography variant="body2" className={props.classes.spacing}>
+ {props.data && attributes(props.data, props.classes)}
+ </Typography>
+ </DialogContent>
+ <DialogActions>
+ <Button
+ variant='flat'
+ color='primary'
+ onClick={props.closeDialog}>
+ Close
+ </Button>
+ </DialogActions>
+ </Dialog>
+ );
+
+const attributes = (user: UserResource, classes: any) => {
+ const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, firstName, lastName, href, identityUrl, username } = user;
+ return (
+ <span>
+ <Grid container direction="row">
+ <Grid item xs={5} className={classes.rightContainer}>
+ {firstName && <Grid item>First name</Grid>}
+ {lastName && <Grid item>Last name</Grid>}
+ {ownerUuid && <Grid item>Owner uuid</Grid>}
+ {createdAt && <Grid item>Created at</Grid>}
+ {modifiedAt && <Grid item>Modified at</Grid>}
+ {modifiedByUserUuid && <Grid item>Modified by user uuid</Grid>}
+ {modifiedByClientUuid && <Grid item>Modified by client uuid</Grid>}
+ {uuid && <Grid item>uuid</Grid>}
+ {href && <Grid item>Href</Grid>}
+ {identityUrl && <Grid item>Identity url</Grid>}
+ {username && <Grid item>Username</Grid>}
+ </Grid>
+ <Grid item xs={7} className={classes.leftContainer}>
+ <Grid item>{firstName}</Grid>
+ <Grid item>{lastName}</Grid>
+ <Grid item>{ownerUuid}</Grid>
+ <Grid item>{createdAt}</Grid>
+ <Grid item>{modifiedAt}</Grid>
+ <Grid item>{modifiedByUserUuid}</Grid>
+ <Grid item>{modifiedByClientUuid}</Grid>
+ <Grid item>{uuid}</Grid>
+ <Grid item>{href}</Grid>
+ <Grid item>{identityUrl}</Grid>
+ <Grid item>{username}</Grid>
+ </Grid>
+ </Grid>
+ </span>
+ );
+};
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';
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",
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";
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) => ({
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { WithStyles, withStyles, Typography, Tabs, Tab, Paper, Button } from '@material-ui/core';
+import { DataExplorer } from "~/views-components/data-explorer/data-explorer";
+import { connect, DispatchProp } from 'react-redux';
+import { DataColumns } from '~/components/data-table/data-table';
+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 { 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, Dispatch } from 'redux';
+import { UserResource } from '~/models/user';
+import { ShareMeIcon, AddIcon } from '~/components/icon/icon';
+import { USERS_PANEL_ID, openUserCreateDialog } from '~/store/users/users-actions';
+
+type UserPanelRules = "button";
+
+const styles = withStyles<UserPanelRules>(theme => ({
+ button: {
+ marginTop: theme.spacing.unit,
+ marginRight: theme.spacing.unit * 2,
+ textAlign: 'right',
+ alignSelf: 'center'
+ },
+}));
+
+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;
+}
+
+interface UserPanelActionProps {
+ openUserCreateDialog: () => void;
+ handleRowDoubleClick: (uuid: string) => void;
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
+}
+
+const mapStateToProps = (state: RootState) => {
+ return {
+ resources: state.resources
+ };
+};
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ openUserCreateDialog: () => dispatch<any>(openUserCreateDialog()),
+ handleRowDoubleClick: (uuid: string) => dispatch<any>(navigateTo(uuid)),
+ onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => dispatch<any>(openContextMenu(event, item))
+});
+
+type UserPanelProps = UserPanelDataProps & UserPanelActionProps & DispatchProp & WithStyles<UserPanelRules>;
+
+export const UserPanel = compose(
+ styles,
+ connect(mapStateToProps, mapDispatchToProps))(
+ class extends React.Component<UserPanelProps> {
+ state = {
+ value: 0,
+ };
+
+ componentDidMount() {
+ this.setState({ value: 0 });
+ }
+
+ render() {
+ const { value } = this.state;
+ return <Paper>
+ <Tabs value={value} onChange={this.handleChange} fullWidth>
+ <Tab label="USERS" />
+ <Tab label="ACTIVITY" disabled />
+ </Tabs>
+ {value === 0 &&
+ <span>
+ <div className={this.props.classes.button}>
+ <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
+ <AddIcon /> NEW USER
+ </Button>
+ </div>
+ <DataExplorer
+ id={USERS_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={true}
+ isUserPanel={true}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={ShareMeIcon}
+ messages={['Your user list is empty.']} />
+ } />
+ </span>}
+ </Paper>;
+ }
+
+ handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ this.setState({ value });
+ }
+
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
+ if (resource) {
+ this.props.onContextMenu(event, {
+ name: '',
+ uuid: resource.uuid,
+ ownerUuid: resource.ownerUuid,
+ kind: resource.kind,
+ menuKind: ContextMenuKind.USER
+ });
+ }
+ }
+
+ handleRowDoubleClick = (uuid: string) => {
+ this.props.handleRowDoubleClick(uuid);
+ }
+
+ handleRowClick = () => {
+ return;
+ }
+ }
+ );
import { AttributesKeepServiceDialog } from '~/views-components/keep-services-dialog/attributes-dialog';
import { AttributesSshKeyDialog } from '~/views-components/ssh-keys-dialog/attributes-dialog';
import { VirtualMachineAttributesDialog } from '~/views-components/virtual-machines-dialog/attributes-dialog';
+import { UserPanel } from '~/views/user-panel/user-panel';
+import { UserAttributesDialog } from '~/views-components/user-dialog/attributes-dialog';
+import { CreateUserDialog } from '~/views-components/dialog-forms/create-user-dialog';
import { HelpApiClientAuthorizationDialog } from '~/views-components/api-client-authorizations-dialog/help-dialog';
type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
<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} />
<Route path={Routes.COMPUTE_NODES} component={ComputeNodePanel} />
<Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
<Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
<CreateProjectDialog />
<CreateRepositoryDialog />
<CreateSshKeyDialog />
+ <CreateUserDialog />
<CurrentTokenDialog />
<FileRemoveDialog />
<FilesUploadCollectionDialog />
<UpdateCollectionDialog />
<UpdateProcessDialog />
<UpdateProjectDialog />
+ <UserAttributesDialog />
<VirtualMachineAttributesDialog />
</Grid>
);
\ No newline at end of file