[ARVADOS-WORKBENCH2] updated: 2.3.0-209-gb13aaa89

Git user git at public.arvados.org
Mon Apr 4 20:28:41 UTC 2022


Summary of changes:
 cypress/integration/user-profile.spec.js           | 157 +++++++++++++++------
 src/components/context-menu/context-menu.tsx       |   3 +
 src/store/context-menu/context-menu-filters.ts     |  40 ++++++
 src/store/users/users-actions.ts                   |  29 +++-
 .../context-menu/action-sets/user-action-set.ts    |  40 ++++--
 src/views-components/context-menu/context-menu.tsx |  26 ++--
 6 files changed, 224 insertions(+), 71 deletions(-)
 create mode 100644 src/store/context-menu/context-menu-filters.ts

       via  b13aaa8909cc1f8b4edf8d32fda9580a3c899418 (commit)
       via  952bcc8f3ef686a2463931bc3f88457398163df7 (commit)
      from  a7031136f64556a75204141b327f694192235cfd (commit)

Those revisions listed above that are new to this repository have
not appeared on any other notification email; so we list those
revisions in full, below.


commit b13aaa8909cc1f8b4edf8d32fda9580a3c899418
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Apr 4 16:28:13 2022 -0400

    18559: Update and optimize cypress tests for user profile context menu
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/user-profile.spec.js b/cypress/integration/user-profile.spec.js
index 2af2a144..7d21249c 100644
--- a/cypress/integration/user-profile.spec.js
+++ b/cypress/integration/user-profile.spec.js
@@ -67,33 +67,46 @@ describe('User profile tests', function() {
         }
     }
 
-    beforeEach(function() {
-        cy.loginAs(adminUser);
-        cy.goToPath('/my-account');
-        enterProfileValues({
-            org: '',
-            org_email: '',
-            role: '',
-            website: '',
-        });
-        cy.get('[data-cy=profile-form] button[type="submit"]').then((btn) => {
-            if (!btn.is(':disabled')) {
-                btn.click();
-            }
-        });
+    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();
+    }
 
-        cy.goToPath('/user/' + activeUser.user.uuid);
-        enterProfileValues({
-            org: '',
-            org_email: '',
-            role: '',
-            website: '',
+    beforeEach(function() {
+        cy.updateResource(adminUser.token, 'users', adminUser.user.uuid, {
+            prefs: {
+                profile: {
+                    organization: '',
+                    organization_email: '',
+                    role: '',
+                    website_url: '',
+                },
+            },
         });
-        cy.get('[data-cy=profile-form] button[type="submit"]').then((btn) => {
-            if (!btn.is(':disabled')) {
-                btn.click();
-            }
+        cy.updateResource(adminUser.token, 'users', activeUser.user.uuid, {
+            prefs: {
+                profile: {
+                    organization: '',
+                    organization_email: '',
+                    role: '',
+                    website_url: '',
+                },
+            },
         });
     });
 
@@ -103,8 +116,14 @@ describe('User profile tests', function() {
         cy.get('header button[title="Account Management"]').click();
         cy.get('#account-menu').contains('My account').click();
 
-        // Admin tab should be hidden
-        cy.get('div [role="tab"]').should('not.contain', 'ADMIN');
+        // Admin actions should be hidden, no account menu
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: false,
+            login: false,
+            setup: false,
+        });
 
         // Check initial values
         assertProfileValues({
@@ -166,15 +185,14 @@ describe('User profile tests', function() {
         // Submit should be disabled
         cy.get('[data-cy=profile-form] button[type="submit"]').should('be.disabled');
 
-        // Admin context items should be hidden
-        cy.get('[data-cy=user-profile-panel-options-btn]').click();
-        cy.get('[data-cy=context-menu]').within(() => {
-            cy.get('[role=button]').should('not.contain', 'Activate User')
-            cy.get('[role=button]').should('not.contain', 'Deactivate User')
-            cy.get('[role=button]').should('not.contain', 'Login As User')
-            cy.get('[role=button]').should('not.contain', 'Setup User');
+        // Admin actions should be hidden, no account menu
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: false,
+            login: false,
+            setup: false,
         });
-        cy.get('div[role=presentation]').click();
     });
 
     it('admin can edit own profile', function() {
@@ -183,15 +201,14 @@ describe('User profile tests', function() {
         cy.get('header button[title="Account Management"]').click();
         cy.get('#account-menu').contains('My account').click();
 
-        // Admin context items should be visible
-        cy.get('[data-cy=user-profile-panel-options-btn]').click();
-        cy.get('[data-cy=context-menu]').within(() => {
-            cy.get('[role=button]').contains('Activate User')
-            cy.get('[role=button]').contains('Deactivate User')
-            cy.get('[role=button]').contains('Login As User')
-            cy.get('[role=button]').contains('Setup User');
+        // Admin actions should be visible, no account menu
+        assertContextMenuItems({
+            account: false,
+            activate: false,
+            deactivate: true,
+            login: false,
+            setup: false,
         });
-        cy.get('div[role=presentation]').click();
 
         // Check initial values
         assertProfileValues({
@@ -262,6 +279,15 @@ describe('User profile tests', function() {
             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() {
@@ -323,6 +349,13 @@ describe('User profile tests', function() {
         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();
@@ -334,6 +367,13 @@ describe('User profile tests', function() {
         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();
@@ -345,6 +385,13 @@ describe('User profile tests', function() {
         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();
@@ -356,17 +403,31 @@ describe('User profile tests', function() {
         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();
@@ -375,6 +436,14 @@ describe('User profile tests', function() {
         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,
+        });
     });
 
 });

commit 952bcc8f3ef686a2463931bc3f88457398163df7
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Apr 4 16:27:20 2022 -0400

    18559: Add context menu filter system for more complex context menus on user profile.
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

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<any>;
     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<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;
+}
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<any>(navigateToUserProfile(uuid));
-    }
-},], [{
+    },
+    filters: [needsUserProfileLink]
+}],[{
     name: "Activate User",
-    adminOnly: true,
     icon: ActiveIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openActivateDialog(uuid));
-    }
-},{
+    },
+    filters: [
+        isAdmin,
+        canActivateUser,
+    ],
+}, {
     name: "Setup User",
-    adminOnly: true,
     icon: AdminMenuIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openSetupDialog(uuid));
-    }
+    },
+    filters: [
+        isAdmin,
+        canSetupUser,
+    ],
 }, {
     name: "Deactivate User",
-    adminOnly: true,
     icon: DeactivateUserIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openDeactivateDialog(uuid));
-    }
+    },
+    filters: [
+        isAdmin,
+        canDeactivateUser,
+    ],
 }, {
     name: "Login As User",
-    adminOnly: true,
     icon: LoginAsIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(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<ContextMenuProps, "anchorEl" | "items" | "open"> & { 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",

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list