From: Stephen Smith Date: Fri, 13 May 2022 19:06:14 +0000 (-0400) Subject: Merge branch '19049-vm-admin-rough-edges' into main. Closes #19049 X-Git-Tag: 2.4.1~1^2~4 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/efda60590a2ed0942a8091d978d73fb047bd4a26?hp=1bc3f80b1aebb3c81b55e1afaad0a649aa9b3b9f Merge branch '19049-vm-admin-rough-edges' into main. Closes #19049 Arvados-DCO-1.1-Signed-off-by: Stephen Smith --- diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js index 568121f1..b62a3441 100644 --- a/cypress/integration/collection.spec.js +++ b/cypress/integration/collection.spec.js @@ -251,7 +251,7 @@ describe('Collection panel tests', function () { .should('contain', 'someKey: someValue') .and('not.contain', 'anotherKey: anotherValue'); // Check that the file listing show both read & write operations - cy.get('[data-cy=collection-files-panel]').within(() => { + cy.waitForDom().get('[data-cy=collection-files-panel]').within(() => { cy.get('[data-cy=collection-files-right-panel]', { timeout: 5000 }) .should('contain', fileName); if (isWritable) { @@ -842,7 +842,7 @@ describe('Collection panel tests', function () { cy.get('[data-cy=form-submit-btn]').click(); - cy.get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click(); + cy.waitForDom().get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click(); cy.get('main').contains(`Files extracted from: ${this.collection.name}`).should('exist'); }); diff --git a/cypress/support/commands.js b/cypress/support/commands.js index c2d78b54..842c9551 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -405,7 +405,10 @@ function b64toBlob(b64Data, contentType = '', sliceSize = 512) { // From https://github.com/cypress-io/cypress/issues/7306#issuecomment-1076451070= // This command requires the async package (https://www.npmjs.com/package/async) Cypress.Commands.add('waitForDom', () => { - cy.window().then(win => { + cy.window().then({ + // Don't timeout before waitForDom finishes + timeout: 10000 + }, win => { let timeElapsed = 0; cy.log("Waiting for DOM mutations to complete"); diff --git a/src/components/autocomplete/autocomplete.tsx b/src/components/autocomplete/autocomplete.tsx index cc184336..0044807b 100644 --- a/src/components/autocomplete/autocomplete.tsx +++ b/src/components/autocomplete/autocomplete.tsx @@ -175,7 +175,7 @@ export class Autocomplete extends React.Component onDelete ? onDelete(item, index) : undefined} /> + onDelete={onDelete && !this.props.disabled ? (() => onDelete(item, index)) : undefined} /> ); } diff --git a/src/models/user.test.ts b/src/models/user.test.ts index 4fab59d5..30f82cf1 100644 --- a/src/models/user.test.ts +++ b/src/models/user.test.ts @@ -34,7 +34,7 @@ describe('User', () => { ownerUuid: 'zzzzz-tpzed-someusersowneruuid', prefs: {}, isAdmin: false, isActive: true }, - expect: 'Some User <>' + expect: 'Some User ' }, { caseName: 'Missing first name', diff --git a/src/models/user.ts b/src/models/user.ts index 2857bce6..9b3d97d8 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -32,12 +32,16 @@ export const getUserFullname = (user: User) => { : ""; }; -export const getUserDisplayName = (user: User, withEmail = false) => { +export const getUserDisplayName = (user: User, withEmail = false, withUuid = false) => { const displayName = getUserFullname(user) || user.email || user.username || user.uuid; + let parts: string[] = [displayName]; if (withEmail && user.email && displayName !== user.email) { - return `${displayName} <<${user.email}>>`; + parts.push(`<${user.email}>`); } - return displayName; + if (withUuid) { + parts.push(`(${user.uuid})`); + } + return parts.join(' '); }; export interface UserResource extends Resource, User { diff --git a/src/store/virtual-machines/virtual-machines-actions.ts b/src/store/virtual-machines/virtual-machines-actions.ts index 7034b4a5..e4b17ea0 100644 --- a/src/store/virtual-machines/virtual-machines-actions.ts +++ b/src/store/virtual-machines/virtual-machines-actions.ts @@ -18,7 +18,7 @@ import { PermissionLevel } from "models/permission"; import { deleteResources, updateResources } from 'store/resources/resources-actions'; import { Participant } from "views-components/sharing-dialog/participant-select"; import { initialize, reset } from "redux-form"; -import { getUserDisplayName } from "models/user"; +import { getUserDisplayName, UserResource } from "models/user"; export const virtualMachinesActions = unionize({ SET_REQUESTED_DATE: ofType(), @@ -40,6 +40,7 @@ export const VIRTUAL_MACHINE_UPDATE_LOGIN_UUID_FIELD = 'uuid'; export const VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD = 'vmUuid'; export const VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD = 'user'; export const VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD = 'groups'; +export const VIRTUAL_MACHINE_ADD_LOGIN_EXCLUDE = 'excludedPerticipants'; export const openUserVirtualMachines = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { @@ -115,8 +116,22 @@ export const loadVirtualMachinesUserData = () => export const openAddVirtualMachineLoginDialog = (vmUuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { - dispatch(initialize(VIRTUAL_MACHINE_ADD_LOGIN_FORM, {[VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: vmUuid, [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: []})); - dispatch(dialogActions.OPEN_DIALOG( {id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: {}} )); + // Get login permissions of vm + const virtualMachines = await services.virtualMachineService.list(); + dispatch(updateResources(virtualMachines.items)); + const logins = await services.permissionService.list({ + filters: new FilterBuilder() + .addIn('head_uuid', virtualMachines.items.map(item => item.uuid)) + .addEqual('name', PermissionLevel.CAN_LOGIN) + .getFilters() + }); + dispatch(updateResources(logins.items)); + + dispatch(initialize(VIRTUAL_MACHINE_ADD_LOGIN_FORM, { + [VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: vmUuid, + [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: [], + })); + dispatch(dialogActions.OPEN_DIALOG( {id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: {excludedParticipants: logins.items.map(it => it.tailUuid)}} )); } export const openEditVirtualMachineLoginDialog = (permissionUuid: string) => @@ -125,7 +140,7 @@ export const openEditVirtualMachineLoginDialog = (permissionUuid: string) => const user = await services.userService.get(login.tailUuid); dispatch(initialize(VIRTUAL_MACHINE_ADD_LOGIN_FORM, { [VIRTUAL_MACHINE_UPDATE_LOGIN_UUID_FIELD]: permissionUuid, - [VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: {name: getUserDisplayName(user, true), uuid: login.tailUuid}, + [VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: {name: getUserDisplayName(user, true, true), uuid: login.tailUuid}, [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: login.properties.groups, })); dispatch(dialogActions.OPEN_DIALOG( {id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: {updating: true}} )); @@ -141,10 +156,15 @@ export interface AddLoginFormData { export const addUpdateVirtualMachineLogin = ({uuid, vmUuid, user, groups}: AddLoginFormData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => { + let userResource: UserResource | undefined = undefined; try { // Get user - const userResource = await services.userService.get(user.uuid); - + userResource = await services.userService.get(user.uuid, false); + } catch (e) { + dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Failed to get user details.", hideDuration: 2000, kind: SnackbarKind.ERROR })); + return; + } + try { if (uuid) { const permission = await services.permissionService.update(uuid, { tailUuid: userResource.uuid, diff --git a/src/views-components/sharing-dialog/participant-select.tsx b/src/views-components/sharing-dialog/participant-select.tsx index 402faa7f..a826fcd5 100644 --- a/src/views-components/sharing-dialog/participant-select.tsx +++ b/src/views-components/sharing-dialog/participant-select.tsx @@ -24,9 +24,12 @@ type ParticipantResource = GroupResource | UserResource; interface ParticipantSelectProps { items: Participant[]; + excludedParticipants?: string[]; label?: string; autofocus?: boolean; onlyPeople?: boolean; + onlyActive?: boolean; + disabled?: boolean; onBlur?: (event: React.FocusEvent) => void; onFocus?: (event: React.FocusEvent) => void; @@ -43,9 +46,9 @@ interface ParticipantSelectState { const getDisplayName = (item: GroupResource | UserResource) => { switch (item.kind) { case ResourceKind.USER: - return getUserDisplayName(item, true); + return getUserDisplayName(item, true, true); case ResourceKind.GROUP: - return item.name; + return item.name + `(${`(${(item as Resource).uuid})`})`; default: return (item as Resource).uuid; } @@ -71,11 +74,12 @@ export const ParticipantSelect = connect()( onChange={this.handleChange} onCreate={this.handleCreate} onSelect={this.handleSelect} - onDelete={this.handleDelete} + onDelete={this.props.onDelete && !this.props.disabled ? this.handleDelete : undefined} onFocus={this.props.onFocus} onBlur={this.props.onBlur} renderChipValue={this.renderChipValue} - renderSuggestion={this.renderSuggestion} /> + renderSuggestion={this.renderSuggestion} + disabled={this.props.disabled}/> ); } @@ -130,11 +134,14 @@ export const ParticipantSelect = connect()( const filterUsers = new FilterBuilder() .addILike('any', value) + .addEqual('is_active', this.props.onlyActive || undefined) + .addNotIn('uuid', this.props.excludedParticipants) .getFilters(); const userItems: ListResults = await userService.list({ filters: filterUsers, limit, count: "none" }); const filterGroups = new FilterBuilder() .addNotIn('group_class', [GroupClass.PROJECT, GroupClass.FILTER]) + .addNotIn('uuid', this.props.excludedParticipants) .addILike('name', value) .getFilters(); diff --git a/src/views-components/virtual-machines-dialog/add-login-dialog.tsx b/src/views-components/virtual-machines-dialog/add-login-dialog.tsx index bfc04716..1654452b 100644 --- a/src/views-components/virtual-machines-dialog/add-login-dialog.tsx +++ b/src/views-components/virtual-machines-dialog/add-login-dialog.tsx @@ -4,7 +4,7 @@ import React from 'react'; import { compose } from "redux"; -import { reduxForm, InjectedFormProps, WrappedFieldProps, Field } from 'redux-form'; +import { reduxForm, InjectedFormProps, Field, GenericField } from 'redux-form'; import { withDialog, WithDialogProps } from "store/dialog/with-dialog"; import { FormDialog } from 'components/form-dialog/form-dialog'; import { VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, VIRTUAL_MACHINE_ADD_LOGIN_FORM, addUpdateVirtualMachineLogin, AddLoginFormData, VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD, VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD } from 'store/virtual-machines/virtual-machines-actions'; @@ -31,26 +31,41 @@ export const VirtualMachineAddLoginDialog = compose( type CreateGroupDialogComponentProps = WithDialogProps<{updating: boolean}> & InjectedFormProps; -const AddLoginFormFields = () => - <> - +const AddLoginFormFields = (props) => { + return <> + ; +} + +interface UserFieldProps { + excludedParticipants: string[]; +} -const UserField = () => - ; +const ParticipantField = Field as new () => GenericField; -const UserSelect = ({ input, meta }: WrappedFieldProps) => +const UserSelect = (props) => (input.onChange(''))} />; + items={props.input.value ? [props.input.value] : []} + excludedParticipants={props.excludedParticipants} + onSelect={props.input.onChange} + onDelete={() => (props.input.onChange(''))} />; + +const ReadOnlyUserSelect = (props) => + ;