--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('User profile tests', function() {
+ let activeUser;
+ let adminUser;
+ const roleGroupName = `Test role group (${Math.floor(999999 * Math.random())})`;
+ const projectGroupName = `Test project group (${Math.floor(999999 * Math.random())})`;
+
+ before(function() {
+ // Only set up common users once. These aren't set up as aliases because
+ // aliases are cleaned up after every test. Also it doesn't make sense
+ // to set the same users on beforeEach() over and over again, so we
+ // separate a little from Cypress' 'Best Practices' here.
+ cy.getUser('admin', 'Admin', 'User', true, true)
+ .as('adminUser').then(function() {
+ adminUser = this.adminUser;
+ }
+ );
+ cy.getUser('user', 'Active', 'User', false, true)
+ .as('activeUser').then(function() {
+ activeUser = this.activeUser;
+ }
+ );
+ });
+
+ function assertProfileValues({
+ firstName,
+ lastName,
+ email,
+ username,
+ org,
+ org_email,
+ role,
+ website,
+ }) {
+ cy.get('[data-cy=profile-form] input[name="firstName"]').invoke('val').should('equal', firstName);
+ cy.get('[data-cy=profile-form] input[name="lastName"]').invoke('val').should('equal', lastName);
+ cy.get('[data-cy=profile-form] [data-cy=email] [data-cy=value]').contains(email);
+ cy.get('[data-cy=profile-form] [data-cy=username] [data-cy=value]').contains(username);
+
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').invoke('val').should('equal', org);
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').invoke('val').should('equal', org_email);
+ cy.get('[data-cy=profile-form] select[name="prefs.profile.role"]').invoke('val').should('equal', role);
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').invoke('val').should('equal', website);
+ }
+
+ function enterProfileValues({
+ org,
+ org_email,
+ role,
+ website,
+ }) {
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').clear();
+ if (org) {
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').type(org);
+ }
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').clear();
+ if (org_email) {
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').type(org_email);
+ }
+ cy.get('[data-cy=profile-form] select[name="prefs.profile.role"]').select(role);
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').clear();
+ if (website) {
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').type(website);
+ }
+ }
+
+ function assertContextMenuItems({
+ account,
+ activate,
+ deactivate,
+ login,
+ setup
+ }) {
+ cy.get('[data-cy=user-profile-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').within(() => {
+ cy.get('[role=button]').contains('Advanced');
+
+ cy.get('[role=button]').should(account ? 'contain' : 'not.contain', 'Account Settings');
+ cy.get('[role=button]').should(activate ? 'contain' : 'not.contain', 'Activate User');
+ cy.get('[role=button]').should(deactivate ? 'contain' : 'not.contain', 'Deactivate User');
+ cy.get('[role=button]').should(login ? 'contain' : 'not.contain', 'Login As User');
+ cy.get('[role=button]').should(setup ? 'contain' : 'not.contain', 'Setup User');
+ });
+ cy.get('div[role=presentation]').click();
+ }
+
+ beforeEach(function() {
+ cy.updateResource(adminUser.token, 'users', adminUser.user.uuid, {
+ prefs: {
+ profile: {
+ organization: '',
+ organization_email: '',
+ role: '',
+ website_url: '',
+ },
+ },
+ });
+ cy.updateResource(adminUser.token, 'users', activeUser.user.uuid, {
+ prefs: {
+ profile: {
+ organization: '',
+ organization_email: '',
+ role: '',
+ website_url: '',
+ },
+ },
+ });
+ });
+
+ it('non-admin can edit own profile', function() {
+ cy.loginAs(activeUser);
+
+ cy.get('header button[title="Account Management"]').click();
+ cy.get('#account-menu').contains('My account').click();
+
+ // Admin actions should be hidden, no account menu
+ assertContextMenuItems({
+ account: false,
+ activate: false,
+ deactivate: false,
+ login: false,
+ setup: false,
+ });
+
+ // Check initial values
+ assertProfileValues({
+ firstName: 'Active',
+ lastName: 'User',
+ email: 'user@example.local',
+ username: 'user',
+ org: '',
+ org_email: '',
+ role: '',
+ website: '',
+ });
+
+ // Change values
+ enterProfileValues({
+ org: 'Org name',
+ org_email: 'email@example.com',
+ role: 'Data Scientist',
+ website: 'example.com',
+ });
+
+ // Submit
+ cy.get('[data-cy=profile-form] button[type="submit"]').click();
+
+ // Check new values
+ assertProfileValues({
+ firstName: 'Active',
+ lastName: 'User',
+ email: 'user@example.local',
+ username: 'user',
+ org: 'Org name',
+ org_email: 'email@example.com',
+ role: 'Data Scientist',
+ website: 'example.com',
+ });
+ });
+
+ it('non-admin cannot edit other profile', function() {
+ cy.loginAs(activeUser);
+ cy.goToPath('/user/' + adminUser.user.uuid);
+
+ assertProfileValues({
+ firstName: 'Admin',
+ lastName: 'User',
+ email: 'admin@example.local',
+ username: 'admin',
+ org: '',
+ org_email: '',
+ role: '',
+ website: '',
+ });
+
+ // Inputs should be disabled
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization"]').should('be.disabled');
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.organization_email"]').should('be.disabled');
+ cy.get('[data-cy=profile-form] select[name="prefs.profile.role"]').should('be.disabled');
+ cy.get('[data-cy=profile-form] input[name="prefs.profile.website_url"]').should('be.disabled');
+
+ // Submit should be disabled
+ cy.get('[data-cy=profile-form] button[type="submit"]').should('be.disabled');
+
+ // Admin actions should be hidden, no account menu
+ assertContextMenuItems({
+ account: false,
+ activate: false,
+ deactivate: false,
+ login: false,
+ setup: false,
+ });
+ });
+
+ it('admin can edit own profile', function() {
+ cy.loginAs(adminUser);
+
+ cy.get('header button[title="Account Management"]').click();
+ cy.get('#account-menu').contains('My account').click();
+
+ // Admin actions should be visible, no account menu
+ assertContextMenuItems({
+ account: false,
+ activate: false,
+ deactivate: true,
+ login: false,
+ setup: false,
+ });
+
+ // Check initial values
+ assertProfileValues({
+ firstName: 'Admin',
+ lastName: 'User',
+ email: 'admin@example.local',
+ username: 'admin',
+ org: '',
+ org_email: '',
+ role: '',
+ website: '',
+ });
+
+ // Change values
+ enterProfileValues({
+ org: 'Admin org name',
+ org_email: 'admin@example.com',
+ role: 'Researcher',
+ website: 'admin.local',
+ });
+ cy.get('[data-cy=profile-form] button[type="submit"]').click();
+
+ // Check new values
+ assertProfileValues({
+ firstName: 'Admin',
+ lastName: 'User',
+ email: 'admin@example.local',
+ username: 'admin',
+ org: 'Admin org name',
+ org_email: 'admin@example.com',
+ role: 'Researcher',
+ website: 'admin.local',
+ });
+ });
+
+ it('admin can edit other profile', function() {
+ cy.loginAs(adminUser);
+ cy.goToPath('/user/' + activeUser.user.uuid);
+
+ // Check new values
+ assertProfileValues({
+ firstName: 'Active',
+ lastName: 'User',
+ email: 'user@example.local',
+ username: 'user',
+ org: '',
+ org_email: '',
+ role: '',
+ website: '',
+ });
+
+ enterProfileValues({
+ org: 'Changed org name',
+ org_email: 'changed@example.com',
+ role: 'Researcher',
+ website: 'changed.local',
+ });
+ cy.get('[data-cy=profile-form] button[type="submit"]').click();
+
+ // Check new values
+ assertProfileValues({
+ firstName: 'Active',
+ lastName: 'User',
+ email: 'user@example.local',
+ username: 'user',
+ org: 'Changed org name',
+ org_email: 'changed@example.com',
+ role: 'Researcher',
+ website: 'changed.local',
+ });
+
+ // Admin actions should be visible, no account menu
+ assertContextMenuItems({
+ account: false,
+ activate: false,
+ deactivate: true,
+ login: true,
+ setup: false,
+ });
+ });
+
+ it('displays role groups on user profile', function() {
+ cy.loginAs(adminUser);
+
+ cy.createGroup(adminUser.token, {
+ name: roleGroupName,
+ group_class: 'role',
+ }).as('roleGroup').then(function() {
+ cy.createLink(adminUser.token, {
+ name: 'can_write',
+ link_class: 'permission',
+ head_uuid: this.roleGroup.uuid,
+ tail_uuid: adminUser.user.uuid
+ });
+ cy.createLink(adminUser.token, {
+ name: 'can_write',
+ link_class: 'permission',
+ head_uuid: this.roleGroup.uuid,
+ tail_uuid: activeUser.user.uuid
+ });
+ });
+
+ cy.createGroup(adminUser.token, {
+ name: projectGroupName,
+ group_class: 'project',
+ }).as('projectGroup').then(function() {
+ cy.createLink(adminUser.token, {
+ name: 'can_write',
+ link_class: 'permission',
+ head_uuid: this.projectGroup.uuid,
+ tail_uuid: adminUser.user.uuid
+ });
+ cy.createLink(adminUser.token, {
+ name: 'can_write',
+ link_class: 'permission',
+ head_uuid: this.projectGroup.uuid,
+ tail_uuid: activeUser.user.uuid
+ });
+ });
+
+ cy.goToPath('/user/' + activeUser.user.uuid);
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').contains(roleGroupName);
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', projectGroupName);
+
+ cy.goToPath('/user/' + adminUser.user.uuid);
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').contains(roleGroupName);
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', projectGroupName);
+ });
+
+ it('allows performing admin functions', function() {
+ cy.loginAs(adminUser);
+ cy.goToPath('/user/' + activeUser.user.uuid);
+
+ // Check that user is active
+ cy.get('[data-cy=account-status]').contains('Active');
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+ cy.get('div [role="tab"]').contains('PROFILE').click();
+ assertContextMenuItems({
+ account: false,
+ activate: false,
+ deactivate: true,
+ login: true,
+ setup: false,
+ });
+
+ // Deactivate user
+ cy.get('[data-cy=user-profile-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Deactivate User').click();
+ cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+ // Check that user is deactivated
+ cy.get('[data-cy=account-status]').contains('Inactive');
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', 'All users');
+ cy.get('div [role="tab"]').contains('PROFILE').click();
+ assertContextMenuItems({
+ account: false,
+ activate: true,
+ deactivate: false,
+ login: true,
+ setup: true,
+ });
+
+ // Setup user
+ cy.get('[data-cy=user-profile-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Setup User').click();
+ cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+ // Check that user is setup
+ cy.get('[data-cy=account-status]').contains('Setup');
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+ cy.get('div [role="tab"]').contains('PROFILE').click();
+ assertContextMenuItems({
+ account: false,
+ activate: true,
+ deactivate: true,
+ login: true,
+ setup: false,
+ });
+
+ // Activate user
+ cy.get('[data-cy=user-profile-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Activate User').click();
+ cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+ // Check that user is active
+ cy.get('[data-cy=account-status]').contains('Active');
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+ cy.get('div [role="tab"]').contains('PROFILE').click();
+ assertContextMenuItems({
+ account: false,
+ activate: false,
+ deactivate: true,
+ login: true,
+ setup: false,
+ });
+
+ // Deactivate and activate user skipping setup
+ cy.get('[data-cy=user-profile-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Deactivate User').click();
+ cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+ // Check
+ cy.get('[data-cy=account-status]').contains('Inactive');
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('not.contain', 'All users');
+ cy.get('div [role="tab"]').contains('PROFILE').click();
+ assertContextMenuItems({
+ account: false,
+ activate: true,
+ deactivate: false,
+ login: true,
+ setup: true,
+ });
+ // reactivate
+ cy.get('[data-cy=user-profile-panel-options-btn]').click();
+ cy.get('[data-cy=context-menu]').contains('Activate User').click();
+ cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+
+ // Check that user is active
+ cy.get('[data-cy=account-status]').contains('Active');
+ cy.get('div [role="tab"]').contains('GROUPS').click();
+ cy.get('[data-cy=user-profile-groups-data-explorer]').should('contain', 'All users');
+ cy.get('div [role="tab"]').contains('PROFILE').click();
+ assertContextMenuItems({
+ account: false,
+ activate: false,
+ deactivate: true,
+ login: true,
+ setup: false,
+ });
+ });
+
+});
cy.get('[data-cy=form-dialog]').within(() => {
cy.get('[data-cy=form-submit-btn]').click();
});
+ cy.get('[data-cy=snackbar]').contains('Permission updated');
cy.get('[data-cy=vm-admin-table]')
.contains(vmHost)
.parents('tr')
cy.get('[data-cy=form-dialog]').within(() => {
cy.get('[data-cy=form-submit-btn]').click();
});
+ cy.get('[data-cy=snackbar]').contains('Permission updated');
cy.get('[data-cy=vm-admin-table]')
.contains(vmHost)
.parents('tr')
cy.get('[data-cy=form-submit-btn]').click();
});
+ // Wait for page to finish loading
+ cy.get('[data-cy=snackbar]').contains('Permission updated');
cy.get('[data-cy=vm-admin-table]')
- .contains('user'); // Wait for page to finish
+ .contains(vmHost)
+ .parents('tr')
+ .within(() => {
+ cy.get('div[role=button]')
+ .parent()
+ .first()
+ .contains('admin')
+ });
cy.get('[data-cy=vm-admin-table]')
.contains(vmHost)
cy.get('[data-cy=form-dialog]').within(() => {
cy.get('[data-cy=form-submit-btn]').click();
});
+ cy.get('[data-cy=snackbar]').contains('Permission updated');
// Verify new login permissions
// Check admin's vm page for login
import { Popover, List, ListItem, ListItemIcon, ListItemText, Divider } from "@material-ui/core";
import { DefaultTransformOrigin } from "../popover/helpers";
import { IconType } from "../icon/icon";
+import { RootState } from "store/store";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
export interface ContextMenuItem {
name?: string | React.ComponentType;
icon?: IconType;
component?: React.ComponentType<any>;
+ adminOnly?: boolean;
+ filters?: ((state: RootState, resource: ContextMenuResource) => boolean)[]
}
export type ContextMenuItemGroup = ContextMenuItem[];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { connect, DispatchProp } from "react-redux";
+import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from "@material-ui/core";
+import { ArvadosTheme } from 'common/custom-theme';
+import CopyToClipboard from 'react-copy-to-clipboard';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+import { CopyIcon } from 'components/icon/icon';
+
+type CssRules = 'copyIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ copyIcon: {
+ marginLeft: theme.spacing.unit,
+ color: theme.palette.grey["500"],
+ cursor: 'pointer',
+ display: 'inline',
+ '& svg': {
+ fontSize: '1rem',
+ verticalAlign: 'middle',
+ }
+ }
+});
+
+interface CopyToClipboardDataProps {
+ children?: React.ReactNode;
+ value: string;
+}
+
+type CopyToClipboardProps = CopyToClipboardDataProps & WithStyles<CssRules> & DispatchProp;
+
+export const CopyToClipboardSnackbar = connect()(withStyles(styles)(
+ class CopyToClipboardSnackbar extends React.Component<CopyToClipboardProps> {
+ onCopy = () => {
+ this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'Copied',
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS
+ }));
+ };
+
+ render() {
+ const { children, value, classes } = this.props;
+ return (
+ <Tooltip title="Copy to clipboard">
+ <span className={classes.copyIcon}>
+ <CopyToClipboard text={value} onCopy={this.onCopy}>
+ {children || <CopyIcon />}
+ </CopyToClipboard>
+ </span>
+ </Tooltip>
+ );
+ }
+ }
+));
import WrapText from '@material-ui/icons/WrapText';
import TextIncrease from '@material-ui/icons/ZoomIn';
import TextDecrease from '@material-ui/icons/ZoomOut';
+import CropFreeSharp from '@material-ui/icons/CropFreeSharp';
+import ExitToApp from '@material-ui/icons/ExitToApp';
+import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
+import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline';
+import NotInterested from '@material-ui/icons/NotInterested';
// Import FontAwesome icons
import { library } from '@fortawesome/fontawesome-svg-core';
import { faPencilAlt, faSlash, faUsers, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
-import { CropFreeSharp } from '@material-ui/icons';
library.add(
faPencilAlt,
faSlash,
export const WordWrapIcon: IconType = (props) => <WrapText {...props} />;
export const TextIncreaseIcon: IconType = (props) => <TextIncrease {...props} />;
export const TextDecreaseIcon: IconType = (props) => <TextDecrease {...props} />;
+export const DeactivateUserIcon: IconType = (props) => <NotInterested {...props} />;
+export const LoginAsIcon: IconType = (props) => <ExitToApp {...props} />;
+export const ActiveIcon: IconType = (props) => <CheckCircleOutline {...props} />;
+export const SetupIcon: IconType = (props) => <RemoveCircleOutline {...props} />;
+export const InactiveIcon: IconType = (props) => <NotInterested {...props} />;
}
});
+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}`,
</Select>
<FormHelperText>{props.meta.error}</FormHelperText>
</FormControl>
-);
\ No newline at end of file
+);
ROLE = 'role',
}
-export const BUILTIN_GROUP_IDS = [
- 'fffffffffffffff',
- 'anonymouspublic',
- '000000000000000',
-]
+export enum BuiltinGroups {
+ ALL = 'fffffffffffffff',
+ ANON = 'anonymouspublic',
+ SYSTEM = '000000000000000',
+}
+
+export const getBuiltinGroupUuid = (cluster: string, groupName: BuiltinGroups): string => {
+ return cluster ? `${cluster}-${ResourceObjectType.GROUP}-${groupName}` : "";
+};
export const isBuiltinGroup = (uuid: string) => {
const match = RESOURCE_UUID_REGEX.exec(uuid);
const parts = match ? match[0].split('-') : [];
- return parts.length === 3 && parts[1] === ResourceObjectType.GROUP && BUILTIN_GROUP_IDS.includes(parts[2]);
+ return parts.length === 3 && parts[1] === ResourceObjectType.GROUP && Object.values<string>(BuiltinGroups).includes(parts[2]);
};
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);
} 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) {
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})`,
export const getGroupUrl = (uuid: string) => `/group/${uuid}`;
+export const getUserProfileUrl = (uuid: string) => `/user/${uuid}`;
+
export interface ResourceRouteParams {
id: 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 });
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) {
- super(serverApi, "users", actions);
+ constructor(serverApi: AxiosInstance, actions: ApiActions, readOnlyFields: string[] = []) {
+ super(serverApi, "users", actions, readOnlyFields.concat([
+ 'fullName',
+ 'isInvited',
+ 'writableBy',
+ ]));
}
activate(uuid: string) {
- return CommonResourceService.defaultResponse(
+ return CommonResourceService.defaultResponse<UserResource>(
this.serverApi
.post(this.resourceType + `/${uuid}/activate`),
this.actions
);
}
+ 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
return state.remoteHostsConfig[state.localCluster];
};
+export const getLocalCluster = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): string => {
+ return getState().auth.localCluster;
+};
+
export const saveApiToken = (token: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
let config: any;
const tokenParts = token.split('/');
import { ResourceKind } from 'models/resource';
import { GroupResource } from 'models/group';
import { extractUuidKind } from 'models/resource';
+import { UserResource } from 'models/user';
export const BREADCRUMBS = 'breadcrumbs';
}
};
-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) => {
+ try {
+ const user = getResource<UserResource>(userUuid)(getState().resources)
+ || await services.userService.get(userUuid, false);
+ const breadcrumbs: ResourceBreadcrumb[] = [
+ { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
+ { label: user ? user.username : userUuid, uuid: userUuid },
+ ];
+ dispatch(setBreadcrumbs(breadcrumbs));
+ } catch (e) {
+ const breadcrumbs: ResourceBreadcrumb[] = [
+ { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
+ { label: userUuid, 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 },
+ ]));
+ };
}
};
+export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) =>
+ (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch<any>(openContextMenu(event, {
+ name: '',
+ uuid: user.uuid,
+ ownerUuid: user.ownerUuid,
+ kind: user.kind,
+ menuKind: ContextMenuKind.USER
+ }));
+ };
+
export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
(dispatch: Dispatch, getState: () => RootState) => {
const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { RootState } from "store/store";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+import { getUserAccountStatus, UserAccountStatus } from "store/users/users-actions";
+import { matchMyAccountRoute, matchUserProfileRoute } from "routes/routes";
+
+export const isAdmin = (state: RootState, resource: ContextMenuResource) => {
+ return state.auth.user!.isAdmin;
+}
+
+export const canActivateUser = (state: RootState, resource: ContextMenuResource) => {
+ const status = getUserAccountStatus(state, resource.uuid);
+ return status === UserAccountStatus.INACTIVE ||
+ status === UserAccountStatus.SETUP;
+};
+
+export const canDeactivateUser = (state: RootState, resource: ContextMenuResource) => {
+ const status = getUserAccountStatus(state, resource.uuid);
+ return status === UserAccountStatus.SETUP ||
+ status === UserAccountStatus.ACTIVE;
+};
+
+export const canSetupUser = (state: RootState, resource: ContextMenuResource) => {
+ const status = getUserAccountStatus(state, resource.uuid);
+ return status === UserAccountStatus.INACTIVE;
+};
+
+export const needsUserProfileLink = (state: RootState, resource: ContextMenuResource) => (
+ state.router.location ?
+ !(matchUserProfileRoute(state.router.location.pathname)
+ || matchMyAccountRoute(state.router.location.pathname)
+ ) : true
+);
+
+export const isOtherUser = (state: RootState, resource: ContextMenuResource) => {
+ return state.auth.user!.uuid !== resource.uuid;
+};
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';
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({
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) =>
} 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(GroupPermissionsPanelActions.REQUEST_ITEMS());
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',
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);
}
}
-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.',
const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
const groupUuid = getCurrentGroupDetailsPanelUuid(api.getState().properties);
if (!dataExplorer || !groupUuid) {
- api.dispatch(groupsDetailsPanelDataExplorerIsNotSet());
+ // No-op if data explorer is not set since refresh may be triggered from elsewhere
} else {
try {
const permissionsOut = await this.services.permissionService.list({
}
}
-const groupsDetailsPanelDataExplorerIsNotSet = () =>
- snackbarActions.OPEN_SNACKBAR({
- message: 'Group permissions panel is not ready.',
- kind: SnackbarKind.ERROR
- });
-
const couldNotFetchGroupDetailsContents = () =>
snackbarActions.OPEN_SNACKBAR({
message: 'Could not fetch group permissions.',
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { Dispatch } from "redux";
-import { RootState } from "store/store";
-import { initialize } from "redux-form";
-import { ServiceRepository } from "services/services";
-import { setBreadcrumbs } from "store/breadcrumbs/breadcrumbs-actions";
-import { authActions } from "store/auth/auth-action";
-import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-
-export const MY_ACCOUNT_FORM = 'myAccountForm';
-
-export const loadMyAccountPanel = () =>
- (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- dispatch(setBreadcrumbs([{ label: 'User profile' }]));
- };
-
-export const saveEditedUser = (resource: any) =>
- async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- try {
- await services.userService.update(resource.uuid, resource);
- dispatch(authActions.USER_DETAILS_SUCCESS(resource));
- dispatch(initialize(MY_ACCOUNT_FORM, resource));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Profile has been updated.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
- } catch (e) {
- return;
- }
- };
import { push } from "react-router-redux";
import { ResourceKind, extractUuidKind } from 'models/resource';
import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getGroupUrl, getNavUrl } from 'routes/routes';
+import { Routes, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
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({
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));
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);
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';
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)
);
sharedWithMePanelMiddleware,
workflowPanelMiddleware,
userPanelMiddleware,
+ userProfileGroupsMiddleware,
groupsPanelMiddleware,
groupDetailsPanelMembersMiddleware,
groupDetailsPanelPermissionsMiddleware,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+import { RootState } from "store/store";
+import { Dispatch } from 'redux';
+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';
+import { getProperty } from 'store/properties/properties';
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { deleteResources, updateResources } from "store/resources/resources-actions";
+import { dialogActions } from "store/dialog/dialog-actions";
+import { filterResources } from "store/resources/resources";
+import { ResourceKind } from "models/resource";
+import { LinkClass, LinkResource } from "models/link";
+import { BuiltinGroups, getBuiltinGroupUuid } from "models/group";
+
+export const USER_PROFILE_PANEL_ID = 'userProfilePanel';
+export const USER_PROFILE_FORM = 'userProfileForm';
+export const DEACTIVATE_DIALOG = 'deactivateDialog';
+export const SETUP_DIALOG = 'setupDialog';
+export const ACTIVATE_DIALOG = 'activateDialog';
+export const IS_PROFILE_INACCESSIBLE = 'isProfileInaccessible';
+
+export const UserProfileGroupsActions = bindDataExplorerActions(USER_PROFILE_PANEL_ID);
+
+export const getCurrentUserProfilePanelUuid = getProperty<string>(USER_PROFILE_PANEL_ID);
+export const getUserProfileIsInaccessible = getProperty<boolean>(IS_PROFILE_INACCESSIBLE);
+
+export const loadUserProfilePanel = (userUuid?: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ // Reset isInacessible to ensure error screen is hidden
+ dispatch(propertiesActions.SET_PROPERTY({ key: IS_PROFILE_INACCESSIBLE, value: false }));
+ // 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 }));
+ try {
+ const user = await services.userService.get(uuid, false);
+ dispatch(initialize(USER_PROFILE_FORM, user));
+ dispatch(updateResources([user]));
+ dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+ } catch (e) {
+ if (e.status === 404) {
+ await dispatch(propertiesActions.SET_PROPERTY({ key: IS_PROFILE_INACCESSIBLE, value: true }));
+ dispatch(reset(USER_PROFILE_FORM));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not load user profile',
+ kind: SnackbarKind.ERROR
+ }));
+ }
+ }
+ }
+ }
+
+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(initialize(USER_PROFILE_FORM, user));
+ 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 openSetupDialog = (uuid: string) =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(dialogActions.OPEN_DIALOG({
+ id: SETUP_DIALOG,
+ data: {
+ title: 'Setup user',
+ text: 'Are you sure you want to setup this user?',
+ confirmButtonLabel: 'Confirm',
+ uuid
+ }
+ }));
+ };
+
+export const openActivateDialog = (uuid: string) =>
+ (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(dialogActions.OPEN_DIALOG({
+ id: ACTIVATE_DIALOG,
+ data: {
+ title: 'Activate user',
+ text: 'Are you sure you want to activate this user?',
+ confirmButtonLabel: 'Confirm',
+ uuid
+ }
+ }));
+ };
+
+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: 'Confirm',
+ uuid
+ }
+ }));
+ };
+
+export const setup = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const resources = await services.userService.setup(uuid);
+ dispatch(updateResources(resources.items));
+
+ // Refresh data explorer
+ dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been setup", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ } finally {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: SETUP_DIALOG }));
+ }
+ };
+
+export const activate = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const user = await services.userService.activate(uuid);
+ dispatch(updateResources([user]));
+
+ // Refresh data explorer
+ dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "User has been activated", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+ } catch (e) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: e.message, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ }
+ };
+
+export const deactivate = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const { resources, auth } = getState();
+ // Call unsetup
+ const user = await services.userService.unsetup(uuid);
+ dispatch(updateResources([user]));
+
+ // Find and remove all users membership
+ const allUsersGroupUuid = getBuiltinGroupUuid(auth.localCluster, BuiltinGroups.ALL);
+ const memberships = filterResources((resource: LinkResource) =>
+ resource.kind === ResourceKind.LINK &&
+ resource.linkClass === LinkClass.PERMISSION &&
+ resource.headUuid === allUsersGroupUuid &&
+ resource.tailUuid === uuid
+ )(resources);
+ // Remove all users membership locally
+ dispatch<any>(deleteResources(memberships.map(link => link.uuid)));
+
+ // Refresh data explorer
+ dispatch(UserProfileGroupsActions.REQUEST_ITEMS());
+
+ 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,
+ }));
+ }
+ };
--- /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, 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';
+import { GroupClass } from 'models/group';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
+
+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()
+ });
+ // Update resources, includes "project" groups
+ api.dispatch(updateResources(groupMembershipLinks.items));
+
+ // Get user's groups details and filter to role groups
+ const groups = await this.services.groupsService.list({
+ filters: new FilterBuilder()
+ .addIn('uuid', groupMembershipLinks.items
+ .map(item => item.headUuid))
+ .addEqual('group_class', GroupClass.ROLE)
+ .getFilters(),
+ count: "none"
+ });
+ api.dispatch(updateResources(groups.items));
+
+ // Get permission links for only role groups
+ const roleGroupMembershipLinks = await this.services.permissionService.list({
+ filters: new FilterBuilder()
+ .addIn('head_uuid', groups.items.map(item => item.uuid))
+ .addEqual('tail_uuid', userUuid)
+ .addEqual('link_class', LinkClass.PERMISSION)
+ .addEqual('head_kind', ResourceKind.GROUP)
+ .getFilters()
+ });
+
+ api.dispatch(UserProfileGroupsActions.SET_ITEMS({
+ ...listResultsToDataExplorerItemsMeta(roleGroupMembershipLinks),
+ items: roleGroupMembershipLinks.items.map(item => item.uuid),
+ }));
+ } catch {
+ api.dispatch(couldNotFetchGroups());
+ } finally {
+ api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
+ }
+ }
+}
+
+const couldNotFetchGroups = () =>
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Could not fetch groups.',
+ kind: SnackbarKind.ERROR
+ });
import { getSortColumn } from "store/data-explorer/data-explorer-reducer";
import { UserResource } from 'models/user';
import { UserPanelColumnNames } from 'views/user-panel/user-panel';
+import { BuiltinGroups, getBuiltinGroupUuid } from 'models/group';
+import { LinkClass } from 'models/link';
export class UserMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
const state = api.getState();
const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
try {
- const responseFirstName = await this.services.userService.list(getParamsFirstName(dataExplorer));
- if (responseFirstName.itemsAvailable) {
- api.dispatch(updateResources(responseFirstName.items));
- api.dispatch(setItems(responseFirstName));
- } else {
- const responseLastName = await this.services.userService.list(getParamsLastName(dataExplorer));
- api.dispatch(updateResources(responseLastName.items));
- api.dispatch(setItems(responseLastName));
- }
+ const users = await this.services.userService.list(getParams(dataExplorer));
+ api.dispatch(updateResources(users.items));
+ api.dispatch(setItems(users));
+
+ // Get "all users" group memberships
+ const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+ const allUserMemberships = await this.services.permissionService.list({
+ filters: new FilterBuilder()
+ .addEqual('head_uuid', allUsersGroupUuid)
+ .addEqual('link_class', LinkClass.PERMISSION)
+ .getFilters()
+ });
+ api.dispatch(updateResources(allUserMemberships.items));
} catch {
api.dispatch(couldNotFetchUsers());
}
}
}
-const getParamsFirstName = (dataExplorer: DataExplorer) => ({
- ...dataExplorerToListParams(dataExplorer),
- order: getOrder(dataExplorer),
- filters: getFiltersFirstName(dataExplorer)
-});
-
-const getParamsLastName = (dataExplorer: DataExplorer) => ({
+const getParams = (dataExplorer: DataExplorer) => ({
...dataExplorerToListParams(dataExplorer),
order: getOrder(dataExplorer),
- filters: getFiltersLastName(dataExplorer)
+ filters: new FilterBuilder()
+ .addFullTextSearch(dataExplorer.searchValue)
+ .getFilters()
});
-const getFiltersFirstName = (dataExplorer: DataExplorer) => {
- const filters = new FilterBuilder()
- .addILike("first_name", dataExplorer.searchValue)
- .getFilters();
- return filters;
-};
-
-const getFiltersLastName = (dataExplorer: DataExplorer) => {
- const filters = new FilterBuilder()
- .addILike("last_name", dataExplorer.searchValue)
- .getFilters();
- return filters;
-};
-
export const getOrder = (dataExplorer: DataExplorer) => {
const sortColumn = getSortColumn(dataExplorer);
const order = new OrderBuilder<UserResource>();
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();
+ switch (sortColumn.name) {
+ case UserPanelColumnNames.NAME:
+ order.addOrder(sortDirection, "firstName")
+ .addOrder(sortDirection, "lastName");
+ break;
+ case UserPanelColumnNames.UUID:
+ order.addOrder(sortDirection, "uuid");
+ break;
+ case UserPanelColumnNames.EMAIL:
+ order.addOrder(sortDirection, "email");
+ break;
+ case UserPanelColumnNames.USERNAME:
+ order.addOrder(sortDirection, "username");
+ break;
+ }
}
+ return order.getOrder();
};
export const setItems = (listResults: ListResults<UserResource>) =>
snackbarActions.OPEN_SNACKBAR({
message: 'Could not fetch users.',
kind: SnackbarKind.ERROR
- });
\ No newline at end of file
+ });
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, stopSubmit } from "redux-form";
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
import { UserResource } from "models/user";
-import { getResource } from 'store/resources/resources';
+import { filterResources, 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 { VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD } from "store/virtual-machines/virtual-machines-actions";
+import { PermissionLevel } from "models/permission";
+import { updateResources } from "store/resources/resources-actions";
+import { BuiltinGroups, getBuiltinGroupUuid } from "models/group";
+import { LinkClass, LinkResource } from "models/link";
+import { ResourceKind } from "models/resource";
export const USERS_PANEL_ID = 'usersPanel';
export const USER_ATTRIBUTES_DIALOG = 'userAttributesDialog';
export const USER_CREATE_FORM_NAME = 'userCreateFormName';
-export const USER_MANAGEMENT_DIALOG = 'userManageDialog';
-export const SETUP_SHELL_ACCOUNT_DIALOG = 'setupShellAccountDialog';
export interface UserCreateFormDialogData {
email: string;
- virtualMachineName: string;
- groupVirtualMachine: string;
+ [VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: string;
+ [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: 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) => {
dispatch(dialogActions.OPEN_DIALOG({ id: USER_ATTRIBUTES_DIALOG, data }));
};
-export const openUserManagement = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { resources } = getState();
- const data = getResource<UserResource>(uuid)(resources);
- dispatch(dialogActions.OPEN_DIALOG({ id: USER_MANAGEMENT_DIALOG, data }));
- };
-
-export const openSetupShellAccount = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- 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 } }));
- };
-
export const loginAs = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const { resources } = getState();
- const data = getResource<UserResource>(uuid)(resources);
- const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid });
- if (data) {
- dispatch<any>(authActions.INIT_USER({ user: data, token: getTokenV2(client) }));
- window.location.reload();
- dispatch<any>(navigateToRootProject);
+ const userUuid = getUserUuid(getState());
+ if (userUuid === uuid) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'You are already logged in as this user',
+ kind: SnackbarKind.WARNING
+ }));
+ } else {
+ try {
+ const { resources } = getState();
+ const data = getResource<UserResource>(uuid)(resources);
+ const client = await services.apiClientAuthorizationService.create({ ownerUuid: uuid }, false);
+ if (data) {
+ dispatch<any>(authActions.INIT_USER({ user: data, token: getTokenV2(client) }));
+ window.location.reload();
+ dispatch<any>(navigateToRootProject);
+ }
+ } catch (e) {
+ if (e.status === 403) {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'You do not have permission to login as this user',
+ kind: SnackbarKind.WARNING
+ }));
+ } else {
+ dispatch(snackbarActions.OPEN_SNACKBAR({
+ message: 'Failed to login as this user',
+ kind: SnackbarKind.ERROR
+ }));
+ }
+ }
}
};
dispatch<any>(navigateTo(uuid));
};
-
-export const createUser = (user: UserCreateFormDialogData) =>
+export const createUser = (data: UserCreateFormDialogData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(USER_CREATE_FORM_NAME));
try {
- const newUser = await services.userService.create({ ...user });
+ const newUser = await services.userService.create({
+ email: data.email,
+ });
+ dispatch(updateResources([newUser]));
+
+ if (data[VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]) {
+ const permission = await services.permissionService.create({
+ headUuid: data[VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD],
+ tailUuid: newUser.uuid,
+ name: PermissionLevel.CAN_LOGIN,
+ properties: {
+ username: newUser.username,
+ groups: data.groups,
+ }
+ });
+ dispatch(updateResources([permission]));
+ }
+
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 }));
return newUser;
} catch (e) {
return;
- }
- };
-
-
-export const setupUserVM = (setupData: SetupShellAccountFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(startSubmit(USER_CREATE_FORM_NAME));
- try {
- // TODO: make correct API call
- // const setupResult = await services.userService.setup({ ...setupData });
- 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;
+ } finally {
+ dispatch(stopSubmit(USER_CREATE_FORM_NAME));
}
};
}
};
-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;
- let newActivity;
- if (isActive) {
- newActivity = await services.userService.unsetup(uuid);
- } else {
- newActivity = await services.userService.update(uuid, { isActive: true });
- }
- dispatch<any>(loadUsersPanel());
- return newActivity;
- };
-
export const toggleIsAdmin = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const { resources } = getState();
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());
};
+
+export enum UserAccountStatus {
+ ACTIVE = 'Active',
+ INACTIVE = 'Inactive',
+ SETUP = 'Setup',
+ }
+
+export const getUserAccountStatus = (state: RootState, uuid: string) => {
+ const user = getResource<UserResource>(uuid)(state.resources);
+ // Get membership links for all users group
+ const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+ const permissions = filterResources((resource: LinkResource) =>
+ resource.kind === ResourceKind.LINK &&
+ resource.linkClass === LinkClass.PERMISSION &&
+ resource.headUuid === allUsersGroupUuid &&
+ resource.tailUuid === uuid
+ )(state.resources);
+
+ return user && user.isActive
+ ? UserAccountStatus.ACTIVE
+ : permissions.length > 0
+ ? UserAccountStatus.SETUP
+ : UserAccountStatus.INACTIVE;
+}
dispatch(updateResources([permission]));
} else {
const permission = await services.permissionService.create({
- headUuid: vmUuid,
+ headUuid: vmUuid,
tailUuid: userResource.uuid,
name: PermissionLevel.CAN_LOGIN,
properties: {
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';
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';
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';
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';
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 }));
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>) => {
export const loadUsers = handleFirstTimeLoad(
async (dispatch: Dispatch<any>) => {
await dispatch(loadUsersPanel());
- dispatch(setBreadcrumbs([{ label: 'Users' }]));
+ dispatch(setUsersBreadcrumbs());
});
export const loadApiClientAuthorizations = handleFirstTimeLoad(
export const REPOSITORY_NAME_VALIDATION = [require, maxLength(255)];
export const USER_EMAIL_VALIDATION = [require, maxLength(255)];
+export const PROFILE_EMAIL_VALIDATION = [maxLength(255)];
+export const PROFILE_URL_VALIDATION = [maxLength(255)];
export const USER_LENGTH_VALIDATION = [maxLength(255)];
export const SSH_KEY_PUBLIC_VALIDATION = [require, isRsaKey, maxLength(1024)];
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, ProjectIcon, AttributesIcon } from "components/icon/icon";
+import {
+ AdvancedIcon,
+ ProjectIcon,
+ AttributesIcon,
+ DeactivateUserIcon,
+ UserPanelIcon,
+ LoginAsIcon,
+ AdminMenuIcon,
+ ActiveIcon,
+} from "components/icon/icon";
import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { openUserAttributes, openUserProjects } from "store/users/users-actions";
+import { loginAs, openUserAttributes, openUserProjects } from "store/users/users-actions";
+import { openSetupDialog, openDeactivateDialog, openActivateDialog } from "store/user-profile/user-profile-actions";
+import { navigateToUserProfile } from "store/navigation/navigation-action";
+import { canActivateUser, canDeactivateUser, canSetupUser, isAdmin, needsUserProfileLink, isOtherUser } from "store/context-menu/context-menu-filters";
export const userActionSet: ContextMenuActionSet = [[{
name: "Attributes",
execute: (dispatch, { uuid }) => {
dispatch<any>(openAdvancedTabDialog(uuid));
}
-}, /*
- // Neither of the buttons on this dialog work correctly (bugs #16114 and #16124) so hide it for now.
- {
- name: "Manage",
+}, {
+ name: "Account Settings",
icon: UserPanelIcon,
execute: (dispatch, { uuid }) => {
- dispatch<any>(openUserManagement(uuid));
- }
-} */
-]];
+ dispatch<any>(navigateToUserProfile(uuid));
+ },
+ filters: [needsUserProfileLink]
+}],[{
+ name: "Activate User",
+ icon: ActiveIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openActivateDialog(uuid));
+ },
+ filters: [
+ isAdmin,
+ canActivateUser,
+ ],
+}, {
+ name: "Setup User",
+ icon: AdminMenuIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openSetupDialog(uuid));
+ },
+ filters: [
+ isAdmin,
+ canSetupUser,
+ ],
+}, {
+ name: "Deactivate User",
+ icon: DeactivateUserIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openDeactivateDialog(uuid));
+ },
+ filters: [
+ isAdmin,
+ canDeactivateUser,
+ ],
+}, {
+ name: "Login As User",
+ icon: LoginAsIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(loginAs(uuid));
+ },
+ filters: [
+ isAdmin,
+ isOtherUser,
+ ],
+}]];
type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resource?: ContextMenuResource };
const mapStateToProps = (state: RootState): DataProps => {
const { open, position, resource } = state.contextMenu;
+
+ const filteredItems = getMenuActionSet(resource).map((group) => (group.filter((item) => {
+ if (resource && item.filters) {
+ // Execute all filters on this item, every returns true IFF all filters return true
+ return item.filters.every((filter) => filter(state, resource));
+ } else {
+ return true;
+ }
+ })));
+
return {
anchorEl: resource ? createAnchorAt(position) : undefined,
- items: getMenuActionSet(resource),
+ items: filteredItems,
open,
resource
};
};
const emptyActionSet: ContextMenuActionSet = [];
-const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => {
- return resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet;
-};
+const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => (
+ resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
+);
export enum ContextMenuKind {
API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",
import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox } from '@material-ui/core';
import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
import { Resource, ResourceKind, TrashableResource } from 'models/resource';
-import { ProjectIcon, FilterGroupIcon, CollectionIcon, ProcessIcon, DefaultIcon, ShareIcon, CollectionOldVersionIcon, WorkflowIcon, RemoveIcon, RenameIcon } from 'components/icon/icon';
+import {
+ ProjectIcon,
+ FilterGroupIcon,
+ CollectionIcon,
+ ProcessIcon,
+ DefaultIcon,
+ ShareIcon,
+ CollectionOldVersionIcon,
+ WorkflowIcon,
+ RemoveIcon,
+ RenameIcon,
+ ActiveIcon,
+ SetupIcon,
+ InactiveIcon,
+} from 'components/icon/icon';
import { formatDate, formatFileSize, formatTime } from 'common/formatters';
import { resourceLabel } from 'common/labels';
import { connect, DispatchProp } from 'react-redux';
import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
-import { toggleIsActive, toggleIsAdmin } from 'store/users/users-actions';
+import { toggleIsAdmin } from 'store/users/users-actions';
import { LinkClass, LinkResource } from 'models/link';
-import { navigateTo, navigateToGroupDetails } from 'store/navigation/navigation-action';
+import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
import { withResourceData } from 'views-components/data-explorer/with-resources';
import { CollectionResource } from 'models/collection';
import { IllegalNamingWarning } from 'components/warning/warning';
import { loadResource } from 'store/resources/resources-actions';
-import { GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
+import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
import { getUserUuid } from 'common/getuser';
import { VirtualMachinesResource } from 'models/virtual-machines';
+import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
return resource || { lastName: '' };
})(renderLastName);
-const renderFullName = (item: { firstName: string, lastName: string }) =>
- <Typography noWrap>{(item.firstName + " " + item.lastName).trim()}</Typography>;
+const renderFullName = (dispatch: Dispatch ,item: { uuid: string, firstName: string, lastName: string }, link?: boolean) => {
+ const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
+ return link ? <Typography noWrap
+ color="primary"
+ style={{ 'cursor': 'pointer' }}
+ onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
+ {displayName}
+ </Typography> :
+ <Typography noWrap>{displayName}</Typography>;
+}
-export const ResourceFullName = connect(
- (state: RootState, props: { uuid: string }) => {
+export const UserResourceFullName = connect(
+ (state: RootState, props: { uuid: string, link?: boolean }) => {
const resource = getResource<UserResource>(props.uuid)(state.resources);
- return resource || { firstName: '', lastName: '' };
- })(renderFullName);
-
+ return {item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link};
+ })((props: {item: {uuid: string, firstName: string, lastName: string}, link?: boolean} & DispatchProp<any>) => renderFullName(props.dispatch, props.item, props.link));
const renderUuid = (item: { uuid: string }) =>
- <Typography data-cy="uuid" noWrap>{item.uuid}</Typography>;
+ <Typography data-cy="uuid" noWrap>
+ {item.uuid}
+ <CopyToClipboardSnackbar value={item.uuid} />
+ </Typography>;
-export const ResourceUuid = connect(
- (state: RootState, props: { uuid: string }) => {
- const resource = getResource<UserResource>(props.uuid)(state.resources);
- return resource || { uuid: '' };
- })(renderUuid);
+export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => (
+ getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' }
+ ))(renderUuid);
const renderEmail = (item: { email: string }) =>
<Typography noWrap>{item.email}</Typography>;
return resource || { email: '' };
})(renderEmail);
-const renderIsActive = (props: { uuid: string, kind: ResourceKind, isActive: boolean, toggleIsActive: (uuid: string) => void, disabled?: boolean }) => {
- if (props.kind === ResourceKind.USER) {
- return <Checkbox
- color="primary"
- checked={props.isActive}
- disabled={!!props.disabled}
- onClick={() => props.toggleIsActive(props.uuid)} />;
+enum UserAccountStatus {
+ ACTIVE = 'Active',
+ INACTIVE = 'Inactive',
+ SETUP = 'Setup',
+ UNKNOWN = ''
+}
+
+const renderAccountStatus = (props: {status: UserAccountStatus}) =>
+ <Grid container alignItems="center" wrap="nowrap" spacing={8} data-cy="account-status">
+ <Grid item>
+ {(() => {
+ switch(props.status) {
+ case UserAccountStatus.ACTIVE:
+ return <ActiveIcon style={{color: '#4caf50', verticalAlign: "middle"}} />;
+ case UserAccountStatus.SETUP:
+ return <SetupIcon style={{color: '#2196f3', verticalAlign: "middle"}} />;
+ case UserAccountStatus.INACTIVE:
+ return <InactiveIcon style={{color: '#9e9e9e', verticalAlign: "middle"}} />;
+ default:
+ return <></>;
+ }
+ })()}
+ </Grid>
+ <Grid item>
+ <Typography noWrap>
+ {props.status}
+ </Typography>
+ </Grid>
+ </Grid>;
+
+const getUserAccountStatus = (state: RootState, props: { uuid: string }) => {
+ const user = getResource<UserResource>(props.uuid)(state.resources);
+ // Get membership links for all users group
+ const allUsersGroupUuid = getBuiltinGroupUuid(state.auth.localCluster, BuiltinGroups.ALL);
+ const permissions = filterResources((resource: LinkResource) =>
+ resource.kind === ResourceKind.LINK &&
+ resource.linkClass === LinkClass.PERMISSION &&
+ resource.headUuid === allUsersGroupUuid &&
+ resource.tailUuid === props.uuid
+ )(state.resources);
+
+ if (user) {
+ return user.isActive ? {status: UserAccountStatus.ACTIVE} : permissions.length > 0 ? {status: UserAccountStatus.SETUP} : {status: UserAccountStatus.INACTIVE};
} else {
- return <Typography />;
+ return {status: UserAccountStatus.UNKNOWN};
}
}
-export const ResourceIsActive = connect(
- (state: RootState, props: { uuid: string, disabled?: boolean }) => {
- const resource = getResource<UserResource>(props.uuid)(state.resources);
- return resource ? {...resource, disabled: !!props.disabled} : { isActive: false, kind: ResourceKind.NONE };
- }, { toggleIsActive }
-)(renderIsActive);
-
-export const ResourceLinkTailIsActive = connect(
- (state: RootState, props: { uuid: string, disabled?: boolean }) => {
+export const ResourceLinkTailAccountStatus = connect(
+ (state: RootState, props: { uuid: string }) => {
const link = getResource<LinkResource>(props.uuid)(state.resources);
- const tailResource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
+ return link && link.tailKind === ResourceKind.USER ? getUserAccountStatus(state, {uuid: link.tailUuid}) : {status: UserAccountStatus.UNKNOWN};
+ })(renderAccountStatus);
- return tailResource ? {...tailResource, disabled: !!props.disabled} : { isActive: false, kind: ResourceKind.NONE };
- }, { toggleIsActive }
-)(renderIsActive);
+export const UserResourceAccountStatus = connect(getUserAccountStatus)(renderAccountStatus);
const renderIsHidden = (props: {
memberLinkUuid: string,
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 />;
}
<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 }) => {
import { WithDialogProps } from 'store/dialog/with-dialog';
import { FormDialog } from 'components/form-dialog/form-dialog';
import { UserEmailField, UserVirtualMachineField, UserGroupsVirtualMachineField } from 'views-components/form-fields/user-form-fields';
+import { UserCreateFormDialogData } from 'store/users/users-actions';
+import { UserResource } from 'models/user';
+import { VirtualMachinesResource } from 'models/virtual-machines';
-export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<any>;
+export type DialogUserProps = WithDialogProps<{}> & InjectedFormProps<UserCreateFormDialogData>;
+
+interface DataProps {
+ user: UserResource;
+ items: VirtualMachinesResource[];
+}
export const UserRepositoryCreate = (props: DialogUserProps) =>
<FormDialog
const UserAddFields = (props: DialogUserProps) => <span>
<UserEmailField />
- <UserVirtualMachineField data={props.data}/>
+ <UserVirtualMachineField data={props.data as DataProps}/>
<UserGroupsVirtualMachineField />
</span>;
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { compose } from "redux";
-import { reduxForm, InjectedFormProps, Field } from 'redux-form';
-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 { 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 { UserResource } from 'models/user';
-
-export const SetupShellAccountDialog = compose(
- withDialog(SETUP_SHELL_ACCOUNT_DIALOG),
- reduxForm<SetupShellAccountFormDialogData>({
- form: SETUP_SHELL_ACCOUNT_DIALOG,
- onSubmit: (data, dispatch) => {
- dispatch(setupUserVM(data));
- }
- })
-)(
- (props: SetupShellAccountDialogComponentProps) =>
- <FormDialog
- dialogTitle='Setup shell account'
- formFields={SetupShellAccountFormFields}
- submitLabel='Submit'
- {...props}
- />
-);
-
-interface UserProps {
- data: {
- user: UserResource;
- };
-}
-
-interface VirtualMachinesProps {
- data: {
- items: VirtualMachinesResource[];
- };
-}
-interface DataProps {
- user: UserResource;
- items: VirtualMachinesResource[];
-}
-
-const UserEmailField = ({ data }: UserProps) =>
- <span>
- <Field
- name='email'
- component={TextField as any}
- disabled
- label={data.user.email} /></span>;
-
-const UserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
- <div style={{ marginBottom: '21px' }}>
- <InputLabel>Virtual Machine</InputLabel>
- <Field
- name='virtualMachine'
- 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)" />;
-
-const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
- [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.hostname, value: it.hostname })));
-
-type SetupShellAccountDialogComponentProps = WithDialogProps<{}> & InjectedFormProps<SetupShellAccountFormDialogData>;
-
-const SetupShellAccountFormFields = (props: SetupShellAccountDialogComponentProps) =>
- <>
- <UserEmailField data={props.data as DataProps} />
- <UserVirtualMachineField data={props.data as DataProps} />
- <UserGroupsVirtualMachineField />
- </>;
import 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 { USER_EMAIL_VALIDATION, CHOOSE_VM_VALIDATION } from "validators/validators";
import { NativeSelectField } from "components/select-field/select-field";
import { InputLabel } from "@material-ui/core";
import { VirtualMachinesResource } from "models/virtual-machines";
+import { VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD } from "store/virtual-machines/virtual-machines-actions";
+import { GroupArrayInput } from "views-components/virtual-machines-dialog/group-array-input";
+
+interface VirtualMachinesProps {
+ data: {
+ items: VirtualMachinesResource[];
+ };
+}
export const UserEmailField = () =>
<Field
autoFocus={true}
label="Email" />;
-export const UserVirtualMachineField = ({ data }: any) =>
+export const RequiredUserVirtualMachineField = ({ data }: VirtualMachinesProps) =>
+ <div style={{ marginBottom: '21px' }}>
+ <InputLabel>Virtual Machine</InputLabel>
+ <Field
+ name={VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD}
+ component={NativeSelectField as any}
+ validate={CHOOSE_VM_VALIDATION}
+ items={getVirtualMachinesList(data.items)} />
+ </div>;
+
+export 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={USER_LENGTH_VALIDATION}
items={getVirtualMachinesList(data.items)} />
</div>;
export 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[]) => {
- const mappedVirtualMachines = virtualMachines.map(it => ({ key: it.hostname, value: it.hostname }));
- return mappedVirtualMachines;
-};
+const getVirtualMachinesList = (virtualMachines: VirtualMachinesResource[]) =>
+ [{ key: "", value: "" }].concat(virtualMachines.map(it => ({ key: it.uuid, value: it.hostname })));
--- /dev/null
+// 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 { activate, ACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(activate(props.data.uuid));
+ }
+});
+
+export const ActivateDialog = compose(
+ withDialog(ACTIVATE_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
--- /dev/null
+// 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 { deactivate, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(deactivate(props.data.uuid));
+ }
+});
+
+export const DeactivateDialog = compose(
+ withDialog(DEACTIVATE_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { compose, Dispatch } from "redux";
-import { connect } from "react-redux";
-import { Dialog, DialogTitle, DialogContent, DialogActions, Button, Typography } 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 { USER_MANAGEMENT_DIALOG, openSetupShellAccount, loginAs } from "store/users/users-actions";
-import { getUserDisplayName } from "models/user";
-
-type CssRules = 'spacing';
-
-const styles = withStyles<CssRules>((theme: ArvadosTheme) => ({
- spacing: {
- paddingBottom: theme.spacing.unit * 2,
- paddingTop: theme.spacing.unit * 2,
- }
-}));
-
-interface UserManageDataProps {
- data: any;
-}
-
-interface UserManageActionProps {
- openSetupShellAccount: (uuid: string) => void;
- loginAs: (uuid: string) => void;
-}
-
-const mapDispatchToProps = (dispatch: Dispatch) => ({
- openSetupShellAccount: (uuid: string) => dispatch<any>(openSetupShellAccount(uuid)),
- loginAs: (uuid: string) => dispatch<any>(loginAs(uuid))
-});
-
-type UserManageProps = UserManageDataProps & UserManageActionProps & WithStyles<CssRules>;
-
-export const UserManageDialog = compose(
- connect(null, mapDispatchToProps),
- withDialog(USER_MANAGEMENT_DIALOG),
- styles)(
- (props: WithDialogProps<UserManageProps> & UserManageProps) =>
- <Dialog open={props.open}
- onClose={props.closeDialog}
- fullWidth
- maxWidth="md">
- {props.data &&
- <span>
- <DialogTitle>{`Manage - ${getUserDisplayName(props.data)}`}</DialogTitle>
- <DialogContent>
- <Typography variant='body1' className={props.classes.spacing}>
- 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>
- <Button variant="contained" color="primary" onClick={() => props.loginAs(props.data.uuid)}>
- {`LOG IN AS ${getUserDisplayName(props.data)}`}
- </Button>
- <Typography variant='body1' className={props.classes.spacing}>
- As an admin, you can setup a shell account for this user. The login name is automatically generated from the user's e-mail address.
- </Typography>
- <Button variant="contained" color="primary" onClick={() => props.openSetupShellAccount(props.data.uuid)}>
- {`SETUP SHELL ACCOUNT FOR ${getUserDisplayName(props.data)}`}
- </Button>
- </DialogContent></span>}
-
- <DialogActions>
- <Button
- variant='text'
- color='primary'
- onClick={props.closeDialog}>
- Close
- </Button>
- </DialogActions>
- </Dialog>
- );
--- /dev/null
+// 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 { setup, SETUP_DIALOG } from 'store/user-profile/user-profile-actions';
+
+const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
+ onConfirm: () => {
+ props.closeDialog();
+ dispatch<any>(setup(props.data.uuid));
+ }
+});
+
+export const SetupDialog = compose(
+ withDialog(SETUP_DIALOG),
+ connect(null, mapDispatchToProps)
+)(ConfirmationDialog);
import { DataExplorer } from "views-components/data-explorer/data-explorer";
import { DataColumns } from 'components/data-table/data-table';
-import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailIsActive, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
+import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailAccountStatus, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers';
import { createTree } from 'models/tree';
import { noop } from 'lodash/fp';
import { RootState } from 'store/store';
export enum GroupDetailsPanelMembersColumnNames {
FULL_NAME = "Name",
USERNAME = "Username",
- ACTIVE = "User Active",
+ STATUS = "Account Status",
VISIBLE = "Visible to other members",
PERMISSION = "Permission",
REMOVE = "Remove",
render: uuid => <ResourceLinkTailUsername uuid={uuid} />
},
{
- name: GroupDetailsPanelMembersColumnNames.ACTIVE,
+ name: GroupDetailsPanelMembersColumnNames.STATUS,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceLinkTailIsActive uuid={uuid} disabled={true} />
+ render: uuid => <ResourceLinkTailAccountStatus uuid={uuid} />
},
{
name: GroupDetailsPanelMembersColumnNames.VISIBLE,
+++ /dev/null
-// 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 >;
- }
-);
+++ /dev/null
-// 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);
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { WithStyles, withStyles, Tabs, Tab, Paper, Button, Grid } from '@material-ui/core';
+import { WithStyles, withStyles, Paper, Button, Grid } 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 { openUserContextMenu } from "store/context-menu/context-menu-actions";
import { getResource, ResourcesState } from "store/resources/resources";
import {
- ResourceFirstName,
- ResourceLastName,
+ UserResourceFullName,
ResourceUuid,
ResourceEmail,
- ResourceIsActive,
ResourceIsAdmin,
- ResourceUsername
+ ResourceUsername,
+ UserResourceAccountStatus,
} from "views-components/data-explorer/renderers";
-import { navigateTo } from "store/navigation/navigation-action";
-import { ContextMenuKind } from "views-components/context-menu/context-menu";
+import { navigateToUserProfile } from "store/navigation/navigation-action";
import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
import { createTree } from 'models/tree';
import { compose, Dispatch } from 'redux';
import { USERS_PANEL_ID, openUserCreateDialog } from 'store/users/users-actions';
import { noop } from 'lodash';
-type UserPanelRules = "button" | 'root' | 'content';
+type UserPanelRules = "button" | 'root';
const styles = withStyles<UserPanelRules>(theme => ({
button: {
root: {
width: '100%',
},
- content: {
- // reserve space for the tab bar
- height: `calc(100% - ${theme.spacing.unit * 7}px)`,
- }
}));
export enum UserPanelColumnNames {
- FIRST_NAME = "First Name",
- LAST_NAME = "Last Name",
+ NAME = "Name",
UUID = "Uuid",
EMAIL = "Email",
- ACTIVE = "Active",
+ STATUS = "Account Status",
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,
+ name: UserPanelColumnNames.NAME,
selected: true,
configurable: true,
sortDirection: SortDirection.NONE,
filters: createTree(),
- render: uuid => <ResourceLastName uuid={uuid} />
+ render: uuid => <UserResourceFullName uuid={uuid} link={true} />
},
{
name: UserPanelColumnNames.UUID,
render: uuid => <ResourceEmail uuid={uuid} />
},
{
- name: UserPanelColumnNames.ACTIVE,
+ name: UserPanelColumnNames.STATUS,
selected: true,
configurable: true,
- sortDirection: SortDirection.NONE,
filters: createTree(),
- render: uuid => <ResourceIsActive uuid={uuid} />
+ render: uuid => <UserResourceAccountStatus uuid={uuid} />
},
{
name: UserPanelColumnNames.ADMIN,
selected: true,
configurable: false,
- sortDirection: SortDirection.NONE,
filters: createTree(),
render: uuid => <ResourceIsAdmin uuid={uuid} />
},
interface UserPanelActionProps {
openUserCreateDialog: () => void;
- handleRowDoubleClick: (uuid: string) => void;
- onContextMenu: (event: React.MouseEvent<HTMLElement>, item: any) => void;
+ handleRowClick: (uuid: string) => void;
+ handleContextMenu: (event, resource: UserResource) => void;
}
const mapStateToProps = (state: RootState) => {
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))
+ handleRowClick: (uuid: string) => dispatch<any>(navigateToUserProfile(uuid)),
+ handleContextMenu: (event, resource: UserResource) => dispatch<any>(openUserContextMenu(event, resource)),
});
type UserPanelProps = UserPanelDataProps & UserPanelActionProps & DispatchProp & WithStyles<UserPanelRules>;
styles,
connect(mapStateToProps, mapDispatchToProps))(
class extends React.Component<UserPanelProps> {
- state = {
- value: 0,
- };
-
- componentDidMount() {
- this.setState({ value: 0 });
- }
-
render() {
- const { value } = this.state;
return <Paper className={this.props.classes.root}>
- <Tabs value={value} onChange={this.handleChange} fullWidth>
- <Tab label="USERS" />
- <Tab label="ACTIVITY" disabled />
- </Tabs>
- {value === 0 &&
- <div className={this.props.classes.content}>
- <DataExplorer
- id={USERS_PANEL_ID}
- onRowClick={noop}
- onRowDoubleClick={noop}
- onContextMenu={this.handleContextMenu}
- contextMenuColumn={true}
- hideColumnSelector
- actions={
- <Grid container justify='flex-end'>
- <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
- <AddIcon /> NEW USER
- </Button>
- </Grid>
- }
- paperProps={{
- elevation: 0,
- }}
- dataTableDefaultView={
- <DataTableDefaultView
- icon={ShareMeIcon}
- messages={['Your user list is empty.']} />
- } />
- </div>}
+ <DataExplorer
+ id={USERS_PANEL_ID}
+ onRowClick={noop}
+ onRowDoubleClick={noop}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={true}
+ hideColumnSelector
+ actions={
+ <Grid container justify='flex-end'>
+ <Button variant="contained" color="primary" onClick={this.props.openUserCreateDialog}>
+ <AddIcon /> NEW USER
+ </Button>
+ </Grid>
+ }
+ paperProps={{
+ elevation: 0,
+ }}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={ShareMeIcon}
+ messages={['Your user list is empty.']} />
+ } />
</Paper>;
}
- handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
- this.setState({ value });
- }
-
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ event.stopPropagation();
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
- });
+ this.props.handleContextMenu(event, resource);
}
}
}
--- /dev/null
+// 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 { DispatchProp } from 'react-redux';
+import { UserResource } from 'models/user';
+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,
+ CardContent,
+ Button,
+ Typography,
+ Grid,
+ InputLabel,
+ Tabs, Tab,
+ Paper,
+ Tooltip,
+ IconButton,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
+import { PROFILE_EMAIL_VALIDATION, PROFILE_URL_VALIDATION } from "validators/validators";
+import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
+import { noop } from 'lodash';
+import { DetailsIcon, GroupsIcon, MoreOptionsIcon } from 'components/icon/icon';
+import { DataColumns } from 'components/data-table/data-table';
+import { ResourceLinkHeadUuid, ResourceLinkHeadPermissionLevel, ResourceLinkHead, ResourceLinkDelete, ResourceLinkTailIsVisible, UserResourceAccountStatus } from 'views-components/data-explorer/renderers';
+import { createTree } from 'models/tree';
+import { getResource, ResourcesState } from 'store/resources/resources';
+import { DefaultView } from 'components/default-view/default-view';
+import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
+
+type CssRules = 'root' | 'emptyRoot' | 'gridItem' | 'label' | 'readOnlyValue' | 'title' | 'description' | 'actions' | 'content' | 'copyIcon';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ width: '100%',
+ overflow: 'auto'
+ },
+ emptyRoot: {
+ width: '100%',
+ overflow: 'auto',
+ padding: theme.spacing.unit * 4,
+ },
+ gridItem: {
+ height: 45,
+ marginBottom: 20
+ },
+ label: {
+ fontSize: '0.675rem',
+ color: theme.palette.grey['600']
+ },
+ readOnlyValue: {
+ fontSize: '0.875rem',
+ },
+ 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)`,
+ },
+ copyIcon: {
+ marginLeft: theme.spacing.unit,
+ color: theme.palette.grey["500"],
+ cursor: 'pointer',
+ display: 'inline',
+ '& svg': {
+ fontSize: '1rem'
+ }
+ }
+});
+
+export interface UserProfilePanelRootActionProps {
+ handleContextMenu: (event, resource: UserResource) => void;
+}
+
+export interface UserProfilePanelRootDataProps {
+ isAdmin: boolean;
+ isSelf: boolean;
+ isPristine: boolean;
+ isValid: boolean;
+ isInaccessible: boolean;
+ userUuid: string;
+ resources: ResourcesState;
+ localCluster: string;
+}
+
+const RoleTypes = [
+ { key: '', value: '' },
+ { 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 & DispatchProp & WithStyles<CssRules>;
+
+export enum UserProfileGroupsColumnNames {
+ NAME = "Name",
+ PERMISSION = "Permission",
+ VISIBLE = "Visible to other members",
+ UUID = "UUID",
+ REMOVE = "Remove",
+}
+
+enum TABS {
+ PROFILE = "PROFILE",
+ GROUPS = "GROUPS",
+
+}
+
+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} />
+ },
+];
+
+const ReadOnlyField = withStyles(styles)(
+ (props: ({ label: string, input: {value: string} }) & WithStyles<CssRules> ) => (
+ <Grid item xs={12} data-cy="field">
+ <Typography className={props.classes.label}>
+ {props.label}
+ </Typography>
+ <Typography className={props.classes.readOnlyValue} data-cy="value">
+ {props.input.value}
+ </Typography>
+ </Grid>
+ )
+);
+
+export const UserProfilePanelRoot = withStyles(styles)(
+ class extends React.Component<UserProfilePanelRootProps> {
+ state = {
+ value: TABS.PROFILE,
+ };
+
+ componentDidMount() {
+ this.setState({ value: TABS.PROFILE});
+ }
+
+ render() {
+ if (this.props.isInaccessible) {
+ return (
+ <Paper className={this.props.classes.emptyRoot}>
+ <CardContent>
+ <DefaultView icon={DetailsIcon} messages={['This user does not exist or your account does not have permission to view it']} />
+ </CardContent>
+ </Paper>
+ );
+ } else {
+ return <Paper className={this.props.classes.root}>
+ <Tabs value={this.state.value} onChange={this.handleChange} variant={"fullWidth"}>
+ <Tab label={TABS.PROFILE} value={TABS.PROFILE} />
+ <Tab label={TABS.GROUPS} value={TABS.GROUPS} />
+ </Tabs>
+ {this.state.value === TABS.PROFILE &&
+ <CardContent>
+ <Grid container justify="space-between">
+ <Grid item>
+ <Typography className={this.props.classes.title}>
+ {this.props.userUuid}
+ <CopyToClipboardSnackbar value={this.props.userUuid} />
+ </Typography>
+ </Grid>
+ <Grid item>
+ <Grid container alignItems="center">
+ <Grid item style={{marginRight: '10px'}}><UserResourceAccountStatus uuid={this.props.userUuid} /></Grid>
+ <Grid item>
+ <Tooltip title="Actions" disableFocusListener>
+ <IconButton
+ data-cy='user-profile-panel-options-btn'
+ aria-label="Actions"
+ onClick={(event) => this.handleContextMenu(event, this.props.userUuid)}>
+ <MoreOptionsIcon />
+ </IconButton>
+ </Tooltip>
+ </Grid>
+ </Grid>
+ </Grid>
+ </Grid>
+ <form onSubmit={this.props.handleSubmit} data-cy="profile-form">
+ <Grid container spacing={24}>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="firstName">
+ <Field
+ label="First name"
+ name="firstName"
+ component={TextField as any}
+ disabled={!this.props.isAdmin && !this.props.isSelf}
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="lastName">
+ <Field
+ label="Last name"
+ name="lastName"
+ component={TextField as any}
+ disabled={!this.props.isAdmin && !this.props.isSelf}
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="email">
+ <Field
+ label="E-mail"
+ name="email"
+ component={ReadOnlyField as any}
+ disabled
+ />
+ </Grid>
+ <Grid item className={this.props.classes.gridItem} sm={6} xs={12} data-cy="username">
+ <Field
+ label="Username"
+ name="username"
+ component={ReadOnlyField 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}
+ 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}
+ disabled={!this.props.isAdmin && !this.props.isSelf}
+ validate={PROFILE_EMAIL_VALIDATION}
+ />
+ </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}
+ validate={PROFILE_URL_VALIDATION}
+ />
+ </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>
+ }
+ {this.state.value === TABS.GROUPS &&
+ <div className={this.props.classes.content}>
+ <DataExplorer
+ id={USER_PROFILE_PANEL_ID}
+ data-cy="user-profile-groups-data-explorer"
+ onRowClick={noop}
+ onRowDoubleClick={noop}
+ onContextMenu={noop}
+ contextMenuColumn={false}
+ hideColumnSelector
+ hideSearchInput
+ paperProps={{
+ elevation: 0,
+ }}
+ dataTableDefaultView={
+ <DataTableDefaultView
+ icon={GroupsIcon}
+ messages={['Group list is empty.']} />
+ } />
+ </div>}
+ </Paper >;
+ }
+ }
+
+ handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ this.setState({ value });
+ }
+
+ handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
+ event.stopPropagation();
+ const resource = getResource<UserResource>(resourceUuid)(this.props.resources);
+ if (resource) {
+ this.props.handleContextMenu(event, resource);
+ }
+ }
+
+ }
+);
--- /dev/null
+// 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 { UserResource } from 'models/user';
+import { getUserProfileIsInaccessible, saveEditedUser } from 'store/user-profile/user-profile-actions';
+import { UserProfilePanelRoot, UserProfilePanelRootDataProps } from 'views/user-profile-panel/user-profile-panel-root';
+import { USER_PROFILE_FORM } from "store/user-profile/user-profile-actions";
+import { matchUserProfileRoute } from 'routes/routes';
+import { openUserContextMenu } from 'store/context-menu/context-menu-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 || '';
+
+ return {
+ isAdmin: state.auth.user!.isAdmin,
+ isSelf: state.auth.user!.uuid === uuid,
+ isPristine: isPristine(USER_PROFILE_FORM)(state),
+ isValid: isValid(USER_PROFILE_FORM)(state),
+ isInaccessible: getUserProfileIsInaccessible(state.properties) || false,
+ localCluster: state.auth.localCluster,
+ userUuid: uuid,
+ resources: state.resources,
+}};
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ handleContextMenu: (event, resource: UserResource) => dispatch<any>(openUserContextMenu(event, resource)),
+});
+
+export const UserProfilePanel = compose(
+ connect(mapStateToProps, mapDispatchToProps),
+ reduxForm({
+ form: USER_PROFILE_FORM,
+ onSubmit: (data, dispatch) => {
+ dispatch(saveEditedUser(data));
+ }
+ }))(UserProfilePanelRoot);
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';
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';
-import { UserManageDialog } from 'views-components/user-dialog/manage-dialog';
-import { SetupShellAccountDialog } from 'views-components/dialog-forms/setup-shell-account-dialog';
+import { DeactivateDialog } from 'views-components/user-dialog/deactivate-dialog';
+import { ActivateDialog } from 'views-components/user-dialog/activate-dialog';
+import { SetupDialog } from 'views-components/user-dialog/setup-dialog';
import { GroupsPanel } from 'views/groups-panel/groups-panel';
import { RemoveGroupDialog } from 'views-components/groups-dialog/remove-dialog';
import { GroupAttributesDialog } from 'views-components/groups-dialog/attributes-dialog';
<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} />
<RepositoryAttributesDialog />
<RepositoriesSampleGitDialog />
<RichTextEditorDialog />
- <SetupShellAccountDialog />
<SharingDialog />
<NotFoundDialog />
<Snackbar />
<UpdateProcessDialog />
<UpdateProjectDialog />
<UserAttributesDialog />
- <UserManageDialog />
+ <DeactivateDialog />
+ <ActivateDialog />
+ <SetupDialog />
<VirtualMachineAttributesDialog />
<FedLogin />
<WebDavS3InfoDialog />