[ARVADOS-WORKBENCH2] created: 2.3.0-175-g7435f8f8
Git user
git at public.arvados.org
Wed Mar 2 22:29:08 UTC 2022
at 7435f8f863ff94834d7188772547cfb0cd4ba1d4 (commit)
commit 7435f8f863ff94834d7188772547cfb0cd4ba1d4
Author: Stephen Smith <stephen at curii.com>
Date: Wed Mar 2 17:22:22 2022 -0500
18559: Disable user profile form fields when not admin or self
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/components/select-field/select-field.tsx b/src/components/select-field/select-field.tsx
index e4dcad6c..6fa7ddea 100644
--- a/src/components/select-field/select-field.tsx
+++ b/src/components/select-field/select-field.tsx
@@ -35,14 +35,18 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
}
});
+interface NativeSelectFieldProps {
+ disabled?: boolean;
+}
+
export const NativeSelectField = withStyles(styles)
- ((props: WrappedFieldProps & WithStyles<CssRules> & { items: any[] }) =>
+ ((props: WrappedFieldProps & NativeSelectFieldProps & WithStyles<CssRules> & { items: any[] }) =>
<FormControl className={props.classes.formControl}>
<Select className={props.classes.selectWrapper}
native
value={props.input.value}
onChange={props.input.onChange}
- disabled={props.meta.submitting}
+ disabled={props.meta.submitting || props.disabled}
name={props.input.name}
inputProps={{
id: `id-${props.input.name}`,
@@ -81,4 +85,4 @@ export const SelectField = withStyles(selectFieldStyles)(
</Select>
<FormHelperText>{props.meta.error}</FormHelperText>
</FormControl>
-);
\ No newline at end of file
+);
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index a725333d..76cad8a5 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -71,6 +71,8 @@ export interface UserProfilePanelRootActionProps {
}
export interface UserProfilePanelRootDataProps {
+ isAdmin: boolean;
+ isSelf: boolean;
isPristine: boolean;
isValid: boolean;
initialValues?: User;
@@ -150,95 +152,97 @@ export const UserProfilePanelRoot = withStyles(styles)(
<Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
<Tab label="PROFILE" />
<Tab label="GROUPS" />
- <Tab label="ADMIN" />
+ <Tab label="ADMIN" disabled={!this.props.isAdmin} />
</Tabs>
{this.state.value === 0 &&
- // <Card className={this.props.classes.root}>
- <CardContent>
- <form onSubmit={this.props.handleSubmit}>
- <Grid container spacing={24}>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <Field
- label="First name"
- name="firstName"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <Field
- label="Last name"
- name="lastName"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <Field
- label="E-mail"
- name="email"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <Field
- label="Username"
- name="username"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <Field
- label="Organization"
- name="prefs.profile.organization"
- component={TextField as any}
- validate={MY_ACCOUNT_VALIDATION}
- required
- />
- </Grid>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <Field
- label="E-mail at Organization"
- name="prefs.profile.organization_email"
- component={TextField as any}
- validate={MY_ACCOUNT_VALIDATION}
- required
- />
- </Grid>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
- <Field
- id="prefs.profile.role"
- name="prefs.profile.role"
- component={NativeSelectField as any}
- items={RoleTypes}
- />
- </Grid>
- <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
- <Field
- label="Website"
- name="prefs.profile.website_url"
- component={TextField as any}
- />
- </Grid>
- <Grid item sm={12}>
- <Grid container direction="row" justify="flex-end">
- <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
- <Button
- color="primary"
- variant="contained"
- type="submit"
- disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
- Save changes
- </Button>
- </Grid>
+ <CardContent>
+ <form onSubmit={this.props.handleSubmit}>
+ <Grid container spacing={24}>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="First name"
+ name="firstName"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Last name"
+ name="lastName"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="E-mail"
+ name="email"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Username"
+ name="username"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Organization"
+ name="prefs.profile.organization"
+ component={TextField as any}
+ validate={MY_ACCOUNT_VALIDATION}
+ required
+ disabled={!this.props.isAdmin && !this.props.isSelf}
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="E-mail at Organization"
+ name="prefs.profile.organization_email"
+ component={TextField as any}
+ validate={MY_ACCOUNT_VALIDATION}
+ required
+ disabled={!this.props.isAdmin && !this.props.isSelf}
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
+ <Field
+ id="prefs.profile.role"
+ name="prefs.profile.role"
+ component={NativeSelectField as any}
+ items={RoleTypes}
+ disabled={!this.props.isAdmin && !this.props.isSelf}
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Website"
+ name="prefs.profile.website_url"
+ component={TextField as any}
+ disabled={!this.props.isAdmin && !this.props.isSelf}
+ />
+ </Grid>
+ <Grid item sm={12}>
+ <Grid container direction="row" justify="flex-end">
+ <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
+ <Button
+ color="primary"
+ variant="contained"
+ type="submit"
+ disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
+ Save changes
+ </Button>
</Grid>
</Grid>
- </form >
- </CardContent>
- // </Card>
+ </Grid>
+ </form >
+ </CardContent>
}
{this.state.value === 1 &&
<div className={this.props.classes.content}>
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
index caac3e8c..2bafd9fa 100644
--- a/src/views/user-profile-panel/user-profile-panel.tsx
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -23,7 +23,8 @@ const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
// const subprocesses = getSubprocesses(uuid)(resources);
return {
-
+ isAdmin: state.auth.user!.isAdmin,
+ isSelf: state.auth.user!.uuid === uuid,
isPristine: isPristine(USER_PROFILE_FORM)(state),
isValid: isValid(USER_PROFILE_FORM)(state),
initialValues: user,
commit 925a083d8e82281a6d1de1f1021a88da147e5bac
Author: Stephen Smith <stephen at curii.com>
Date: Wed Mar 2 17:21:31 2022 -0500
18559: Fix user profile form initialization & cleanup
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
index 103456f3..82e90a2b 100644
--- a/src/store/user-profile/user-profile-actions.ts
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -3,7 +3,7 @@
// SPDX-License-Identifier: AGPL-3.0
import { RootState } from "store/store";
import { Dispatch } from 'redux';
-import { reset } from "redux-form";
+import { initialize, reset } from "redux-form";
import { ServiceRepository } from "services/services";
import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
import { propertiesActions } from 'store/properties/properties-actions';
@@ -25,9 +25,10 @@ export const loadUserProfilePanel = (userUuid?: string) =>
// Get user uuid from route or use current user uuid
const uuid = userUuid || getState().auth.user?.uuid;
if (uuid) {
+ await dispatch(propertiesActions.SET_PROPERTY({ key: USER_PROFILE_PANEL_ID, value: uuid }));
const user = await services.userService.get(uuid);
+ dispatch(initialize(USER_PROFILE_FORM, user));
dispatch(updateResources([user]));
- await dispatch(propertiesActions.SET_PROPERTY({ key: USER_PROFILE_PANEL_ID, value: uuid }));
dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
}
}
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index be8ef681..9f4d4ce4 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -157,7 +157,7 @@ export const UserPanel = compose(
render() {
const { value } = this.state;
return <Paper className={this.props.classes.root}>
- <Tabs value={value} onChange={this.handleChange} fullWidth>
+ <Tabs value={value} onChange={this.handleChange} variant={"fullWidth"}>
<Tab label="USERS" />
<Tab label="ACTIVITY" disabled />
</Tabs>
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
index c0c80e3c..a725333d 100644
--- a/src/views/user-profile-panel/user-profile-panel-root.tsx
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -89,11 +89,6 @@ const RoleTypes = [
type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & WithStyles<CssRules>;
-// type LocalClusterProp = { localCluster: string };
-// const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
-// <span>{localCluster === input.value.substring(0, 5) ? "" : "federated"} user {input.value}</span>
-// );
-
export enum UserProfileGroupsColumnNames {
NAME = "Name",
PERMISSION = "Permission",
@@ -152,10 +147,7 @@ export const UserProfilePanelRoot = withStyles(styles)(
render() {
return <Paper className={this.props.classes.root}>
- {/* <Typography variant="title" className={this.props.classes.title}>
- Logged in as <Field name="uuid" component={renderField} localCluster={this.props.localCluster} />
- </Typography> */}
- <Tabs value={this.state.value} onChange={this.handleChange} fullWidth>
+ <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
<Tab label="PROFILE" />
<Tab label="GROUPS" />
<Tab label="ADMIN" />
@@ -254,7 +246,6 @@ export const UserProfilePanelRoot = withStyles(styles)(
id={USER_PROFILE_PANEL_ID}
onRowClick={noop}
onRowDoubleClick={noop}
- // onContextMenu={this.handleContextMenu}
contextMenuColumn={false}
hideColumnSelector
hideSearchInput
@@ -352,17 +343,5 @@ export const UserProfilePanelRoot = withStyles(styles)(
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
- // });
- // }
- }
}
);
commit 9da78c51275666c685545d29cd92ffa0d32f7b2f
Author: Stephen Smith <stephen at curii.com>
Date: Wed Mar 2 15:44:47 2022 -0500
18559: Link from users panel to user profile page.
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index b0eb7918..205ae08f 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -100,6 +100,8 @@ export const getProcessLogUrl = (uuid: string) => `/process-logs/${uuid}`;
export const getGroupUrl = (uuid: string) => `/group/${uuid}`;
+export const getUserProfileUrl = (uuid: string) => `/user/${uuid}`;
+
export interface ResourceRouteParams {
id: string;
}
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 47d8e4fb..776409c0 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -6,7 +6,7 @@ import { Dispatch, compose, AnyAction } from 'redux';
import { push } from "react-router-redux";
import { ResourceKind, extractUuidKind } from 'models/resource';
import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl } from 'routes/routes';
+import { Routes, getProcessLogUrl, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
import { RootState } from 'store/store';
import { ServiceRepository } from 'services/services';
import { pluginConfig } from 'plugins';
@@ -144,6 +144,8 @@ export const navigateToKeepServices = push(Routes.KEEP_SERVICES);
export const navigateToUsers = push(Routes.USERS);
+export const navigateToUserProfile = compose(push, getUserProfileUrl);
+
export const navigateToApiClientAuthorizations = push(Routes.API_CLIENT_AUTHORIZATIONS);
export const navigateToGroups = push(Routes.GROUPS);
diff --git a/src/views-components/data-explorer/renderers.tsx b/src/views-components/data-explorer/renderers.tsx
index a2acaca4..7cdd9b83 100644
--- a/src/views-components/data-explorer/renderers.tsx
+++ b/src/views-components/data-explorer/renderers.tsx
@@ -195,7 +195,10 @@ const renderIsActive = (props: { uuid: string, kind: ResourceKind, isActive: boo
color="primary"
checked={props.isActive}
disabled={!!props.disabled}
- onClick={() => props.toggleIsActive(props.uuid)} />;
+ onClick={(e) => {
+ e.stopPropagation();
+ props.toggleIsActive(props.uuid)
+ }} />;
} else {
return <Typography />;
}
@@ -230,7 +233,10 @@ const renderIsHidden = (props: {
color="primary"
checked={props.visible}
disabled={!props.canManage}
- onClick={() => props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible)} />;
+ onClick={(e) => {
+ e.stopPropagation();
+ props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
+ }} />;
} else {
return <Typography />;
}
@@ -263,7 +269,10 @@ const renderIsAdmin = (props: { uuid: string, isAdmin: boolean, toggleIsAdmin: (
<Checkbox
color="primary"
checked={props.isAdmin}
- onClick={() => props.toggleIsAdmin(props.uuid)} />;
+ onClick={(e) => {
+ e.stopPropagation();
+ props.toggleIsAdmin(props.uuid);
+ }} />;
export const ResourceIsAdmin = connect(
(state: RootState, props: { uuid: string }) => {
diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx
index 5fb979a2..be8ef681 100644
--- a/src/views/user-panel/user-panel.tsx
+++ b/src/views/user-panel/user-panel.tsx
@@ -20,7 +20,7 @@ import {
ResourceIsAdmin,
ResourceUsername
} from "views-components/data-explorer/renderers";
-import { navigateTo } from "store/navigation/navigation-action";
+import { navigateToUserProfile } 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';
@@ -124,7 +124,7 @@ interface UserPanelDataProps {
interface UserPanelActionProps {
openUserCreateDialog: () => void;
- handleRowDoubleClick: (uuid: string) => void;
+ handleRowClick: (uuid: string) => void;
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
}
@@ -136,7 +136,7 @@ const mapStateToProps = (state: RootState) => {
const mapDispatchToProps = (dispatch: Dispatch) => ({
openUserCreateDialog: () => dispatch<any>(openUserCreateDialog()),
- handleRowDoubleClick: (uuid: string) => dispatch<any>(navigateTo(uuid)),
+ handleRowClick: (uuid: string) => dispatch<any>(navigateToUserProfile(uuid)),
onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => dispatch<any>(openContextMenu(event, item))
});
@@ -165,7 +165,7 @@ export const UserPanel = compose(
<div className={this.props.classes.content}>
<DataExplorer
id={USERS_PANEL_ID}
- onRowClick={noop}
+ onRowClick={this.props.handleRowClick}
onRowDoubleClick={noop}
onContextMenu={this.handleContextMenu}
contextMenuColumn={true}
@@ -194,6 +194,7 @@ export const UserPanel = compose(
}
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ event.stopPropagation();
const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
if (resource) {
this.props.onContextMenu(event, {
commit addb01b6d7636a8963ddb1eff4799ebc96f44739
Author: Stephen Smith <stephen at curii.com>
Date: Wed Mar 2 10:09:23 2022 -0500
18559: Add groups and admin tab to user profile, use for other users profile
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/routes/route-change-handlers.ts b/src/routes/route-change-handlers.ts
index 044a38bf..b9158083 100644
--- a/src/routes/route-change-handlers.ts
+++ b/src/routes/route-change-handlers.ts
@@ -42,7 +42,8 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
const apiClientAuthorizationsMatch = Routes.matchApiClientAuthorizationsRoute(pathname);
const myAccountMatch = Routes.matchMyAccountRoute(pathname);
const linkAccountMatch = Routes.matchLinkAccountRoute(pathname);
- const userMatch = Routes.matchUsersRoute(pathname);
+ const usersMatch = Routes.matchUsersRoute(pathname);
+ const userProfileMatch = Routes.matchUserProfileRoute(pathname);
const groupsMatch = Routes.matchGroupsRoute(pathname);
const groupDetailsMatch = Routes.matchGroupDetailsRoute(pathname);
const linksMatch = Routes.matchLinksRoute(pathname);
@@ -100,11 +101,13 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
} else if (apiClientAuthorizationsMatch) {
store.dispatch(WorkbenchActions.loadApiClientAuthorizations);
} else if (myAccountMatch) {
- store.dispatch(WorkbenchActions.loadMyAccount);
+ store.dispatch(WorkbenchActions.loadUserProfile());
} else if (linkAccountMatch) {
store.dispatch(WorkbenchActions.loadLinkAccount);
- } else if (userMatch) {
+ } else if (usersMatch) {
store.dispatch(WorkbenchActions.loadUsers);
+ } else if (userProfileMatch) {
+ store.dispatch(WorkbenchActions.loadUserProfile(userProfileMatch.params.id));
} else if (groupsMatch) {
store.dispatch(WorkbenchActions.loadGroupsPanel);
} else if (groupDetailsMatch) {
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 41c71f7c..b0eb7918 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -40,6 +40,7 @@ export const Routes = {
LINK_ACCOUNT: '/link_account',
KEEP_SERVICES: `/keep-services`,
USERS: '/users',
+ USER_PROFILE: `/user/:id(${RESOURCE_UUID_PATTERN})`,
API_CLIENT_AUTHORIZATIONS: `/api_client_authorizations`,
GROUPS: '/groups',
GROUP_DETAILS: `/group/:id(${RESOURCE_UUID_PATTERN})`,
@@ -175,6 +176,9 @@ export const matchFedTokenRoute = (route: string) =>
export const matchUsersRoute = (route: string) =>
matchPath(route, { path: Routes.USERS });
+export const matchUserProfileRoute = (route: string) =>
+ matchPath<ResourceRouteParams>(route, { path: Routes.USER_PROFILE });
+
export const matchApiClientAuthorizationsRoute = (route: string) =>
matchPath(route, { path: Routes.API_CLIENT_AUTHORIZATIONS });
diff --git a/src/store/breadcrumbs/breadcrumbs-actions.ts b/src/store/breadcrumbs/breadcrumbs-actions.ts
index 72e908aa..69179272 100644
--- a/src/store/breadcrumbs/breadcrumbs-actions.ts
+++ b/src/store/breadcrumbs/breadcrumbs-actions.ts
@@ -17,6 +17,7 @@ import { updateResources } from '../resources/resources-actions';
import { ResourceKind } from 'models/resource';
import { GroupResource } from 'models/group';
import { extractUuidKind } from 'models/resource';
+import { UserResource } from 'models/user';
export const BREADCRUMBS = 'breadcrumbs';
@@ -112,21 +113,47 @@ export const setProcessBreadcrumbs = (processUuid: string) =>
}
};
-export const GROUPS_PANEL_LABEL = 'Groups';
-
export const setGroupsBreadcrumbs = () =>
- setBreadcrumbs([{ label: GROUPS_PANEL_LABEL }]);
+ setBreadcrumbs([{ label: SidePanelTreeCategory.GROUPS }]);
export const setGroupDetailsBreadcrumbs = (groupUuid: string) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const group = getResource<GroupResource>(groupUuid)(getState().resources);
const breadcrumbs: ResourceBreadcrumb[] = [
- { label: GROUPS_PANEL_LABEL, uuid: GROUPS_PANEL_LABEL },
- { label: group ? group.name : groupUuid, uuid: groupUuid },
+ { label: SidePanelTreeCategory.GROUPS, uuid: SidePanelTreeCategory.GROUPS },
+ { label: group ? group.name : (await services.groupsService.get(groupUuid)).name, uuid: groupUuid },
];
dispatch(setBreadcrumbs(breadcrumbs));
};
+
+export const USERS_PANEL_LABEL = 'Users';
+
+export const setUsersBreadcrumbs = () =>
+ setBreadcrumbs([{ label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL }]);
+
+export const setUserProfileBreadcrumbs = (userUuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+
+ const user = getResource<UserResource>(userUuid)(getState().resources);
+
+ const breadcrumbs: ResourceBreadcrumb[] = [
+ { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
+ { label: user ? user.username : (await services.userService.get(userUuid)).username, uuid: userUuid },
+ ];
+
+ dispatch(setBreadcrumbs(breadcrumbs));
+
+ };
+
+export const MY_ACCOUNT_PANEL_LABEL = 'My Account';
+
+export const setMyAccountBreadcrumbs = () =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(setBreadcrumbs([
+ { label: MY_ACCOUNT_PANEL_LABEL, uuid: MY_ACCOUNT_PANEL_LABEL },
+ ]));
+ };
diff --git a/src/store/group-details-panel/group-details-panel-actions.ts b/src/store/group-details-panel/group-details-panel-actions.ts
index 71ca67c5..f72ac49c 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -16,7 +16,7 @@ import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
import { LinkResource } from 'models/link';
import { deleteResources, updateResources } from 'store/resources/resources-actions';
import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
-// import { UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
+import { UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
@@ -93,7 +93,7 @@ export const removeGroupMember = (uuid: string) =>
});
dispatch<any>(deleteResources([uuid]));
dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
- // dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+ dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
};
diff --git a/src/store/navigation/navigation-action.ts b/src/store/navigation/navigation-action.ts
index 19cc36ae..47d8e4fb 100644
--- a/src/store/navigation/navigation-action.ts
+++ b/src/store/navigation/navigation-action.ts
@@ -11,6 +11,7 @@ import { RootState } from 'store/store';
import { ServiceRepository } from 'services/services';
import { pluginConfig } from 'plugins';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
const navigationNotAvailable = (id: string) =>
snackbarActions.OPEN_SNACKBAR({
@@ -69,6 +70,12 @@ export const navigateTo = (uuid: string) =>
case SidePanelTreeCategory.ALL_PROCESSES:
dispatch(navigateToAllProcesses);
return;
+ case USERS_PANEL_LABEL:
+ dispatch(navigateToUsers);
+ return;
+ case MY_ACCOUNT_PANEL_LABEL:
+ dispatch(navigateToMyAccount);
+ return;
}
dispatch(navigationNotAvailable(uuid));
diff --git a/src/store/store.ts b/src/store/store.ts
index 688c8a05..94f110a0 100644
--- a/src/store/store.ts
+++ b/src/store/store.ts
@@ -49,6 +49,8 @@ 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 { UserProfileGroupsMiddlewareService } from 'store/user-profile/user-profile-groups-middleware-service';
+import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions'
import { GroupsPanelMiddlewareService } from 'store/groups-panel/groups-panel-middleware-service';
import { GROUPS_PANEL_ID } from 'store/groups-panel/groups-panel-actions';
import { GroupDetailsPanelMembersMiddlewareService } from 'store/group-details-panel/group-details-panel-members-middleware-service';
@@ -114,6 +116,9 @@ export function configureStore(history: History, services: ServiceRepository, co
const userPanelMiddleware = dataExplorerMiddleware(
new UserMiddlewareService(services, USERS_PANEL_ID)
);
+ const userProfileGroupsMiddleware = dataExplorerMiddleware(
+ new UserProfileGroupsMiddlewareService(services, USER_PROFILE_PANEL_ID)
+ );
const groupsPanelMiddleware = dataExplorerMiddleware(
new GroupsPanelMiddlewareService(services, GROUPS_PANEL_ID)
);
@@ -160,6 +165,7 @@ export function configureStore(history: History, services: ServiceRepository, co
sharedWithMePanelMiddleware,
workflowPanelMiddleware,
userPanelMiddleware,
+ userProfileGroupsMiddleware,
groupsPanelMiddleware,
groupDetailsPanelMembersMiddleware,
groupDetailsPanelPermissionsMiddleware,
diff --git a/src/store/user-profile/user-profile-actions.ts b/src/store/user-profile/user-profile-actions.ts
new file mode 100644
index 00000000..103456f3
--- /dev/null
+++ b/src/store/user-profile/user-profile-actions.ts
@@ -0,0 +1,79 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import { RootState } from "store/store";
+import { Dispatch } from 'redux';
+import { reset } from "redux-form";
+import { ServiceRepository } from "services/services";
+import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
+import { propertiesActions } from 'store/properties/properties-actions';
+import { getProperty } from 'store/properties/properties';
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { updateResources } from "store/resources/resources-actions";
+import { dialogActions } from "store/dialog/dialog-actions";
+
+export const USER_PROFILE_PANEL_ID = 'userProfilePanel';
+export const USER_PROFILE_FORM = 'userProfileForm';
+export const DEACTIVATE_DIALOG = 'deactivateDialog';
+
+export const UserProfileGroupsActions = bindDataExplorerActions(USER_PROFILE_PANEL_ID);
+
+export const getCurrentUserProfilePanelUuid = getProperty<string>(USER_PROFILE_PANEL_ID);
+
+export const loadUserProfilePanel = (userUuid?: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ // Get user uuid from route or use current user uuid
+ const uuid = userUuid || getState().auth.user?.uuid;
+ if (uuid) {
+ const user = await services.userService.get(uuid);
+ dispatch(updateResources([user]));
+ await dispatch(propertiesActions.SET_PROPERTY({ key: USER_PROFILE_PANEL_ID, value: uuid }));
+ dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+ }
+ }
+
+export const saveEditedUser = (resource: any) =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const user = await services.userService.update(resource.uuid, resource);
+ dispatch(updateResources([user]));
+ dispatch(reset(USER_PROFILE_FORM));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Profile has been updated.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Could not update profile",
+ kind: SnackbarKind.ERROR,
+ }));
+ }
+ };
+
+export const openDeactivateDialog = (uuid: string) =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(dialogActions.OPEN_DIALOG({
+ id: DEACTIVATE_DIALOG,
+ data: {
+ title: 'Deactivate user',
+ text: 'Are you sure you want to deactivate this user?',
+ confirmButtonLabel: 'Deactvate',
+ uuid
+ }
+ }));
+}
+
+export const unsetup = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const user = await services.userService.unsetup(uuid);
+ dispatch(updateResources([user]));
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "User has been deactivated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: "Could not deactivate user",
+ kind: SnackbarKind.ERROR,
+ }));
+ }
+ };
diff --git a/src/store/user-profile/user-profile-groups-middleware-service.ts b/src/store/user-profile/user-profile-groups-middleware-service.ts
new file mode 100644
index 00000000..47c63901
--- /dev/null
+++ b/src/store/user-profile/user-profile-groups-middleware-service.ts
@@ -0,0 +1,61 @@
+// 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, listResultsToDataExplorerItemsMeta } from 'store/data-explorer/data-explorer-middleware-service';
+import { RootState } from 'store/store';
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { getCurrentUserProfilePanelUuid, UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
+import { updateResources } from 'store/resources/resources-actions';
+import { FilterBuilder } from 'services/api/filter-builder';
+import { LinkClass } from 'models/link';
+import { ResourceKind } from 'models/resource';
+
+export class UserProfileGroupsMiddlewareService extends DataExplorerMiddlewareService {
+ constructor(private services: ServiceRepository, id: string) {
+ super(id);
+ }
+
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ const state = api.getState();
+ const userUuid = getCurrentUserProfilePanelUuid(state.properties);
+ try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+
+ // Get user
+ const user = await this.services.userService.get(userUuid || '');
+ api.dispatch(updateResources([user]));
+
+ // Get user's group memberships
+ const groupMembershipLinks = await this.services.permissionService.list({
+ filters: new FilterBuilder()
+ .addEqual('tail_uuid', userUuid)
+ .addEqual('link_class', LinkClass.PERMISSION)
+ .addEqual('head_kind', ResourceKind.GROUP)
+ .getFilters()
+ });
+ api.dispatch(updateResources(groupMembershipLinks.items));
+
+ // Get user's groups details
+ const groups = await this.services.groupsService.list({
+ filters: new FilterBuilder()
+ .addIn('uuid', groupMembershipLinks.items
+ .map(item => item.headUuid))
+ .getFilters(),
+ count: "none"
+ });
+ api.dispatch(updateResources(groups.items));
+
+ api.dispatch(UserProfileGroupsActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(groupMembershipLinks),
+ items: groupMembershipLinks.items.map(item => item.uuid),
+ }));
+ } catch {
+ // api.dispatch(couldNotFetchUsers());
+ } finally {
+ api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
+ }
+ }
+}
diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts
index cd4d5c73..fded1140 100644
--- a/src/store/users/users-actions.ts
+++ b/src/store/users/users-actions.ts
@@ -8,13 +8,16 @@ import { RootState } from 'store/store';
import { getUserUuid } from "common/getuser";
import { ServiceRepository } from "services/services";
import { dialogActions } from 'store/dialog/dialog-actions';
-import { startSubmit, reset } from "redux-form";
+import { startSubmit, reset, initialize, stopSubmit } from "redux-form";
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
import { UserResource } from "models/user";
import { getResource } from 'store/resources/resources';
import { navigateTo, navigateToUsers, navigateToRootProject } from "store/navigation/navigation-action";
import { authActions } from 'store/auth/auth-action';
import { getTokenV2 } from "models/api-client-authorization";
+import { AddLoginFormData, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD } from "store/virtual-machines/virtual-machines-actions";
+import { PermissionLevel } from "models/permission";
+import { updateResources } from "store/resources/resources-actions";
export const USERS_PANEL_ID = 'usersPanel';
export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
@@ -28,11 +31,7 @@ export interface UserCreateFormDialogData {
groupVirtualMachine: string;
}
-export interface SetupShellAccountFormDialogData {
- email: string;
- virtualMachineName: string;
- groupVirtualMachine: string;
-}
+export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
export const openUserAttributes = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
@@ -53,8 +52,8 @@ export const openSetupShellAccount = (uuid: string) =>
const { resources } = getState();
const user = getResource<UserResource>(uuid)(resources);
const virtualMachines = await services.virtualMachineService.list();
- dispatch(dialogActions.CLOSE_DIALOG({ id: USER_MANAGEMENT_DIALOG }));
- dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: { user, ...virtualMachines } }));
+ dispatch(initialize(SETUP_SHELL_ACCOUNT_DIALOG, {[VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: user, [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: []}));
+ dispatch(dialogActions.OPEN_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG, data: virtualMachines }));
};
export const loginAs = (uuid: string) =>
@@ -84,7 +83,6 @@ export const openUserProjects = (uuid: string) =>
dispatch<any>(navigateTo(uuid));
};
-
export const createUser = (user: UserCreateFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(USER_CREATE_FORM_NAME));
@@ -101,20 +99,32 @@ export const createUser = (user: UserCreateFormDialogData) =>
}
};
-
-export const setupUserVM = (setupData: SetupShellAccountFormDialogData) =>
+export const setupUserVM = (setupData: AddLoginFormData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(startSubmit(USER_CREATE_FORM_NAME));
+ dispatch(startSubmit(SETUP_SHELL_ACCOUNT_DIALOG));
try {
- // TODO: make correct API call
- // const setupResult = await services.userService.setup({ ...setupData });
+ const userResource = await services.userService.get(setupData.user.uuid);
+
+ const resources = await services.userService.setup(setupData.user.uuid);
+ dispatch(updateResources(resources.items));
+
+ const permission = await services.permissionService.create({
+ headUuid: setupData.vmUuid,
+ tailUuid: userResource.uuid,
+ name: PermissionLevel.CAN_LOGIN,
+ properties: {
+ username: userResource.username,
+ groups: setupData.groups,
+ }
+ });
+ dispatch(updateResources([permission]));
+
dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_SHELL_ACCOUNT_DIALOG }));
dispatch(reset(SETUP_SHELL_ACCOUNT_DIALOG));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been added to VM.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
- dispatch<any>(loadUsersPanel());
- dispatch(userBindedActions.REQUEST_ITEMS());
} catch (e) {
- return;
+ dispatch(stopSubmit(SETUP_SHELL_ACCOUNT_DIALOG));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
};
@@ -154,13 +164,6 @@ export const toggleIsAdmin = (uuid: string) =>
return newActivity;
};
-export const userBindedActions = bindDataExplorerActions(USERS_PANEL_ID);
-
-export const loadUsersData = () =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- await services.userService.list({ count: "none" });
- };
-
export const loadUsersPanel = () =>
(dispatch: Dispatch) => {
dispatch(userBindedActions.REQUEST_ITEMS());
diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts
index e2cf6fd4..7034b4a5 100644
--- a/src/store/virtual-machines/virtual-machines-actions.ts
+++ b/src/store/virtual-machines/virtual-machines-actions.ts
@@ -157,7 +157,7 @@ export const addUpdateVirtualMachineLogin = ({uuid, vmUuid, user, groups}: AddLo
dispatch(updateResources([permission]));
} else {
const permission = await services.permissionService.create({
- headUuid: vmUuid,
+ headUuid: vmUuid,
tailUuid: userResource.uuid,
name: PermissionLevel.CAN_LOGIN,
properties: {
diff --git a/src/store/workbench/workbench-actions.ts b/src/store/workbench/workbench-actions.ts
index 98508f75..5e83ed7b 100644
--- a/src/store/workbench/workbench-actions.ts
+++ b/src/store/workbench/workbench-actions.ts
@@ -31,7 +31,10 @@ import {
setProcessBreadcrumbs,
setSharedWithMeBreadcrumbs,
setSidePanelBreadcrumbs,
- setTrashBreadcrumbs
+ setTrashBreadcrumbs,
+ setUsersBreadcrumbs,
+ setMyAccountBreadcrumbs,
+ setUserProfileBreadcrumbs,
} from 'store/breadcrumbs/breadcrumbs-actions';
import { navigateTo, navigateToRootProject } from 'store/navigation/navigation-action';
import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
@@ -58,7 +61,6 @@ import {
import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
import { loadWorkflowPanel, workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
import { loadSshKeysPanel } from 'store/auth/auth-action-ssh';
-import { loadMyAccountPanel } from 'store/my-account/my-account-panel-actions';
import { loadLinkAccountPanel, linkAccountPanelActions } from 'store/link-account-panel/link-account-panel-actions';
import { loadSiteManagerPanel } from 'store/auth/auth-action-session';
import { workflowPanelColumns } from 'views/workflow-panel/workflow-panel-view';
@@ -80,6 +82,7 @@ import { loadVirtualMachinesPanel } from 'store/virtual-machines/virtual-machine
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 * as userProfilePanelActions from 'store/user-profile/user-profile-actions';
import { linkPanelActions, loadLinkPanel } from 'store/link-panel/link-panel-actions';
import { linkPanelColumns } from 'views/link-panel/link-panel-root';
import { userPanelColumns } from 'views/user-panel/user-panel';
@@ -101,6 +104,7 @@ import { allProcessesPanelColumns } from 'views/all-processes-panel/all-processe
import { collectionPanelFilesAction } from '../collection-panel/collection-panel-files/collection-panel-files-actions';
import { createTree } from 'models/tree';
import { AdminMenuIcon } from 'components/icon/icon';
+import { userProfileGroupsColumns } from 'views/user-profile-panel/user-profile-panel-root';
export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
@@ -139,6 +143,7 @@ export const loadWorkbench = () =>
dispatch(groupPanelActions.GroupsPanelActions.SET_COLUMNS({ columns: groupsPanelColumns }));
dispatch(groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({ columns: groupDetailsMembersPanelColumns }));
dispatch(groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({ columns: groupDetailsPermissionsPanelColumns }));
+ dispatch(userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({ columns: userProfileGroupsColumns }));
dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
dispatch(apiClientAuthorizationsActions.SET_COLUMNS({ columns: apiClientAuthorizationPanelColumns }));
dispatch(collectionsContentAddressActions.SET_COLUMNS({ columns: collectionContentAddressPanelColumns }));
@@ -514,10 +519,18 @@ export const loadSiteManager = handleFirstTimeLoad(
await dispatch(loadSiteManagerPanel());
});
-export const loadMyAccount = handleFirstTimeLoad(
- (dispatch: Dispatch<any>) => {
- dispatch(loadMyAccountPanel());
- });
+export const loadUserProfile = (userUuid?: string) =>
+ handleFirstTimeLoad(
+ (dispatch: Dispatch<any>) => {
+ if (userUuid) {
+ dispatch(setUserProfileBreadcrumbs(userUuid));
+ dispatch(userProfilePanelActions.loadUserProfilePanel(userUuid));
+ } else {
+ dispatch(setMyAccountBreadcrumbs());
+ dispatch(userProfilePanelActions.loadUserProfilePanel());
+ }
+ }
+ );
export const loadLinkAccount = handleFirstTimeLoad(
(dispatch: Dispatch<any>) => {
@@ -532,7 +545,7 @@ export const loadKeepServices = handleFirstTimeLoad(
export const loadUsers = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
await dispatch(loadUsersPanel());
- dispatch(setBreadcrumbs([{ label: 'Users' }]));
+ dispatch(setUsersBreadcrumbs());
});
export const loadApiClientAuthorizations = handleFirstTimeLoad(
diff --git a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
index 3bf700ba..666ea38e 100644
--- a/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
+++ b/src/views-components/dialog-forms/setup-shell-account-dialog.tsx
@@ -8,15 +8,17 @@ import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
import { FormDialog } from 'components/form-dialog/form-dialog';
import { TextField } from 'components/text-field/text-field';
import { VirtualMachinesResource } from 'models/virtual-machines';
-import { USER_LENGTH_VALIDATION, CHOOSE_VM_VALIDATION } from 'validators/validators';
+import { CHOOSE_VM_VALIDATION } from 'validators/validators';
import { InputLabel } from '@material-ui/core';
import { NativeSelectField } from 'components/select-field/select-field';
-import { SetupShellAccountFormDialogData, SETUP_SHELL_ACCOUNT_DIALOG, setupUserVM } from 'store/users/users-actions';
+import { SETUP_SHELL_ACCOUNT_DIALOG, setupUserVM } from 'store/users/users-actions';
import { UserResource } from 'models/user';
+import { VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, AddLoginFormData } from 'store/virtual-machines/virtual-machines-actions';
+import { GroupArrayInput } from 'views-components/virtual-machines-dialog/group-array-input';
export const SetupShellAccountDialog = compose(
withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
- reduxForm<SetupShellAccountFormDialogData>({
+ reduxForm<AddLoginFormData>({
form: SETUP_SHELL_ACCOUNT_DIALOG,
onSubmit: (data, dispatch) => {
dispatch(setupUserVM(data));
@@ -32,12 +34,6 @@ export const SetupShellAccountDialog = compose(
/>
);
-interface UserProps {
- data: {
- user: UserResource;
- };
-}
-
interface VirtualMachinesProps {
data: {
items: VirtualMachinesResource[];
@@ -48,39 +44,39 @@ interface DataProps {
items: VirtualMachinesResource[];
}
-const UserEmailField = ({ data }: UserProps) =>
+const UserNameField = () =>
<span>
+ <InputLabel>VM Login</InputLabel>
<Field
- name='email'
+ name={`${VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD}.username`}
component={TextField as any}
- disabled
- label={data.user.email} /></span>;
+ disabled /></span>;
const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
<div style={{ marginBottom: '21px' }}>
<InputLabel>Virtual Machine</InputLabel>
<Field
- name='virtualMachine'
+ name={VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD}
component={NativeSelectField as any}
validate={CHOOSE_VM_VALIDATION}
items={getVirtualMachinesList(data.items)} />
</div>;
const UserGroupsVirtualMachineField = () =>
- <Field
- name='groups'
- component={TextField as any}
- validate={USER_LENGTH_VALIDATION}
- label="Groups for virtual machine (comma separated list)" />;
+ <GroupArrayInput
+ name={VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD}
+ input={{id:"Add groups to VM login (eg: docker, sudo)", disabled:false}}
+ required={false}
+ />
const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
- [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.hostname, value: it.hostname })));
+ [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.uuid, value: it.hostname })));
-type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
+type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<AddLoginFormData>;
const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
<>
- <UserEmailField data={props.data as DataProps} />
+ <UserNameField />
<UserVirtualMachineField data={props.data as DataProps} />
<UserGroupsVirtualMachineField />
</>;
diff --git a/src/views-components/user-dialog/deactivate-dialog.tsx b/src/views-components/user-dialog/deactivate-dialog.tsx
new file mode 100644
index 00000000..6babf367
--- /dev/null
+++ b/src/views-components/user-dialog/deactivate-dialog.tsx
@@ -0,0 +1,21 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch, compose } from 'redux';
+import { connect } from "react-redux";
+import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
+import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
+import { unsetup, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(unsetup(props.data.uuid));
+ }
+});
+
+export const DeactivateDialog = compose(
+ withDialog(DEACTIVATE_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
diff --git a/src/views-components/user-dialog/manage-dialog.tsx b/src/views-components/user-dialog/manage-dialog.tsx
index b812f5cb..a62e1a21 100644
--- a/src/views-components/user-dialog/manage-dialog.tsx
+++ b/src/views-components/user-dialog/manage-dialog.tsx
@@ -10,7 +10,8 @@ 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 { USER_MANAGEMENT_DIALOG, openSetupShellAccount, loginAs } from "store/users/users-actions";
+import { USER_MANAGEMENT_DIALOG } from "store/users/users-actions";
+import { openSetupShellAccount, loginAs } from 'store/users/users-actions';
import { getUserDisplayName } from "models/user";
type CssRules = 'spacing';
diff --git a/src/views/my-account-panel/my-account-panel-root.tsx b/src/views/my-account-panel/my-account-panel-root.tsx
deleted file mode 100644
index 283b9acc..00000000
--- a/src/views/my-account-panel/my-account-panel-root.tsx
+++ /dev/null
@@ -1,164 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from 'react';
-import { Field, InjectedFormProps, WrappedFieldProps } from "redux-form";
-import { TextField } from "components/text-field/text-field";
-import { NativeSelectField } from "components/select-field/select-field";
-import {
- StyleRulesCallback,
- WithStyles,
- withStyles,
- Card,
- CardContent,
- Button,
- Typography,
- Grid,
- InputLabel
-} from '@material-ui/core';
-import { ArvadosTheme } from 'common/custom-theme';
-import { User } from "models/user";
-import { MY_ACCOUNT_VALIDATION } from "validators/validators";
-
-type CssRules = 'root' | 'gridItem' | 'label' | 'title' | 'actions';
-
-const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- root: {
- width: '100%',
- overflow: 'auto'
- },
- gridItem: {
- height: 45,
- marginBottom: 20
- },
- label: {
- fontSize: '0.675rem'
- },
- title: {
- marginBottom: theme.spacing.unit * 3,
- color: theme.palette.grey["600"]
- },
- actions: {
- display: 'flex',
- justifyContent: 'flex-end'
- }
-});
-
-export interface MyAccountPanelRootActionProps { }
-
-export interface MyAccountPanelRootDataProps {
- isPristine: boolean;
- isValid: boolean;
- initialValues?: User;
- localCluster: string;
-}
-
-const RoleTypes = [
- { key: 'Bio-informatician', value: 'Bio-informatician' },
- { key: 'Data Scientist', value: 'Data Scientist' },
- { key: 'Analyst', value: 'Analyst' },
- { key: 'Researcher', value: 'Researcher' },
- { key: 'Software Developer', value: 'Software Developer' },
- { key: 'System Administrator', value: 'System Administrator' },
- { key: 'Other', value: 'Other' }
-];
-
-type MyAccountPanelRootProps = InjectedFormProps<MyAccountPanelRootActionProps> & MyAccountPanelRootDataProps & WithStyles<CssRules>;
-
-type LocalClusterProp = { localCluster: string };
-const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
- <span>{localCluster === input.value.substring(0, 5) ? "" : "federated"} user {input.value}</span>
-);
-
-export const MyAccountPanelRoot = withStyles(styles)(
- ({ classes, isValid, handleSubmit, reset, isPristine, invalid, submitting, localCluster }: MyAccountPanelRootProps) => {
- return <Card className={classes.root}>
- <CardContent>
- <Typography variant="title" className={classes.title}>
- Logged in as <Field name="uuid" component={renderField} localCluster={localCluster} />
- </Typography>
- <form onSubmit={handleSubmit}>
- <Grid container spacing={24}>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <Field
- label="First name"
- name="firstName"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <Field
- label="Last name"
- name="lastName"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <Field
- label="E-mail"
- name="email"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <Field
- label="Username"
- name="username"
- component={TextField as any}
- disabled
- />
- </Grid>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <Field
- label="Organization"
- name="prefs.profile.organization"
- component={TextField as any}
- validate={MY_ACCOUNT_VALIDATION}
- required
- />
- </Grid>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <Field
- label="E-mail at Organization"
- name="prefs.profile.organization_email"
- component={TextField as any}
- validate={MY_ACCOUNT_VALIDATION}
- required
- />
- </Grid>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <InputLabel className={classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
- <Field
- id="prefs.profile.role"
- name="prefs.profile.role"
- component={NativeSelectField as any}
- items={RoleTypes}
- />
- </Grid>
- <Grid item className={classes.gridItem} sm={6} xs={12}>
- <Field
- label="Website"
- name="prefs.profile.website_url"
- component={TextField as any}
- />
- </Grid>
- <Grid container direction="row" justify="flex-end" >
- <Button color="primary" onClick={reset} disabled={isPristine}>Discard changes</Button>
- <Button
- color="primary"
- variant="contained"
- type="submit"
- disabled={isPristine || invalid || submitting}>
- Save changes
- </Button>
- </Grid>
- </Grid>
- </form >
- </CardContent >
- </Card >;
- }
-);
diff --git a/src/views/my-account-panel/my-account-panel.tsx b/src/views/my-account-panel/my-account-panel.tsx
deleted file mode 100644
index 2421a28a..00000000
--- a/src/views/my-account-panel/my-account-panel.tsx
+++ /dev/null
@@ -1,27 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { RootState } from 'store/store';
-import { compose } from 'redux';
-import { reduxForm, isPristine, isValid } from 'redux-form';
-import { connect } from 'react-redux';
-import { saveEditedUser } from 'store/my-account/my-account-panel-actions';
-import { MyAccountPanelRoot, MyAccountPanelRootDataProps } from 'views/my-account-panel/my-account-panel-root';
-import { MY_ACCOUNT_FORM } from "store/my-account/my-account-panel-actions";
-
-const mapStateToProps = (state: RootState): MyAccountPanelRootDataProps => ({
- isPristine: isPristine(MY_ACCOUNT_FORM)(state),
- isValid: isValid(MY_ACCOUNT_FORM)(state),
- initialValues: state.auth.user,
- localCluster: state.auth.localCluster
-});
-
-export const MyAccountPanel = compose(
- connect(mapStateToProps),
- reduxForm({
- form: MY_ACCOUNT_FORM,
- onSubmit: (data, dispatch) => {
- dispatch(saveEditedUser(data));
- }
- }))(MyAccountPanelRoot);
diff --git a/src/views/user-profile-panel/user-profile-panel-root.tsx b/src/views/user-profile-panel/user-profile-panel-root.tsx
new file mode 100644
index 00000000..c0c80e3c
--- /dev/null
+++ b/src/views/user-profile-panel/user-profile-panel-root.tsx
@@ -0,0 +1,368 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { Field, InjectedFormProps } from "redux-form";
+import { TextField } from "components/text-field/text-field";
+import { DataExplorer } from "views-components/data-explorer/data-explorer";
+import { NativeSelectField } from "components/select-field/select-field";
+import {
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ Card,
+ CardContent,
+ Button,
+ Typography,
+ Grid,
+ InputLabel,
+ Tabs, Tab,
+ Paper
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { User } from "models/user";
+import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
+import { MY_ACCOUNT_VALIDATION } from "validators/validators";
+import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
+import { noop } from 'lodash';
+import { GroupsIcon } from 'components/icon/icon';
+import { DataColumns } from 'components/data-table/data-table';
+import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
+import { createTree } from 'models/tree';
+
+type CssRules = 'root' | 'adminRoot' | 'gridItem' | 'label' | 'title' | 'description' | 'actions' | 'content';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ overflow: 'auto'
+ },
+ adminRoot: {
+ // ...theme.mixins.gutters()
+ },
+ gridItem: {
+ height: 45,
+ marginBottom: 20
+ },
+ label: {
+ fontSize: '0.675rem'
+ },
+ title: {
+ fontSize: '1.1rem',
+ },
+ description: {
+ color: theme.palette.grey["600"]
+ },
+ actions: {
+ display: 'flex',
+ justifyContent: 'flex-end'
+ },
+ content: {
+ // reserve space for the tab bar
+ height: `calc(100% - ${theme.spacing.unit * 7}px)`,
+ }
+});
+
+export interface UserProfilePanelRootActionProps {
+ openSetupShellAccount: (uuid: string) => void;
+ loginAs: (uuid: string) => void;
+ openDeactivateDialog: (uuid: string) => void;
+}
+
+export interface UserProfilePanelRootDataProps {
+ isPristine: boolean;
+ isValid: boolean;
+ initialValues?: User;
+ localCluster: string;
+}
+
+const RoleTypes = [
+ { key: 'Bio-informatician', value: 'Bio-informatician' },
+ { key: 'Data Scientist', value: 'Data Scientist' },
+ { key: 'Analyst', value: 'Analyst' },
+ { key: 'Researcher', value: 'Researcher' },
+ { key: 'Software Developer', value: 'Software Developer' },
+ { key: 'System Administrator', value: 'System Administrator' },
+ { key: 'Other', value: 'Other' }
+];
+
+type UserProfilePanelRootProps = InjectedFormProps<{}> & UserProfilePanelRootActionProps & UserProfilePanelRootDataProps & WithStyles<CssRules>;
+
+// type LocalClusterProp = { localCluster: string };
+// const renderField: React.ComponentType<WrappedFieldProps & LocalClusterProp> = ({ input, localCluster }) => (
+// <span>{localCluster === input.value.substring(0, 5) ? "" : "federated"} user {input.value}</span>
+// );
+
+export enum UserProfileGroupsColumnNames {
+ NAME = "Name",
+ PERMISSION = "Permission",
+ VISIBLE = "Visible to other members",
+ UUID = "UUID",
+ REMOVE = "Remove",
+}
+
+export const userProfileGroupsColumns: DataColumns<string> = [
+ {
+ name: UserProfileGroupsColumnNames.NAME,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkHead uuid={uuid} />
+ },
+ {
+ name: UserProfileGroupsColumnNames.PERMISSION,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkHeadPermissionLevel uuid={uuid} />
+ },
+ {
+ name: UserProfileGroupsColumnNames.VISIBLE,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkTailIsVisible uuid={uuid} />
+ },
+ {
+ name: UserProfileGroupsColumnNames.UUID,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkHeadUuid uuid={uuid} />
+ },
+ {
+ name: UserProfileGroupsColumnNames.REMOVE,
+ selected: true,
+ configurable: true,
+ filters: createTree(),
+ render: uuid => <ResourceLinkDelete uuid={uuid} />
+ },
+];
+
+export const UserProfilePanelRoot = withStyles(styles)(
+ class extends React.Component<UserProfilePanelRootProps> {
+ state = {
+ value: 0,
+ };
+
+ componentDidMount() {
+ this.setState({ value: 0 });
+ }
+
+ render() {
+ return <Paper className={this.props.classes.root}>
+ {/* <Typography variant="title" className={this.props.classes.title}>
+ Logged in as <Field name="uuid" component={renderField} localCluster={this.props.localCluster} />
+ </Typography> */}
+ <Tabs value={this.state.value} onChange={this.handleChange} fullWidth>
+ <Tab label="PROFILE" />
+ <Tab label="GROUPS" />
+ <Tab label="ADMIN" />
+ </Tabs>
+ {this.state.value === 0 &&
+ // <Card className={this.props.classes.root}>
+ <CardContent>
+ <form onSubmit={this.props.handleSubmit}>
+ <Grid container spacing={24}>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="First name"
+ name="firstName"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Last name"
+ name="lastName"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="E-mail"
+ name="email"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Username"
+ name="username"
+ component={TextField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Organization"
+ name="prefs.profile.organization"
+ component={TextField as any}
+ validate={MY_ACCOUNT_VALIDATION}
+ required
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="E-mail at Organization"
+ name="prefs.profile.organization_email"
+ component={TextField as any}
+ validate={MY_ACCOUNT_VALIDATION}
+ required
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <InputLabel className={this.props.classes.label} htmlFor="prefs.profile.role">Role</InputLabel>
+ <Field
+ id="prefs.profile.role"
+ name="prefs.profile.role"
+ component={NativeSelectField as any}
+ items={RoleTypes}
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12}>
+ <Field
+ label="Website"
+ name="prefs.profile.website_url"
+ component={TextField as any}
+ />
+ </Grid>
+ <Grid item sm={12}>
+ <Grid container direction="row" justify="flex-end">
+ <Button color="primary" onClick={this.props.reset} disabled={this.props.isPristine}>Discard changes</Button>
+ <Button
+ color="primary"
+ variant="contained"
+ type="submit"
+ disabled={this.props.isPristine || this.props.invalid || this.props.submitting}>
+ Save changes
+ </Button>
+ </Grid>
+ </Grid>
+ </Grid>
+ </form >
+ </CardContent>
+ // </Card>
+ }
+ {this.state.value === 1 &&
+ <div className={this.props.classes.content}>
+ <DataExplorer
+ id={USER_PROFILE_PANEL_ID}
+ onRowClick={noop}
+ onRowDoubleClick={noop}
+ // onContextMenu={this.handleContextMenu}
+ contextMenuColumn={false}
+ hideColumnSelector
+ hideSearchInput
+ paperProps={{
+ elevation: 0,
+ }}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={GroupsIcon}
+ messages={['Group list is empty.']} />
+ } />
+ </div>}
+ {this.state.value === 2 &&
+ <Paper elevation={0} className={this.props.classes.adminRoot}>
+ <Card elevation={0}>
+ <CardContent>
+ <Grid container
+ direction="row"
+ justify={'flex-end'}
+ alignItems={'center'}>
+ <Grid item xs>
+ <Typography variant="h6" className={this.props.classes.title}>
+ Setup Account
+ </Typography>
+ <Typography variant="body1" className={this.props.classes.description}>
+ This button sets up a user. After setup, they will be able use Arvados. This dialog box also allows you to optionally set up a shell account for this user. The login name is automatically generated from the user's e-mail address.
+ </Typography>
+ </Grid>
+ <Grid item sm={'auto'} xs={12}>
+ <Button variant="contained"
+ color="primary"
+ onClick={() => {this.props.openSetupShellAccount(this.props.initialValues.uuid)}}
+ disabled={false}>
+ Setup Account
+ </Button>
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+ <Card elevation={0}>
+ <CardContent>
+ <Grid container
+ direction="row"
+ justify={'flex-end'}
+ alignItems={'center'}>
+ <Grid item xs>
+ <Typography variant="h6" className={this.props.classes.title}>
+ Deactivate
+ </Typography>
+ <Typography variant="body1" className={this.props.classes.description}>
+ As an admin, you can deactivate and reset this user. This will remove all repository/VM permissions for the user. If you "setup" the user again, the user will have to sign the user agreement again. You may also want to reassign data ownership.
+ </Typography>
+ </Grid>
+ <Grid item sm={'auto'} xs={12}>
+ <Button variant="contained"
+ color="primary"
+ onClick={() => {this.props.openDeactivateDialog(this.props.initialValues.uuid)}}
+ disabled={false}>
+ Deactivate
+ </Button>
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+ <Card elevation={0}>
+ <CardContent>
+ <Grid container
+ direction="row"
+ justify={'flex-end'}
+ alignItems={'center'}>
+ <Grid item xs>
+ <Typography variant="h6" className={this.props.classes.title}>
+ Log In
+ </Typography>
+ <Typography variant="body1" className={this.props.classes.description}>
+ As an admin, you can log in as this user. When you’ve finished, you will need to log out and log in again with your own account.
+ </Typography>
+ </Grid>
+ <Grid item sm={'auto'} xs={12}>
+ <Button variant="contained"
+ color="primary"
+ onClick={() => {this.props.loginAs(this.props.initialValues.uuid)}}
+ disabled={false}>
+ Log In
+ </Button>
+ </Grid>
+ </Grid>
+ </CardContent>
+ </Card>
+ </Paper>}
+ </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
+ // });
+ // }
+ }
+ }
+);
diff --git a/src/views/user-profile-panel/user-profile-panel.tsx b/src/views/user-profile-panel/user-profile-panel.tsx
new file mode 100644
index 00000000..caac3e8c
--- /dev/null
+++ b/src/views/user-profile-panel/user-profile-panel.tsx
@@ -0,0 +1,46 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from 'store/store';
+import { compose, Dispatch } from 'redux';
+import { reduxForm, isPristine, isValid } from 'redux-form';
+import { connect } from 'react-redux';
+import { saveEditedUser } from 'store/user-profile/user-profile-actions';
+import { UserProfilePanelRoot, UserProfilePanelRootDataProps } from 'views/user-profile-panel/user-profile-panel-root';
+import { openDeactivateDialog, USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
+import { matchUserProfileRoute } from 'routes/routes';
+import { UserResource } from 'models/user';
+import { getResource } from 'store/resources/resources';
+import { openSetupShellAccount, loginAs } from 'store/users/users-actions';
+
+const mapStateToProps = (state: RootState): UserProfilePanelRootDataProps => {
+ const pathname = state.router.location ? state.router.location.pathname : '';
+ const match = matchUserProfileRoute(pathname);
+ const uuid = match ? match.params.id : state.auth.user?.uuid || '';
+ // get user resource
+ const user = getResource<UserResource>(uuid)(state.resources);
+ // const subprocesses = getSubprocesses(uuid)(resources);
+
+ return {
+
+ isPristine: isPristine(USER_PROFILE_FORM)(state),
+ isValid: isValid(USER_PROFILE_FORM)(state),
+ initialValues: user,
+ localCluster: state.auth.localCluster
+}};
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
+ loginAs: (uuid: string) => dispatch<any>(loginAs(uuid)),
+ openDeactivateDialog: (uuid: string) => dispatch<any>(openDeactivateDialog(uuid)),
+});
+
+export const UserProfilePanel = compose(
+ connect(mapStateToProps, mapDispatchToProps),
+ reduxForm({
+ form: USER_PROFILE_FORM,
+ onSubmit: (data, dispatch) => {
+ dispatch(saveEditedUser(data));
+ }
+ }))(UserProfilePanelRoot);
diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx
index 49922202..1202529c 100644
--- a/src/views/workbench/workbench.tsx
+++ b/src/views/workbench/workbench.tsx
@@ -47,7 +47,7 @@ import { SearchResultsPanel } from 'views/search-results-panel/search-results-pa
import { SshKeyPanel } from 'views/ssh-key-panel/ssh-key-panel';
import { SshKeyAdminPanel } from 'views/ssh-key-panel/ssh-key-admin-panel';
import { SiteManagerPanel } from "views/site-manager-panel/site-manager-panel";
-import { MyAccountPanel } from 'views/my-account-panel/my-account-panel';
+import { UserProfilePanel } from 'views/user-profile-panel/user-profile-panel';
import { SharingDialog } from 'views-components/sharing-dialog/sharing-dialog';
import { NotFoundDialog } from 'views-components/not-found-dialog/not-found-dialog';
import { AdvancedTabDialog } from 'views-components/advanced-tab-dialog/advanced-tab-dialog';
@@ -81,6 +81,7 @@ import { UserAttributesDialog } from 'views-components/user-dialog/attributes-di
import { CreateUserDialog } from 'views-components/dialog-forms/create-user-dialog';
import { HelpApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/help-dialog';
import { UserManageDialog } from 'views-components/user-dialog/manage-dialog';
+import { DeactivateDialog } from 'views-components/user-dialog/deactivate-dialog';
import { SetupShellAccountDialog } from 'views-components/dialog-forms/setup-shell-account-dialog';
import { GroupsPanel } from 'views/groups-panel/groups-panel';
import { RemoveGroupDialog } from 'views-components/groups-dialog/remove-dialog';
@@ -172,7 +173,8 @@ let routes = <>
<Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
<Route path={Routes.USERS} component={UserPanel} />
<Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
- <Route path={Routes.MY_ACCOUNT} component={MyAccountPanel} />
+ <Route path={Routes.MY_ACCOUNT} component={UserProfilePanel} />
+ <Route path={Routes.USER_PROFILE} component={UserProfilePanel} />
<Route path={Routes.GROUPS} component={GroupsPanel} />
<Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
<Route path={Routes.LINKS} component={LinkPanel} />
@@ -268,6 +270,7 @@ export const WorkbenchPanel =
<UpdateProjectDialog />
<UserAttributesDialog />
<UserManageDialog />
+ <DeactivateDialog />
<VirtualMachineAttributesDialog />
<FedLogin />
<WebDavS3InfoDialog />
commit 4e862392eae3d1a1846b3f33c6e29f4e68c31aca
Author: Stephen Smith <stephen at curii.com>
Date: Wed Mar 2 09:28:36 2022 -0500
18559: Add user service setup and add return type for unsetup.
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/services/user-service/user-service.ts b/src/services/user-service/user-service.ts
index e6560c84..97cb3c71 100644
--- a/src/services/user-service/user-service.ts
+++ b/src/services/user-service/user-service.ts
@@ -6,6 +6,7 @@ import { AxiosInstance } from "axios";
import { CommonResourceService } from "services/common-service/common-resource-service";
import { UserResource } from "models/user";
import { ApiActions } from "services/api/api-actions";
+import { ListResults } from "services/common-service/common-service";
export class UserService extends CommonResourceService<UserResource> {
constructor(serverApi: AxiosInstance, actions: ApiActions, readOnlyFields: string[] = []) {
@@ -24,8 +25,16 @@ export class UserService extends CommonResourceService<UserResource> {
);
}
+ setup(uuid: string) {
+ return CommonResourceService.defaultResponse<ListResults<any>>(
+ this.serverApi
+ .post(this.resourceType + `/setup`, {}, { params: { uuid } }),
+ this.actions
+ );
+ }
+
unsetup(uuid: string) {
- return CommonResourceService.defaultResponse(
+ return CommonResourceService.defaultResponse<UserResource>(
this.serverApi
.post(this.resourceType + `/${uuid}/unsetup`),
this.actions
commit 3583b37935585f9b19605d98c47ccef73c23cb15
Author: Stephen Smith <stephen at curii.com>
Date: Mon Feb 28 21:16:28 2022 -0500
18559: Add read only fields to user service
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/services/user-service/user-service.ts b/src/services/user-service/user-service.ts
index dbbd5be8..e6560c84 100644
--- a/src/services/user-service/user-service.ts
+++ b/src/services/user-service/user-service.ts
@@ -8,8 +8,12 @@ import { UserResource } from "models/user";
import { ApiActions } from "services/api/api-actions";
export class UserService extends CommonResourceService<UserResource> {
- constructor(serverApi: AxiosInstance, actions: ApiActions) {
- super(serverApi, "users", actions);
+ constructor(serverApi: AxiosInstance, actions: ApiActions, readOnlyFields: string[] = []) {
+ super(serverApi, "users", actions, readOnlyFields.concat([
+ 'fullName',
+ 'isInvited',
+ 'writableBy',
+ ]));
}
activate(uuid: string) {
commit 33963600639e6e7f3cd4afea9d4210ee815c180b
Author: Stephen Smith <stephen at curii.com>
Date: Mon Feb 28 18:23:07 2022 -0500
18559: Reduce unnecessary reloading in groups panel
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/src/store/group-details-panel/group-details-panel-actions.ts b/src/store/group-details-panel/group-details-panel-actions.ts
index e00ff773..71ca67c5 100644
--- a/src/store/group-details-panel/group-details-panel-actions.ts
+++ b/src/store/group-details-panel/group-details-panel-actions.ts
@@ -14,8 +14,9 @@ import { ServiceRepository } from 'services/services';
import { PermissionResource, PermissionLevel } from 'models/permission';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
import { LinkResource } from 'models/link';
-import { deleteResources } from 'store/resources/resources-actions';
+import { deleteResources, updateResources } from 'store/resources/resources-actions';
import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
+// import { UserProfileGroupsActions } from 'store/user-profile/user-profile-actions';
export const GROUP_DETAILS_MEMBERS_PANEL_ID = 'groupDetailsMembersPanel';
export const GROUP_DETAILS_PERMISSIONS_PANEL_ID = 'groupDetailsPermissionsPanel';
@@ -48,9 +49,8 @@ export const openAddGroupMembersDialog = () =>
export const editPermissionLevel = (uuid: string, level: PermissionLevel) =>
async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
try {
- await permissionService.update(uuid, {name: level});
- dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
- dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
+ const permission = await permissionService.update(uuid, {name: level});
+ dispatch(updateResources([permission]));
dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Permission level changed.', hideDuration: 2000 }));
} catch (e) {
dispatch(snackbarActions.OPEN_SNACKBAR({
@@ -83,25 +83,19 @@ export const openRemoveGroupMemberDialog = (uuid: string) =>
export const removeGroupMember = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, { permissionService }: ServiceRepository) => {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
+ await deleteGroupMember({
+ link: {
+ uuid,
+ },
+ permissionService,
+ dispatch,
+ });
+ dispatch<any>(deleteResources([uuid]));
+ dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
+ // dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
- const groupUuid = getCurrentGroupDetailsPanelUuid(getState().properties);
-
- if (groupUuid) {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
-
- await deleteGroupMember({
- link: {
- uuid,
- },
- permissionService,
- dispatch,
- });
-
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
- dispatch(GroupMembersPanelActions.REQUEST_ITEMS());
-
- }
-
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
};
export const setMemberIsHidden = (memberLinkUuid: string, permissionLinkUuid: string, visible: boolean) =>
@@ -113,7 +107,6 @@ export const setMemberIsHidden = (memberLinkUuid: string, permissionLinkUuid: st
try {
await permissionService.delete(permissionLinkUuid);
dispatch<any>(deleteResources([permissionLinkUuid]));
- dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
dispatch(snackbarActions.OPEN_SNACKBAR({
message: 'Removed read permission.',
hideDuration: 2000,
@@ -128,17 +121,17 @@ export const setMemberIsHidden = (memberLinkUuid: string, permissionLinkUuid: st
} else if (visible && memberLink) {
// Create read permission
try {
- await permissionService.create({
+ const permission = await permissionService.create({
headUuid: memberLink.tailUuid,
tailUuid: memberLink.headUuid,
name: PermissionLevel.CAN_READ,
});
+ dispatch(updateResources([permission]));
dispatch(snackbarActions.OPEN_SNACKBAR({
message: 'Created read permission.',
hideDuration: 2000,
kind: SnackbarKind.SUCCESS,
}));
- dispatch(GroupPermissionsPanelActions.REQUEST_ITEMS());
} catch(e) {
dispatch(snackbarActions.OPEN_SNACKBAR({
message: 'Failed to create permission',
diff --git a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
index e6f18f7f..3a58927a 100644
--- a/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
+++ b/src/store/group-details-panel/group-details-panel-members-middleware-service.ts
@@ -24,7 +24,8 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
const groupUuid = getCurrentGroupDetailsPanelUuid(api.getState().properties);
if (!dataExplorer || !groupUuid) {
- api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
+ // Noop if data explorer refresh is triggered from another panel
+ return;
} else {
try {
const groupResource = await this.services.groupsService.get(groupUuid);
@@ -69,12 +70,6 @@ export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddl
}
}
-const groupsDetailsPanelDataExplorerIsNotSet = () =>
- snackbarActions.OPEN_SNACKBAR({
- message: 'Group members panel is not ready.',
- kind: SnackbarKind.ERROR
- });
-
const couldNotFetchGroupDetailsContents = () =>
snackbarActions.OPEN_SNACKBAR({
message: 'Could not fetch group members.',
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list