From: Stephen Smith Date: Mon, 4 Apr 2022 20:27:20 +0000 (-0400) Subject: 18559: Add context menu filter system for more complex context menus on user profile. X-Git-Tag: 2.4.0~1^2~1 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/952bcc8f3ef686a2463931bc3f88457398163df7 18559: Add context menu filter system for more complex context menus on user profile. Arvados-DCO-1.1-Signed-off-by: Stephen Smith --- diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx index cb53edbc..a44e8b7b 100644 --- a/src/components/context-menu/context-menu.tsx +++ b/src/components/context-menu/context-menu.tsx @@ -5,12 +5,15 @@ import React from "react"; 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; adminOnly?: boolean; + filters?: ((state: RootState, resource: ContextMenuResource) => boolean)[] } export type ContextMenuItemGroup = ContextMenuItem[]; diff --git a/src/store/context-menu/context-menu-filters.ts b/src/store/context-menu/context-menu-filters.ts new file mode 100644 index 00000000..53993fa5 --- /dev/null +++ b/src/store/context-menu/context-menu-filters.ts @@ -0,0 +1,40 @@ +// 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; +}; diff --git a/src/store/users/users-actions.ts b/src/store/users/users-actions.ts index d74e05ee..b553b324 100644 --- a/src/store/users/users-actions.ts +++ b/src/store/users/users-actions.ts @@ -11,13 +11,16 @@ import { dialogActions } from 'store/dialog/dialog-actions'; 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'; @@ -147,3 +150,27 @@ 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(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; +} diff --git a/src/views-components/context-menu/action-sets/user-action-set.ts b/src/views-components/context-menu/action-sets/user-action-set.ts index 6511b9a0..c298e1ab 100644 --- a/src/views-components/context-menu/action-sets/user-action-set.ts +++ b/src/views-components/context-menu/action-sets/user-action-set.ts @@ -17,6 +17,7 @@ import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab'; 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", @@ -41,35 +42,46 @@ export const userActionSet: ContextMenuActionSet = [[{ icon: UserPanelIcon, execute: (dispatch, { uuid }) => { dispatch(navigateToUserProfile(uuid)); - } -},], [{ + }, + filters: [needsUserProfileLink] +}],[{ name: "Activate User", - adminOnly: true, icon: ActiveIcon, execute: (dispatch, { uuid }) => { dispatch(openActivateDialog(uuid)); - } -},{ + }, + filters: [ + isAdmin, + canActivateUser, + ], +}, { name: "Setup User", - adminOnly: true, icon: AdminMenuIcon, execute: (dispatch, { uuid }) => { dispatch(openSetupDialog(uuid)); - } + }, + filters: [ + isAdmin, + canSetupUser, + ], }, { name: "Deactivate User", - adminOnly: true, icon: DeactivateUserIcon, execute: (dispatch, { uuid }) => { dispatch(openDeactivateDialog(uuid)); - } + }, + filters: [ + isAdmin, + canDeactivateUser, + ], }, { name: "Login As User", - adminOnly: true, icon: LoginAsIcon, execute: (dispatch, { uuid }) => { dispatch(loginAs(uuid)); - } -}, - -]]; + }, + filters: [ + isAdmin, + isOtherUser, + ], +}]]; diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx index 0409ec36..6f3a4389 100644 --- a/src/views-components/context-menu/context-menu.tsx +++ b/src/views-components/context-menu/context-menu.tsx @@ -14,10 +14,19 @@ import { sortByProperty } from "common/array-utils"; type DataProps = Pick & { resource?: ContextMenuResource }; const mapStateToProps = (state: RootState): DataProps => { const { open, position, resource } = state.contextMenu; - const isAdmin = state.auth.user?.isAdmin; + + 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, isAdmin), + items: filteredItems, open, resource }; @@ -60,16 +69,9 @@ export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => }; const emptyActionSet: ContextMenuActionSet = []; -const getMenuActionSet = (resource?: ContextMenuResource, isAdmin?: boolean): ContextMenuActionSet => { - if (resource) { - return menuActionSets - .get(resource.menuKind)! - .map((group) => (group.filter((item) => (item.adminOnly ? isAdmin : true)))) - || emptyActionSet - } else { - return emptyActionSet; - } -}; +const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => ( + resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet +); export enum ContextMenuKind { API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",