Merge branch '16583-intermediate-collections' refs #16583
authorPeter Amstutz <peter.amstutz@curii.com>
Mon, 23 May 2022 14:30:04 +0000 (10:30 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Mon, 23 May 2022 14:30:04 +0000 (10:30 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

cypress/integration/collection.spec.js
cypress/support/commands.js
src/components/autocomplete/autocomplete.tsx
src/models/user.test.ts
src/models/user.ts
src/store/virtual-machines/virtual-machines-actions.ts
src/views-components/sharing-dialog/participant-select.tsx
src/views-components/virtual-machines-dialog/add-login-dialog.tsx

index 568121f119f8c3b5aa500cab491aa4c2f55930d3..b62a34414fb58b57c4e6ac0d418f14dbce78553f 100644 (file)
@@ -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');
             });
index c2d78b54b2e71f3b295bbee47d63a6352193f926..842c9551f7ee9f436bbde6c3b787477248379aca 100644 (file)
@@ -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");
index cc1843367eb0b55fb64f36ea6051a398ae7e6f53..0044807b8a9ddb3e2abeb43a546dad5a2ada4d83 100644 (file)
@@ -175,7 +175,7 @@ export class Autocomplete<Value, Suggestion> extends React.Component<Autocomplet
                 <Chip
                     label={this.renderChipValue(item)}
                     key={index}
-                    onDelete={() => onDelete ? onDelete(item, index) : undefined} />
+                    onDelete={onDelete && !this.props.disabled ? (() =>  onDelete(item, index)) : undefined} />
         );
     }
 
index 4fab59d5db8db13008069d96b4252001f31b599c..30f82cf1821bad21bb5d144126edd2765b85b011 100644 (file)
@@ -34,7 +34,7 @@ describe('User', () => {
                     ownerUuid: 'zzzzz-tpzed-someusersowneruuid',
                     prefs: {}, isAdmin: false, isActive: true
                 },
-                expect: 'Some User <<someuser@example.com>>'
+                expect: 'Some User <someuser@example.com>'
             },
             {
                 caseName: 'Missing first name',
index 2857bce6bf14d2f71ad5d5f21bc0dacb9af9e2ad..9b3d97d8486337befae509d35761d93cf1edf6be 100644 (file)
@@ -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 {
index 7034b4a570f1ad1c135f6fc00bfdc6868b5190b0..e4b17ea0b807d4bfbeb544e9f1a811aedbec290b 100644 (file)
@@ -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<string>(),
@@ -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,
index 402faa7f9414d935c64ccc3ec677c98f5db8a00f..a826fcd59aaa9f6be62f0e5979861c679031474d 100644 (file)
@@ -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<HTMLInputElement>) => void;
     onFocus?: (event: React.FocusEvent<HTMLInputElement>) => 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<any> = 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();
 
index bfc047164a4bc8d00bdb15f82c8db19b422c6521..1654452bf493b656b031c648fa1faf96a1275e1a 100644 (file)
@@ -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<AddLoginFormData>;
 
-const AddLoginFormFields = () =>
-    <>
-        <UserField />
+const AddLoginFormFields = (props) => {
+    return <>
+        <ParticipantField
+            name={VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD}
+            component={props.data.updating ? ReadOnlyUserSelect : UserSelect}
+            excludedParticipants={props.data.excludedParticipants}
+        />
         <GroupArrayInput
             name={VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD}
             input={{id:"Add groups to VM login (eg: docker, sudo)", disabled:false}}
             required={false}
         />
     </>;
+}
+
+interface UserFieldProps {
+    excludedParticipants: string[];
+}
 
-const UserField = () =>
-    <Field
-        name={VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD}
-        component={UserSelect}
-        />;
+const ParticipantField = Field as new () => GenericField<UserFieldProps>;
 
-const UserSelect = ({ input, meta }: WrappedFieldProps) =>
+const UserSelect = (props) =>
     <ParticipantSelect
         onlyPeople
+        onlyActive
         label='Search for user to grant login permission'
-        items={input.value ? [input.value] : []}
-        onSelect={input.onChange}
-        onDelete={() => (input.onChange(''))} />;
+        items={props.input.value ? [props.input.value] : []}
+        excludedParticipants={props.excludedParticipants}
+        onSelect={props.input.onChange}
+        onDelete={() => (props.input.onChange(''))} />;
+
+const ReadOnlyUserSelect = (props) =>
+        <ParticipantSelect
+            onlyPeople
+            onlyActive
+            label='User'
+            items={props.input.value ? [props.input.value] : []}
+            disabled={true} />;