From: Lucas Di Pentima Date: Tue, 24 May 2022 15:26:37 +0000 (-0300) Subject: Merge branch '16115-sharing-links'. Closes #16115 X-Git-Tag: 2.4.1~1^2~2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/d1c2611350e47527730f6d3b226b998abf15a607?hp=2d230988f0c91d088e68bc0aecb1fd91d52c1f1f Merge branch '16115-sharing-links'. Closes #16115 Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- 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 74f44f7a..e98000fc 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -423,7 +423,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/public/webshell/index.html b/public/webshell/index.html index 028664c4..aae70a97 100644 --- a/public/webshell/index.html +++ b/public/webshell/index.html @@ -60,7 +60,7 @@ if (currentTime - lastTime > idleTimeoutMs) { //logout sh.reset(); - sh.sessionClosed(); + sh.sessionClosed("Session timed out after " + timeout + " seconds."); document.body.onmousemove = undefined; document.body.onkeydown = undefined; } else { @@ -88,10 +88,12 @@ sh.keysPressed(token + "\n"); sh.vt100('(sent authentication token)\n'); token = null; - updateIdleTimer(); - document.body.onmousemove = updateIdleTimer; - document.body.onkeydown = updateIdleTimer; - setTimeout(checkIdleTimer, 1000); + if (timeout > 0) { + updateIdleTimer(); + document.body.onmousemove = updateIdleTimer; + document.body.onkeydown = updateIdleTimer; + setTimeout(checkIdleTimer, 1000); + } } else { setTimeout(trySendToken, 200); } diff --git a/public/webshell/shell_in_a_box.js b/public/webshell/shell_in_a_box.js index c258b5d7..6b0a5b69 100644 --- a/public/webshell/shell_in_a_box.js +++ b/public/webshell/shell_in_a_box.js @@ -128,7 +128,7 @@ function ShellInABox(url, container) { }; extend(ShellInABox, VT100); -ShellInABox.prototype.sessionClosed = function() { +ShellInABox.prototype.sessionClosed = function(msg) { try { this.connected = false; if (this.session) { @@ -136,7 +136,7 @@ ShellInABox.prototype.sessionClosed = function() { if (this.cursorX > 0) { this.vt100('\r\n'); } - this.vt100('Session closed.'); + this.vt100(msg || 'Session closed.'); this.currentRequest.abort(); } // Revealing the "reconnect" button is commented out until we hook 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/collection.ts b/src/models/collection.ts index 9a5d2ee2..defaca76 100644 --- a/src/models/collection.ts +++ b/src/models/collection.ts @@ -70,4 +70,5 @@ export enum CollectionType { GENERAL = 'nil', OUTPUT = 'output', LOG = 'log', + INTERMEDIATE = 'intermediate', } 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/resource-type-filters/resource-type-filters.test.ts b/src/store/resource-type-filters/resource-type-filters.test.ts index 698515bd..a3684507 100644 --- a/src/store/resource-type-filters/resource-type-filters.test.ts +++ b/src/store/resource-type-filters/resource-type-filters.test.ts @@ -47,6 +47,7 @@ describe("serializeResourceTypeFilters", () => { deselectNode(ObjectTypeFilter.PROCESS), deselectNode(CollectionTypeFilter.GENERAL_COLLECTION), deselectNode(CollectionTypeFilter.LOG_COLLECTION), + deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION), )(); const serializedFilters = serializeResourceTypeFilters(filters); @@ -54,6 +55,20 @@ describe("serializeResourceTypeFilters", () => { .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.COLLECTION}"]],["collections.properties.type","in",["output"]]`); }); + it("should serialize intermediate collections and projects", () => { + const filters = pipe( + () => getInitialResourceTypeFilters(), + deselectNode(ObjectTypeFilter.PROCESS), + deselectNode(CollectionTypeFilter.GENERAL_COLLECTION), + deselectNode(CollectionTypeFilter.LOG_COLLECTION), + deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION), + )(); + + const serializedFilters = serializeResourceTypeFilters(filters); + expect(serializedFilters) + .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.COLLECTION}"]],["collections.properties.type","in",["intermediate"]]`); + }); + it("should serialize general and log collections", () => { const filters = pipe( () => getInitialResourceTypeFilters(), diff --git a/src/store/resource-type-filters/resource-type-filters.ts b/src/store/resource-type-filters/resource-type-filters.ts index a39807d5..0539cefe 100644 --- a/src/store/resource-type-filters/resource-type-filters.ts +++ b/src/store/resource-type-filters/resource-type-filters.ts @@ -38,6 +38,7 @@ export enum CollectionTypeFilter { GENERAL_COLLECTION = 'General', OUTPUT_COLLECTION = 'Output', LOG_COLLECTION = 'Log', + INTERMEDIATE_COLLECTION = 'Intermediate', } export enum ProcessTypeFilter { @@ -82,6 +83,7 @@ export const getInitialResourceTypeFilters = pipe( initFilter(ObjectTypeFilter.COLLECTION), initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION), initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION), + initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION), initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION), ), ); @@ -111,6 +113,7 @@ export const getTrashPanelTypeFilters = pipe( initFilter(ObjectTypeFilter.COLLECTION), initFilter(CollectionTypeFilter.GENERAL_COLLECTION, ObjectTypeFilter.COLLECTION), initFilter(CollectionTypeFilter.OUTPUT_COLLECTION, ObjectTypeFilter.COLLECTION), + initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION), initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION), ); @@ -167,6 +170,10 @@ const collectionTypeToPropertyValue = (type: CollectionTypeFilter) => { return CollectionType.OUTPUT; case CollectionTypeFilter.LOG_COLLECTION: return CollectionType.LOG; + case CollectionTypeFilter.INTERMEDIATE_COLLECTION: + return CollectionType.INTERMEDIATE; + default: + return CollectionType.GENERAL; } }; @@ -273,7 +280,7 @@ export const serializeSimpleObjectTypeFilters = (filters: Tree { +export const buildProcessStatusFilters = (fb: FilterBuilder, activeStatusFilter: string, resourcePrefix?: string): FilterBuilder => { switch (activeStatusFilter) { case ProcessStatusFilter.ONHOLD: { fb.addDistinct('state', ContainerRequestState.FINAL, resourcePrefix); diff --git a/src/store/run-process-panel/run-process-panel-actions.test.ts b/src/store/run-process-panel/run-process-panel-actions.test.ts index 7edc4cff..745be88f 100644 --- a/src/store/run-process-panel/run-process-panel-actions.test.ts +++ b/src/store/run-process-panel/run-process-panel-actions.test.ts @@ -131,6 +131,7 @@ describe("run-process-panel-actions", () => { }, schedulingParameters: { max_run_time: undefined }, state: "Committed", + useExisting: false }); // and diff --git a/src/store/run-process-panel/run-process-panel-actions.ts b/src/store/run-process-panel/run-process-panel-actions.ts index b421f072..adb5ade7 100644 --- a/src/store/run-process-panel/run-process-panel-actions.ts +++ b/src/store/run-process-panel/run-process-panel-actions.ts @@ -177,7 +177,8 @@ export const runProcess = async (dispatch: Dispatch, getState: () => RootSt properties: { workflowUuid: selectedWorkflow.uuid, workflowName: selectedWorkflow.name - } + }, + useExisting: false }; const newProcess = await services.containerRequestService.create(newProcessData); dispatch(navigateTo(newProcess.uuid)); 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) => + ;