import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
import Computer from '@material-ui/icons/Computer';
import CropFreeSharp from '@material-ui/icons/CropFreeSharp';
-import Cancel from '@material-ui/icons/Cancel';
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';
export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
export const CanManageIcon: IconType = (props) => <Computer {...props} />;
export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
-export const DeactivateUserIcon: IconType = (props) => <Cancel {...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} />;
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]);
};
}
activate(uuid: string) {
- return CommonResourceService.defaultResponse(
+ return CommonResourceService.defaultResponse<UserResource>(
this.serverApi
.post(this.resourceType + `/${uuid}/activate`),
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 { propertiesActions } from 'store/properties/properties-actions';
import { getProperty } from 'store/properties/properties';
import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-import { updateResources } from "store/resources/resources-actions";
+import { 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 openDeactivateDialog = (uuid: string) =>
+export const openSetupDialog = (uuid: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
dispatch(dialogActions.OPEN_DIALOG({
- id: DEACTIVATE_DIALOG,
+ id: SETUP_DIALOG,
data: {
- title: 'Deactivate user',
- text: 'Are you sure you want to deactivate this user?',
- confirmButtonLabel: 'Deactvate',
- uuid
+ title: 'Setup user',
+ text: 'Are you sure you want to setup this user?',
+ confirmButtonLabel: 'Confirm',
+ uuid
}
- }));
-}
+ }));
+ };
-export const openSetupDialog = (uuid: string) =>
+export const openActivateDialog = (uuid: string) =>
(dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
dispatch(dialogActions.OPEN_DIALOG({
- id: SETUP_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: 'Setup user',
- text: 'Are you sure you want to setup this user?',
- confirmButtonLabel: 'Confirm',
- uuid
+ 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));
- 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 }));
- }
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const resources = await services.userService.setup(uuid);
+ dispatch(updateResources(resources.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]));
+ 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 unsetup = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- try {
- const user = await services.userService.unsetup(uuid);
- dispatch(updateResources([user]));
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "User has been deactivated.",
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
- } catch (e) {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: "Could not deactivate user",
- kind: SnackbarKind.ERROR,
- }));
- }
- };
+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)));
+
+ 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,
+ }));
+ }
+ };
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) {
api.dispatch(updateResources(responseLastName.items));
api.dispatch(setItems(responseLastName));
}
+ // 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());
}
UserPanelIcon,
LoginAsIcon,
AdminMenuIcon,
+ ActiveIcon,
} from "components/icon/icon";
import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
import { loginAs, openUserAttributes, openUserProjects } from "store/users/users-actions";
-import { openSetupDialog, openDeactivateDialog } from "store/user-profile/user-profile-actions";
+import { openSetupDialog, openDeactivateDialog, openActivateDialog } from "store/user-profile/user-profile-actions";
import { navigateToUserProfile } from "store/navigation/navigation-action";
export const userActionSet: ContextMenuActionSet = [[{
}
}, {
name: "Account Settings",
- adminOnly: true,
icon: UserPanelIcon,
execute: (dispatch, { uuid }) => {
dispatch<any>(navigateToUserProfile(uuid));
}
-}, {
+},], [{
+ name: "Activate User",
+ icon: ActiveIcon,
+ execute: (dispatch, { uuid }) => {
+ dispatch<any>(openActivateDialog(uuid));
+ }
+},{
name: "Setup User",
adminOnly: true,
icon: AdminMenuIcon,
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 { 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';
}, { toggleIsActive }
)(renderIsActive);
+enum UserAccountStatus {
+ ACTIVE = 'Active',
+ INACTIVE = 'Inactive',
+ SETUP = 'Setup',
+ UNKNOWN = 'UNKNOWN'
+}
+
+const renderAccountStatus = (props: {status: UserAccountStatus}) =>
+ <Grid container alignItems="center" wrap="nowrap" spacing={8}>
+ <Grid item>
+ {(() => {
+ switch(props.status) {
+ case UserAccountStatus.ACTIVE:
+ return <ActiveIcon style={{color: '#4caf50'}} />;
+ case UserAccountStatus.SETUP:
+ return <SetupIcon style={{color: '#2196f3'}} />;
+ case UserAccountStatus.INACTIVE:
+ return <InactiveIcon style={{color: '#9e9e9e'}} />;
+ default:
+ return <InactiveIcon />;
+ }
+ })()}
+ </Grid>
+ <Grid item>
+ <Typography noWrap>
+ {props.status}
+ </Typography>
+ </Grid>
+ </Grid>;
+
+export const UserResourceAccountStatus = connect(
+ (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 {status: UserAccountStatus.UNKNOWN};
+ }
+ })(renderAccountStatus);
+
export const ResourceLinkTailIsActive = connect(
(state: RootState, props: { uuid: string, disabled?: boolean }) => {
const link = getResource<LinkResource>(props.uuid)(state.resources);
--- /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);
import { connect } from "react-redux";
import { ConfirmationDialog } from "components/confirmation-dialog/confirmation-dialog";
import { withDialog, WithDialogProps } from "store/dialog/with-dialog";
-import { unsetup, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
+import { deactivate, DEACTIVATE_DIALOG } from 'store/user-profile/user-profile-actions';
const mapDispatchToProps = (dispatch: Dispatch, props: WithDialogProps<any>) => ({
onConfirm: () => {
props.closeDialog();
- dispatch<any>(unsetup(props.data.uuid));
+ dispatch<any>(deactivate(props.data.uuid));
}
});
UserResourceFullName,
ResourceUuid,
ResourceEmail,
- ResourceIsActive,
ResourceIsAdmin,
- ResourceUsername
+ ResourceUsername,
+ UserResourceAccountStatus,
} from "views-components/data-explorer/renderers";
import { navigateToUserProfile } from "store/navigation/navigation-action";
import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
NAME = "Name",
UUID = "Uuid",
EMAIL = "Email",
- ACTIVE = "Active",
+ STATUS = "Account Status",
ADMIN = "Admin",
REDIRECT_TO_USER = "Redirect to user",
USERNAME = "Username"
render: uuid => <ResourceEmail uuid={uuid} />
},
{
- name: UserPanelColumnNames.ACTIVE,
+ name: UserPanelColumnNames.STATUS,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceIsActive uuid={uuid} />
+ render: uuid => <UserResourceAccountStatus uuid={uuid} />
},
{
name: UserPanelColumnNames.ADMIN,
import { CreateUserDialog } from 'views-components/dialog-forms/create-user-dialog';
import { HelpApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/help-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';
<UpdateProjectDialog />
<UserAttributesDialog />
<DeactivateDialog />
+ <ActivateDialog />
<SetupDialog />
<VirtualMachineAttributesDialog />
<FedLogin />