From: Lucas Di Pentima Date: Tue, 5 Apr 2022 01:00:47 +0000 (-0300) Subject: Merge branch '18966-collection-not-found-ui'. Closes #18966 X-Git-Url: https://git.arvados.org/arvados.git/commitdiff_plain/8045851b13e03215f3f2c8be6d54b43bd4619862?hp=41b06aa91cc156dba92c763beeac741d249ffb01 Merge branch '18966-collection-not-found-ui'. Closes #18966 Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- diff --git a/cypress/integration/user-profile.spec.js b/cypress/integration/user-profile.spec.js new file mode 100644 index 0000000000..7d21249c4b --- /dev/null +++ b/cypress/integration/user-profile.spec.js @@ -0,0 +1,449 @@ +// 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, + }); + }); + +}); diff --git a/cypress/integration/virtual-machine-admin.spec.js b/cypress/integration/virtual-machine-admin.spec.js index 73804b2030..f01a891106 100644 --- a/cypress/integration/virtual-machine-admin.spec.js +++ b/cypress/integration/virtual-machine-admin.spec.js @@ -64,6 +64,7 @@ describe('Virtual machine login manage tests', function() { 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') @@ -92,6 +93,7 @@ describe('Virtual machine login manage tests', function() { 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') @@ -160,8 +162,17 @@ describe('Virtual machine login manage tests', function() { 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) @@ -183,6 +194,7 @@ describe('Virtual machine login manage tests', function() { 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 diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx index 36f0903df3..a44e8b7bd4 100644 --- a/src/components/context-menu/context-menu.tsx +++ b/src/components/context-menu/context-menu.tsx @@ -5,11 +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/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx b/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx new file mode 100644 index 0000000000..3b2ff68a33 --- /dev/null +++ b/src/components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar.tsx @@ -0,0 +1,58 @@ +// 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 = (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 & DispatchProp; + +export const CopyToClipboardSnackbar = connect()(withStyles(styles)( + class CopyToClipboardSnackbar extends React.Component { + onCopy = () => { + this.props.dispatch(snackbarActions.OPEN_SNACKBAR({ + message: 'Copied', + hideDuration: 2000, + kind: SnackbarKind.SUCCESS + })); + }; + + render() { + const { children, value, classes } = this.props; + return ( + + + + {children || } + + + + ); + } + } +)); diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index 557e22e77c..19b4beea1e 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -68,11 +68,15 @@ import Computer from '@material-ui/icons/Computer'; 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, @@ -179,3 +183,8 @@ export const AddUserIcon: IconType = (props) => ; export const WordWrapIcon: IconType = (props) => ; export const TextIncreaseIcon: IconType = (props) => ; export const TextDecreaseIcon: IconType = (props) => ; +export const DeactivateUserIcon: IconType = (props) => ; +export const LoginAsIcon: IconType = (props) => ; +export const ActiveIcon: IconType = (props) => ; +export const SetupIcon: IconType = (props) => ; +export const InactiveIcon: IconType = (props) => ; diff --git a/src/components/select-field/select-field.tsx b/src/components/select-field/select-field.tsx index e4dcad6c10..6fa7ddea62 100644 --- a/src/components/select-field/select-field.tsx +++ b/src/components/select-field/select-field.tsx @@ -35,14 +35,18 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ } }); +interface NativeSelectFieldProps { + disabled?: boolean; +} + export const NativeSelectField = withStyles(styles) - ((props: WrappedFieldProps & WithStyles & { items: any[] }) => + ((props: WrappedFieldProps & NativeSelectFieldProps & WithStyles & { items: any[] }) =>