From: Daniel Kutyła Date: Fri, 17 Dec 2021 23:09:37 +0000 (+0100) Subject: Merge remote-tracking branch 'origin/main' into 18207-Workbench2-is-not-clearing... X-Git-Tag: 2.4.0~17^2~4 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/394ebdfd13fe40a7096f484c46a353d2537f4c9a?hp=df0c0d462a52003cd722d25520cd7a4ad6583c57 Merge remote-tracking branch 'origin/main' into 18207-Workbench2-is-not-clearing-the-project-content-when-switching Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła --- diff --git a/.licenseignore b/.licenseignore index 853135fc..9b943a1f 100644 --- a/.licenseignore +++ b/.licenseignore @@ -14,3 +14,4 @@ public/* .npmrc src/lib/cwl-svg/* tools/arvados_config.yml +cypress/fixtures/files/5mb.bin diff --git a/Makefile b/Makefile index aaf2271c..ab58fc58 100644 --- a/Makefile +++ b/Makefile @@ -97,8 +97,9 @@ $(DEB_FILE): build --url="https://arvados.org" \ --license="GNU Affero General Public License, version 3.0" \ --description="$(DESCRIPTION)" \ - --config-files="etc/arvados/workbench2/workbench2.example.json" \ - $(WORKSPACE)/build/=$(DEST_DIR) + --config-files="etc/arvados/$(APP_NAME)/workbench2.example.json" \ + $(WORKSPACE)/build/=$(DEST_DIR) \ + etc/arvados/workbench2/workbench2.example.json=/etc/arvados/$(APP_NAME)/workbench2.example.json $(RPM_FILE): build fpm \ @@ -112,8 +113,9 @@ $(RPM_FILE): build --url="https://arvados.org" \ --license="GNU Affero General Public License, version 3.0" \ --description="$(DESCRIPTION)" \ - --config-files="etc/arvados/workbench2/workbench2.example.json" \ - $(WORKSPACE)/build/=$(DEST_DIR) + --config-files="etc/arvados/$(APP_NAME)/workbench2.example.json" \ + $(WORKSPACE)/build/=$(DEST_DIR) \ + etc/arvados/workbench2/workbench2.example.json=/etc/arvados/$(APP_NAME)/workbench2.example.json copy: $(DEB_FILE) $(RPM_FILE) for target in $(TARGETS) ; do \ @@ -130,5 +132,16 @@ copy: $(DEB_FILE) $(RPM_FILE) # use FPM to create DEB and RPM packages: copy +packages-in-docker: workbench2-build-image + docker run --env ci="true" \ + --env ARVADOS_DIRECTORY=/tmp/arvados \ + --env APP_NAME=${APP_NAME} \ + --env ITERATION=${ITERATION} \ + --env TARGETS="${TARGETS}" \ + -w="/tmp/workbench2" \ + -t -v ${WORKSPACE}:/tmp/workbench2 \ + -v ${ARVADOS_DIRECTORY}:/tmp/arvados workbench2-build:latest \ + make packages + workbench2-build-image: (cd docker && docker build -t workbench2-build .) diff --git a/README.md b/README.md index 8bb50dbe..4ec4bd1c 100644 --- a/README.md +++ b/README.md @@ -82,7 +82,6 @@ Currently this configuration schema is supported: ``` { "API_HOST": "string", - "VOCABULARY_URL": "string", "FILE_VIEWERS_CONFIG_URL": "string", } ``` @@ -93,12 +92,6 @@ The Arvados base URL. The `REACT_APP_ARVADOS_API_HOST` environment variable can be used to set the default URL if the run time configuration is unreachable. -### VOCABULARY_URL -Local path, or any URL that allows cross-origin requests. See -[Vocabulary JSON file example](public/vocabulary-example.json). - -To use the URL defined in the Arvados cluster configuration, remove the entire `VOCABULARY_URL` entry from the runtime configuration. Found in `/config.json` by default. - ## FILE_VIEWERS_CONFIG_URL Local path, or any URL that allows cross-origin requests. See: diff --git a/cypress/fixtures/files/5mb.bin b/cypress/fixtures/files/5mb.bin new file mode 100644 index 00000000..d52f252e Binary files /dev/null and b/cypress/fixtures/files/5mb.bin differ diff --git a/cypress/integration/collection.spec.js b/cypress/integration/collection.spec.js index fb126af6..82a26cef 100644 --- a/cypress/integration/collection.spec.js +++ b/cypress/integration/collection.spec.js @@ -97,14 +97,37 @@ describe('Collection panel tests', function () { }); // Confirm proper vocabulary labels are displayed on the UI. cy.get('[data-cy=collection-properties-panel]') - .should('contain', 'Color') - .and('contain', 'Magenta'); + .should('contain', 'Color: Magenta'); // Confirm proper vocabulary IDs were saved on the backend. cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`) .its('body').as('collection') .then(function () { expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3'); }); + + // Case-insensitive on-blur auto-selection test + // Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2) + cy.get('[data-cy=resource-properties-form]').within(() => { + cy.get('[data-cy=property-field-key]').within(() => { + cy.get('input').type('sIzE'); + }); + cy.get('[data-cy=property-field-value]').within(() => { + cy.get('input').type('sMaLL'); + }); + // Cannot "type()" TAB on Cypress so let's click another field + // to trigger the onBlur event. + cy.get('[data-cy=property-field-key]').click(); + cy.root().submit(); + }); + // Confirm proper vocabulary labels are displayed on the UI. + cy.get('[data-cy=collection-properties-panel]') + .should('contain', 'Size: S'); + // Confirm proper vocabulary IDs were saved on the backend. + cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`) + .its('body').as('collection') + .then(function () { + expect(this.collection.properties.IDTAGSIZES).to.equal('IDVALSIZES2'); + }); }); }); @@ -572,6 +595,86 @@ describe('Collection panel tests', function () { }) }); + it('moves a collection to a different project', function () { + const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`; + const projName = `Test Project ${Math.floor(Math.random() * 999999)}`; + const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`; + + cy.createCollection(adminUser.token, { + name: collName, + owner_uuid: activeUser.user.uuid, + manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`, + }).as('testCollection'); + cy.createGroup(adminUser.token, { + name: projName, + group_class: 'project', + owner_uuid: activeUser.user.uuid, + }).as('testProject'); + + cy.getAll('@testCollection', '@testProject') + .then(function ([testCollection, testProject]) { + cy.loginAs(activeUser); + cy.goToPath(`/collections/${testCollection.uuid}`); + cy.get('[data-cy=collection-files-panel]').should('contain', fileName); + cy.get('[data-cy=collection-info-panel]') + .should('not.contain', projName) + .and('not.contain', testProject.uuid); + cy.get('[data-cy=collection-panel-options-btn]').click(); + cy.get('[data-cy=context-menu]').contains('Move to').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'Move to') + .within(() => { + cy.get('[data-cy=projects-tree-home-tree-picker]') + .find('i') + .click(); + cy.get('[data-cy=projects-tree-home-tree-picker]') + .contains(projName) + .click(); + }); + cy.get('[data-cy=form-submit-btn]').click(); + cy.get('[data-cy=snackbar]') + .contains('Collection has been moved') + cy.get('[data-cy=collection-info-panel]') + .contains(projName).and('contain', testProject.uuid); + // Double check that the collection is in the project + cy.goToPath(`/projects/${testProject.uuid}`); + cy.get('[data-cy=project-panel]').should('contain', collName); + }); + }); + + it('makes a copy of an existing collection', function() { + const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`; + const copyName = `Copy of: ${collName}`; + + cy.createCollection(adminUser.token, { + name: collName, + owner_uuid: activeUser.user.uuid, + manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n", + }).as('collection').then(function () { + cy.loginAs(activeUser) + cy.goToPath(`/collections/${this.collection.uuid}`); + cy.get('[data-cy=collection-files-panel]') + .should('contain', 'some-file'); + cy.get('[data-cy=collection-panel-options-btn]').click(); + cy.get('[data-cy=context-menu]').contains('Make a copy').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'Make a copy') + .within(() => { + cy.get('[data-cy=projects-tree-home-tree-picker]') + .contains('Projects') + .click(); + cy.get('[data-cy=form-submit-btn]').click(); + }); + cy.get('[data-cy=snackbar]') + .contains('Collection has been copied.') + cy.get('[data-cy=snackbar-goto-action]').click(); + cy.get('[data-cy=project-panel]') + .contains(copyName).click(); + cy.get('[data-cy=collection-files-panel]') + .should('contain', 'some-file'); + }); + }); + it('uses the collection version browser to view a previous version', function () { const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`; @@ -770,4 +873,84 @@ describe('Collection panel tests', function () { .contains(adminUser.user.uuid); }); }); + + describe('file upload', () => { + beforeEach(() => { + cy.createCollection(adminUser.token, { + name: `Test collection ${Math.floor(Math.random() * 999999)}`, + owner_uuid: activeUser.user.uuid, + manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n" + }) + .as('testCollection1'); + }); + + it('allows to cancel running upload', () => { + cy.getAll('@testCollection1') + .then(function([testCollection1]) { + cy.loginAs(activeUser); + + cy.goToPath(`/collections/${testCollection1.uuid}`); + + cy.get('[data-cy=upload-button]').click(); + + cy.fixture('files/5mb.bin', 'base64').then(content => { + cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin'); + cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin'); + + cy.get('[data-cy=form-submit-btn]').click(); + + cy.get('button').contains('Cancel').click(); + + cy.get('[data-cy=form-submit-btn]').should('not.exist'); + }); + }); + }); + + it('allows to cancel single file from the running upload', () => { + cy.getAll('@testCollection1') + .then(function([testCollection1]) { + cy.loginAs(activeUser); + + cy.goToPath(`/collections/${testCollection1.uuid}`); + + cy.get('[data-cy=upload-button]').click(); + + cy.fixture('files/5mb.bin', 'base64').then(content => { + cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin'); + cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin'); + + cy.get('[data-cy=form-submit-btn]').click(); + + cy.get('button[aria-label=Remove]').eq(1).click(); + + cy.get('[data-cy=form-submit-btn]').should('not.exist'); + + cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist'); + }); + }); + }); + + it('allows to cancel all files from the running upload', () => { + cy.getAll('@testCollection1') + .then(function([testCollection1]) { + cy.loginAs(activeUser); + + cy.goToPath(`/collections/${testCollection1.uuid}`); + + cy.get('[data-cy=upload-button]').click(); + + cy.fixture('files/5mb.bin', 'base64').then(content => { + cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin'); + cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin'); + + cy.get('[data-cy=form-submit-btn]').click(); + + cy.get('button[aria-label=Remove]').should('exist'); + cy.get('button[aria-label=Remove]').click({ multiple: true, force: true }); + + cy.get('[data-cy=form-submit-btn]').should('not.exist'); + }); + }); + }); + }); }) diff --git a/cypress/integration/favorites.spec.js b/cypress/integration/favorites.spec.js index 9f4e2b84..9bc90ebd 100644 --- a/cypress/integration/favorites.spec.js +++ b/cypress/integration/favorites.spec.js @@ -44,7 +44,8 @@ describe('Favorites tests', function () { }); }); - it('can copy selected into the collection', () => { + // Disabled while addressing #18587 + it.skip('can copy selected into the collection', () => { cy.createCollection(adminUser.token, { name: `Test source collection ${Math.floor(Math.random() * 999999)}`, manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n" @@ -150,7 +151,7 @@ describe('Favorites tests', function () { cy.getAll('@mySharedWritableProject', '@testTargetCollection') .then(function ([mySharedWritableProject, testTargetCollection]) { cy.loginAs(adminUser); - + cy.get('[data-cy=side-panel-tree]').contains('My Favorites').click(); const newProjectName = `New project name ${mySharedWritableProject.name}`; @@ -160,7 +161,7 @@ describe('Favorites tests', function () { cy.testEditProjectOrCollection('main', mySharedWritableProject.name, newProjectName, newProjectDescription); cy.testEditProjectOrCollection('main', testTargetCollection.name, newCollectionName, newCollectionDescription, false); - + cy.get('[data-cy=side-panel-tree]').contains('Projects').click(); cy.get('main').contains(newProjectName).rightclick(); @@ -171,7 +172,7 @@ describe('Favorites tests', function () { cy.get('[data-cy=side-panel-tree]').contains('Public Favorites').click(); cy.testEditProjectOrCollection('main', newProjectName, mySharedWritableProject.name, 'newProjectDescription'); - cy.testEditProjectOrCollection('main', newCollectionName, testTargetCollection.name, 'newCollectionDescription', false); + cy.testEditProjectOrCollection('main', newCollectionName, testTargetCollection.name, 'newCollectionDescription', false); }); }); diff --git a/cypress/integration/group-manage.spec.js b/cypress/integration/group-manage.spec.js new file mode 100644 index 00000000..c98c2201 --- /dev/null +++ b/cypress/integration/group-manage.spec.js @@ -0,0 +1,284 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +describe('Group manage tests', function() { + let activeUser; + let adminUser; + let otherUser; + let userThree; + const groupName = `Test 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; + } + ); + cy.getUser('otheruser', 'Other', 'User', false, true) + .as('otherUser').then(function() { + otherUser = this.otherUser; + } + ); + cy.getUser('userThree', 'User', 'Three', false, true) + .as('userThree').then(function() { + userThree = this.userThree; + } + ); + }); + + beforeEach(function() { + cy.clearCookies(); + cy.clearLocalStorage(); + }); + + it('creates a new group', function() { + cy.loginAs(activeUser); + + // Navigate to Groups + cy.get('[data-cy=side-panel-tree]').contains('Groups').click(); + + // Create new group + cy.get('[data-cy=groups-panel-new-group]').click(); + cy.get('[data-cy=form-dialog]') + .should('contain', 'Create Group') + .within(() => { + cy.get('input[name=name]').type(groupName); + cy.get('[data-cy=users-field] input').type("three"); + }); + cy.get('[role=tooltip]').click(); + cy.get('[data-cy=form-dialog] button[type=submit]').click(); + + // Check that the group was created + cy.get('[data-cy=groups-panel-data-explorer]').contains(groupName).click(); + cy.get('[data-cy=group-members-data-explorer]').contains(activeUser.user.full_name); + cy.get('[data-cy=group-members-data-explorer]').contains(userThree.user.full_name); + }); + + it('adds users to the group', function() { + // Add other user to the group + cy.get('[data-cy=group-member-add]').click(); + cy.get('.sharing-dialog') + .should('contain', 'Sharing settings') + .within(() => { + cy.get('[data-cy=invite-people-field] input').type("other"); + }); + cy.get('[role=tooltip]').click(); + cy.get('.sharing-dialog').contains('Save').click(); + + // Check that both users are present with appropriate permissions + cy.get('[data-cy=group-members-data-explorer]') + .contains(otherUser.user.full_name) + .parents('tr') + .within(() => { + cy.contains('Read'); + }); + cy.get('[data-cy=group-members-data-explorer] tr') + .contains(activeUser.user.full_name) + .parents('tr') + .within(() => { + cy.contains('Manage'); + }); + }); + + it('changes permission level of a member', function() { + // Test change permission level + cy.get('[data-cy=group-members-data-explorer]') + .contains(otherUser.user.full_name) + .parents('tr') + .within(() => { + cy.contains('Read') + .parents('td') + .within(() => { + cy.get('button').click(); + }); + }); + cy.get('[data-cy=context-menu]') + .contains('Write') + .click(); + cy.get('[data-cy=group-members-data-explorer]') + .contains(otherUser.user.full_name) + .parents('tr') + .within(() => { + cy.contains('Write'); + }); + }); + + it('can unhide and re-hide users', function() { + // Must use admin user to have manage permission on user + cy.loginAs(adminUser); + cy.get('[data-cy=side-panel-tree]').contains('Groups').click(); + cy.get('[data-cy=groups-panel-data-explorer]').contains(groupName).click(); + + // Check that other user is hidden + cy.get('[data-cy=group-details-permissions-tab]').click(); + cy.get('[data-cy=group-permissions-data-explorer]') + .should('not.contain', otherUser.user.full_name) + cy.get('[data-cy=group-details-members-tab]').click(); + + // Test unhide + cy.get('[data-cy=group-members-data-explorer]') + .contains(otherUser.user.full_name) + .parents('tr') + .within(() => { + cy.get('[data-cy=user-visible-checkbox]').click(); + }); + // Check that other user is visible + cy.get('[data-cy=group-details-permissions-tab]').click(); + cy.get('[data-cy=group-permissions-data-explorer]') + .contains(otherUser.user.full_name) + .parents('tr') + .within(() => { + cy.contains('Read'); + }); + // Test re-hide + cy.get('[data-cy=group-details-members-tab]').click(); + cy.get('[data-cy=group-members-data-explorer]') + .contains(otherUser.user.full_name) + .parents('tr') + .within(() => { + cy.get('[data-cy=user-visible-checkbox]').click(); + }); + // Check that other user is hidden + cy.get('[data-cy=group-details-permissions-tab]').click(); + cy.get('[data-cy=group-permissions-data-explorer]') + .should('not.contain', otherUser.user.full_name) + }); + + it('displays resources shared with the group', function() { + // Switch to activeUser + cy.loginAs(activeUser); + cy.get('[data-cy=side-panel-tree]').contains('Groups').click(); + + // Get groupUuid and create shared project + cy.get('[data-cy=groups-panel-data-explorer]') + .contains(groupName) + .parents('tr') + .find('[data-cy=uuid]') + .invoke('text') + .as('groupUuid') + .then((groupUuid) => { + cy.createProject({ + owningUser: activeUser, + projectName: 'test-project', + }).as('testProject').then((testProject) => { + cy.shareWith(activeUser.token, groupUuid, testProject.uuid, 'can_read'); + }); + }); + + // Check that the project is listed in permissions + cy.get('[data-cy=groups-panel-data-explorer]').contains(groupName).click(); + cy.get('[data-cy=group-details-permissions-tab]').click(); + cy.get('[data-cy=group-permissions-data-explorer]') + .contains('test-project') + .parents('tr') + .within(() => { + cy.contains('Read'); + }); + }); + + it('removes users from the group', function() { + cy.get('[data-cy=side-panel-tree]').contains('Groups').click(); + cy.get('[data-cy=groups-panel-data-explorer]').contains(groupName).click(); + + // Remove other user + cy.get('[data-cy=group-members-data-explorer]') + .contains(otherUser.user.full_name) + .parents('tr') + .within(() => { + cy.get('[data-cy=resource-delete-button]').click(); + }); + cy.get('[data-cy=confirmation-dialog-ok-btn]').click(); + cy.get('[data-cy=group-members-data-explorer]') + .should('not.contain', otherUser.user.full_name); + + // Remove user three + cy.get('[data-cy=group-members-data-explorer]') + .contains(userThree.user.full_name) + .parents('tr') + .within(() => { + cy.get('[data-cy=resource-delete-button]').click(); + }); + cy.get('[data-cy=confirmation-dialog-ok-btn]').click(); + cy.get('[data-cy=group-members-data-explorer]') + .should('not.contain', userThree.user.full_name); + }); + + it('renames the group', function() { + // Navigate to Groups + cy.get('[data-cy=side-panel-tree]').contains('Groups').click(); + + // Open rename dialog + cy.get('[data-cy=groups-panel-data-explorer]') + .contains(groupName) + .rightclick(); + cy.get('[data-cy=context-menu]') + .contains('Rename') + .click(); + + // Rename the group + cy.get('[data-cy=form-dialog]') + .should('contain', 'Edit Group') + .within(() => { + cy.get('input[name=name]').clear().type(groupName + ' (renamed)'); + cy.get('button[type=submit]').click(); + }); + + // Check that the group was renamed + cy.get('[data-cy=groups-panel-data-explorer]') + .contains(groupName + ' (renamed)'); + }); + + it('deletes the group', function() { + // Navigate to Groups + cy.get('[data-cy=side-panel-tree]').contains('Groups').click(); + + // Delete the group + cy.get('[data-cy=groups-panel-data-explorer]') + .contains(groupName + ' (renamed)') + .rightclick(); + cy.get('[data-cy=context-menu]') + .contains('Remove') + .click(); + cy.get('[data-cy=confirmation-dialog-ok-btn]').click(); + + // Check that the group was deleted + cy.get('[data-cy=groups-panel-data-explorer]') + .should('not.contain', groupName + ' (renamed)'); + }); + + it('disables group-related controls for built-in groups', function() { + cy.loginAs(adminUser); + + ['All users', 'Anonymous users', 'System group'].forEach((builtInGroup) => { + cy.get('[data-cy=side-panel-tree]').contains('Groups').click(); + cy.get('[data-cy=groups-panel-data-explorer]').contains(builtInGroup).click(); + + // Check group member actions + cy.get('[data-cy=group-members-data-explorer]') + .within(() => { + cy.get('[data-cy=group-member-add]').should('not.exist'); + cy.get('[data-cy=user-visible-checkbox] input').should('be.disabled'); + cy.get('[data-cy=resource-delete-button]').should('be.disabled'); + cy.get('[data-cy=edit-permission-button]').should('not.exist'); + }); + + // Check permissions actions + cy.get('[data-cy=group-details-permissions-tab]').click(); + cy.get('[data-cy=group-permissions-data-explorer]').within(() => { + cy.get('[data-cy=resource-delete-button]').should('be.disabled'); + cy.get('[data-cy=edit-permission-button]').should('not.exist'); + }); + }); + }); + +}); diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js index af2d93e3..b3d6bbed 100644 --- a/cypress/integration/project.spec.js +++ b/cypress/integration/project.spec.js @@ -179,4 +179,38 @@ describe('Project tests', function() { cy.get('[data-cy=not-found-page]').should('not.exist'); }); }); + + it('shows details panel when clicking on the info icon', () => { + cy.createGroup(activeUser.token, { + name: `Test root project ${Math.floor(Math.random() * 999999)}`, + group_class: 'project', + }).as('testRootProject').then(function(testRootProject) { + cy.loginAs(activeUser); + + cy.get('[data-cy=side-panel-tree]').contains(testRootProject.name).click(); + + cy.get('[data-cy=additional-info-icon]').click(); + + cy.contains(testRootProject.uuid).should('exist'); + }); + }); + + it('clears search input when changing project', () => { + cy.createGroup(activeUser.token, { + name: `Test root project ${Math.floor(Math.random() * 999999)}`, + group_class: 'project', + }).as('testProject1'); + + cy.getAll('@testProject1').then(function([testProject1]) { + cy.loginAs(activeUser); + + cy.get('[data-cy=side-panel-tree]').contains(testProject1.name).click(); + + cy.get('[data-cy=search-input] input').type('test123'); + + cy.get('[data-cy=side-panel-tree]').contains('Projects').click(); + + cy.get('[data-cy=search-input] input').should('not.have.value', 'test123'); + }); + }); }); \ No newline at end of file diff --git a/cypress/integration/side-panel.spec.js b/cypress/integration/side-panel.spec.js index 912e68eb..f9d4dca3 100644 --- a/cypress/integration/side-panel.spec.js +++ b/cypress/integration/side-panel.spec.js @@ -114,4 +114,30 @@ describe('Side panel tests', function() { }); }); + it('side panel react to refresh when project data changes', () => { + const project = 'writableProject'; + + cy.createProject({ + owningUser: activeUser, + targetUser: activeUser, + projectName: project, + canWrite: true, + addToFavorites: false + }); + + cy.getAll('@writableProject') + .then(function ([writableProject]) { + cy.loginAs(activeUser); + + cy.get('[data-cy=side-panel-tree]').contains('Projects').click(); + + cy.get('[data-cy=side-panel-tree]').contains(writableProject.name).should('exist'); + + cy.trashGroup(activeUser.token, writableProject.uuid); + + cy.contains('Refresh').click(); + + cy.contains(writableProject.name).should('not.exist'); + }); + }); }) diff --git a/cypress/support/commands.js b/cypress/support/commands.js index 069ed96d..07290e55 100644 --- a/cypress/support/commands.js +++ b/cypress/support/commands.js @@ -280,4 +280,42 @@ Cypress.Commands.add('createProject', ({ cy.addToFavorites(user.token, user.user.uuid, project.uuid); } }); -}); \ No newline at end of file +}); + +Cypress.Commands.add( + 'upload', + { + prevSubject: 'element', + }, + (subject, file, fileName) => { + cy.window().then(window => { + const blob = b64toBlob(file, '', 512); + const testFile = new window.File([blob], fileName); + + cy.wrap(subject).trigger('drop', { + dataTransfer: { files: [testFile] }, + }); + }) + } +) + +function b64toBlob(b64Data, contentType = '', sliceSize = 512) { + const byteCharacters = atob(b64Data) + const byteArrays = [] + + for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) { + const slice = byteCharacters.slice(offset, offset + sliceSize); + + const byteNumbers = new Array(slice.length); + for (let i = 0; i < slice.length; i++) { + byteNumbers[i] = slice.charCodeAt(i); + } + + const byteArray = new Uint8Array(byteNumbers); + + byteArrays.push(byteArray); + } + + const blob = new Blob(byteArrays, { type: contentType }); + return blob +} \ No newline at end of file diff --git a/docker/Dockerfile b/docker/Dockerfile index 729d62c4..3bffcac4 100644 --- a/docker/Dockerfile +++ b/docker/Dockerfile @@ -11,8 +11,16 @@ RUN apt-get update && \ libsecret-1-0 libsecret-1-dev rpm ruby ruby-dev rubygems build-essential \ libpam0g-dev libgbm1 git && \ apt-get clean -RUN apt-get -yq --no-install-recommends -t buster-backports install golang-go && \ - apt-get clean + +# Get Go 1.16.9 +RUN cd /usr/src && \ + wget https://golang.org/dl/go1.16.9.linux-amd64.tar.gz && \ + tar xzf go1.16.9.linux-amd64.tar.gz && \ + ln -s /usr/src/go/bin/go /usr/local/bin/go-1.16.9 && \ + ln -s /usr/src/go/bin/gofmt /usr/local/bin/gofmt-1.16.9 && \ + ln -s /usr/local/bin/go-1.16.9 /usr/local/bin/go && \ + ln -s /usr/local/bin/gofmt-1.16.9 /usr/local/bin/gofmt + RUN gem install --no-ri --no-rdoc fpm RUN git clone https://git.arvados.org/arvados.git && cd arvados && \ go mod download && \ diff --git a/src/common/config.ts b/src/common/config.ts index 56f7c488..2518c95e 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -51,7 +51,6 @@ export interface ClusterConfigJSON { }; Workbench: { ArvadosDocsite: string; - VocabularyURL: string; FileViewersConfigURL: string; WelcomePageHTML: string; InactivePageHTML: string; @@ -204,15 +203,10 @@ remove the entire ${varName} entry from ${WORKBENCH_CONFIG_URL}`); } config.fileViewersConfigUrl = fileViewerConfigUrl; - let vocabularyUrl; if (workbenchConfig.VOCABULARY_URL !== undefined) { - warnLocalConfig("VOCABULARY_URL"); - vocabularyUrl = workbenchConfig.VOCABULARY_URL; + console.warn(`A value for VOCABULARY_URL was found in ${WORKBENCH_CONFIG_URL}. It will be ignored as the cluster already provides its own endpoint, you can safely remove it.`) } - else { - vocabularyUrl = config.clusterConfig.Workbench.VocabularyURL || "/vocabulary-example.json"; - } - config.vocabularyUrl = vocabularyUrl; + config.vocabularyUrl = getVocabularyURL(workbenchConfig.API_HOST); return { config, apiHost: workbenchConfig.API_HOST }; }); @@ -240,7 +234,6 @@ export const mockClusterConfigJSON = (config: Partial): Clust }, Workbench: { ArvadosDocsite: "", - VocabularyURL: "", FileViewersConfigURL: "", WelcomePageHTML: "", InactivePageHTML: "", @@ -315,5 +308,7 @@ const getDefaultConfig = (): WorkbenchConfig => { export const ARVADOS_API_PATH = "arvados/v1"; export const CLUSTER_CONFIG_PATH = "arvados/v1/config"; +export const VOCABULARY_PATH = "arvados/v1/vocabulary"; export const DISCOVERY_DOC_PATH = "discovery/v1/apis/arvados/v1/rest"; -export const getClusterConfigURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`; +export const getClusterConfigURL = (apiHost: string) => `https://${apiHost}/${CLUSTER_CONFIG_PATH}?nocache=${(new Date()).getTime()}`; +export const getVocabularyURL = (apiHost: string) => `https://${apiHost}/${VOCABULARY_PATH}?nocache=${(new Date()).getTime()}`; diff --git a/src/common/labels.ts b/src/common/labels.ts index f534bd2b..682513fb 100644 --- a/src/common/labels.ts +++ b/src/common/labels.ts @@ -11,6 +11,8 @@ export const resourceLabel = (type: string, subtype = '') => { case ResourceKind.PROJECT: if (subtype === "filter") { return "Filter group"; + } else if (subtype === "role") { + return "Group"; } return "Project"; case ResourceKind.PROCESS: diff --git a/src/common/webdav.ts b/src/common/webdav.ts index 758a5e18..93ec21cb 100644 --- a/src/common/webdav.ts +++ b/src/common/webdav.ts @@ -84,6 +84,15 @@ export class WebDAV { .keys(headers) .forEach(key => r.setRequestHeader(key, headers[key])); + if (!(window as any).cancelTokens) { + Object.assign(window, { cancelTokens: {} }); + } + + (window as any).cancelTokens[config.url] = () => { + resolve(r); + r.abort(); + } + if (config.onUploadProgress) { r.upload.addEventListener('progress', config.onUploadProgress); } diff --git a/src/components/chips/chips.tsx b/src/components/chips/chips.tsx index eb68ed7a..c4724d1b 100644 --- a/src/components/chips/chips.tsx +++ b/src/components/chips/chips.tsx @@ -38,7 +38,7 @@ export const Chips = withStyles(styles)( render() { const { values, filler } = this.props; return - {values.map(this.renderChip)} + {values && values.map(this.renderChip)} {filler && {filler}} ; } diff --git a/src/components/collection-panel-files/collection-panel-files.tsx b/src/components/collection-panel-files/collection-panel-files.tsx index 97cbc8ce..1ef6b5c9 100644 --- a/src/components/collection-panel-files/collection-panel-files.tsx +++ b/src/components/collection-panel-files/collection-panel-files.tsx @@ -48,7 +48,6 @@ const styles: StyleRulesCallback = (theme: Theme) => ({ wrapper: { display: 'flex', minHeight: '600px', - marginBottom: '1rem', color: 'rgba(0, 0, 0, 0.87)', fontSize: '0.875rem', fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif', @@ -463,7 +462,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
1 ? classes.searchWrapper : classes.searchWrapperHidden}> - +
{ @@ -490,7 +489,7 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState data-parent-path={name} className={classNames(classes.row, getActiveClass(name))} key={id}> - {getItemIcon(type, getActiveClass(name))} + {getItemIcon(type, getActiveClass(name))}
{name}
@@ -510,14 +509,19 @@ export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState
- +
{ isWritable &&
- } - className={classes.cardSubheader} - classes={{ action: classes.button }} - action={<> - {isWritable && - } - {!tooManyFiles && - - onOptionsMenuOpen(ev, isWritable)}> - - - } - - } /> - {tooManyFiles - ?
- File listing may take some time, please click to browse: -
- : <> - - - Name - - - File size - - - {isLoading - ?
- :
- onItemMenuOpen(ev, item, isWritable)} - {...treeProps} />
} - - } - ); -}; - -export const CollectionPanelFiles = withStyles(styles)(CollectionPanelFilesComponent); diff --git a/src/components/data-explorer/data-explorer.tsx b/src/components/data-explorer/data-explorer.tsx index d272e870..55840ae9 100644 --- a/src/components/data-explorer/data-explorer.tsx +++ b/src/components/data-explorer/data-explorer.tsx @@ -11,17 +11,19 @@ import { SearchInput } from 'components/search-input/search-input'; import { ArvadosTheme } from "common/custom-theme"; import { createTree } from 'models/tree'; import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree'; -import { MoreOptionsIcon } from 'components/icon/icon'; +import { CloseIcon, MaximizeIcon, MoreOptionsIcon } from 'components/icon/icon'; import { PaperProps } from '@material-ui/core/Paper'; +import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view'; -type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title'; +type CssRules = 'searchBox' | "toolbar" | "toolbarUnderTitle" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ searchBox: { paddingBottom: theme.spacing.unit * 2 }, toolbar: { - paddingTop: theme.spacing.unit * 2 + paddingTop: theme.spacing.unit, + paddingRight: theme.spacing.unit * 2, }, toolbarUnderTitle: { paddingTop: 0 @@ -30,7 +32,7 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ overflow: 'auto' }, root: { - height: '100%' + height: '100%', }, moreOptionsButton: { padding: 0 @@ -39,7 +41,14 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ paddingLeft: theme.spacing.unit * 3, paddingTop: theme.spacing.unit * 3, fontSize: '18px' - } + }, + dataTable: { + height: '100%', + overflow: 'auto', + }, + container: { + height: '100%', + }, }); interface DataExplorerDataProps { @@ -79,40 +88,56 @@ interface DataExplorerActionProps { extractKey?: (item: T) => React.Key; } -type DataExplorerProps = DataExplorerDataProps & DataExplorerActionProps & WithStyles; +type DataExplorerProps = DataExplorerDataProps & + DataExplorerActionProps & WithStyles & MPVPanelProps; export const DataExplorer = withStyles(styles)( class DataExplorerGeneric extends React.Component> { + componentDidMount() { if (this.props.onSetColumns) { this.props.onSetColumns(this.props.columns); } } + render() { const { columns, onContextMenu, onFiltersChange, onSortToggle, working, extractKey, rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch, items, itemsAvailable, onRowClick, onRowDoubleClick, classes, dataTableDefaultView, hideColumnSelector, actions, paperProps, hideSearchInput, - paperKey, fetchMode, currentItemUuid, title + paperKey, fetchMode, currentItemUuid, title, + doHidePanel, doMaximizePanel, panelName, panelMaximized } = this.props; - return - {title &&
{title}
} - {(!hideColumnSelector || !hideSearchInput) && + + const dataCy = this.props["data-cy"]; + return + + {title && {title}} + {(!hideColumnSelector || !hideSearchInput || !!actions) && -
+ {!hideSearchInput &&
{!hideSearchInput && } -
+
} {actions} {!hideColumnSelector && }
-
} - + + } + { doHidePanel && + + + } +
} + onRowClick(item)} @@ -124,8 +149,8 @@ export const DataExplorer = withStyles(styles)( working={working} defaultView={dataTableDefaultView} currentItemUuid={currentItemUuid} - currentRoute={paperKey} /> - + currentRoute={paperKey} /> + {fetchMode === DataTableFetchMode.PAGINATED ? Load more} - + +
; } diff --git a/src/components/data-table/data-table.tsx b/src/components/data-table/data-table.tsx index 79a94599..9a31cbfd 100644 --- a/src/components/data-table/data-table.tsx +++ b/src/components/data-table/data-table.tsx @@ -39,13 +39,11 @@ type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell' | const styles: StyleRulesCallback = (theme: Theme) => ({ root: { - overflowX: 'auto', - overflowY: 'auto', - height: 'calc(100vh - 280px)', + width: '100%', }, content: { display: 'inline-block', - width: '100%' + width: '100%', }, tableBody: { background: theme.palette.background.paper diff --git a/src/components/file-upload/file-upload.tsx b/src/components/file-upload/file-upload.tsx index 617529cd..54d5b5db 100644 --- a/src/components/file-upload/file-upload.tsx +++ b/src/components/file-upload/file-upload.tsx @@ -123,6 +123,17 @@ export const FileUpload = withStyles(styles)( if (!disabled) { onDelete(file); } + + let interval = setInterval(() => { + const key = Object.keys((window as any).cancelTokens).find(key => key.indexOf(file.file.name) > -1); + + if (key) { + clearInterval(interval); + (window as any).cancelTokens[key](); + delete (window as any).cancelTokens[key]; + } + }, 100); + } render() { const { classes, onDrop, disabled, files } = this.props; @@ -140,6 +151,7 @@ export const FileUpload = withStyles(styles)( inputs[0].focus(); } }} + data-cy="drag-and-drop" disabled={disabled} inputProps={{ onFocus: () => { diff --git a/src/components/form-dialog/form-dialog.tsx b/src/components/form-dialog/form-dialog.tsx index 19145cea..0fc799de 100644 --- a/src/components/form-dialog/form-dialog.tsx +++ b/src/components/form-dialog/form-dialog.tsx @@ -42,7 +42,9 @@ interface DialogProjectDataProps { dialogTitle: string; formFields: React.ComponentType & WithDialogProps>; submitLabel?: string; + cancelCallback?: Function; enableWhenPristine?: boolean; + doNotDisableCancel?: boolean; } type DialogProjectProps = DialogProjectDataProps & WithDialogProps<{}> & InjectedFormProps & WithStyles; @@ -65,10 +67,18 @@ export const FormDialog = withStyles(styles)((props: DialogProjectProps) => + + ]; + + const aPanel = + + {children[idx]} + ; + panels = [...panels, aPanel]; + }; + }; + + return + + { toggles.map((tgl, idx) => {tgl}) } + + + { panelVisibility.includes(true) + ? panels + : + + } + + ; +}; + +export const MPVContainer = withStyles(styles)(MPVContainerComponent); \ No newline at end of file diff --git a/src/components/panel-default-view/panel-default-view.tsx b/src/components/panel-default-view/panel-default-view.tsx deleted file mode 100644 index c364bb75..00000000 --- a/src/components/panel-default-view/panel-default-view.tsx +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) The Arvados Authors. All rights reserved. -// -// SPDX-License-Identifier: AGPL-3.0 - -import React from 'react'; -import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles'; -import { DefaultViewDataProps, DefaultView } from 'components/default-view/default-view'; - -type CssRules = 'classRoot' | 'classIcon' | 'classMessage'; - -const styles: StyleRulesCallback = () => ({ - classRoot: { - position: 'absolute', - width: '80%', - left: '50%', - top: '50%', - transform: 'translate(-50%, -50%)' - }, - classMessage: { - fontSize: '1.75rem', - }, - classIcon: { - fontSize: '6rem' - } -}); - -type PanelDefaultViewProps = Pick & WithStyles; - -export const PanelDefaultView = withStyles(styles)( - ({ classes, ...props }: PanelDefaultViewProps) => - ); diff --git a/src/components/refresh-button/refresh-button.tsx b/src/components/refresh-button/refresh-button.tsx index f2c41d28..9971547b 100644 --- a/src/components/refresh-button/refresh-button.tsx +++ b/src/components/refresh-button/refresh-button.tsx @@ -22,13 +22,20 @@ const styles: StyleRulesCallback = theme => ({ }, }); -export const RefreshButton = ({ history, classes }: RouteComponentProps & WithStyles) => +interface RefreshButtonProps { + onClick?: () => void; +} + +export const RefreshButton = ({ history, classes, onClick }: RouteComponentProps & WithStyles & RefreshButtonProps) => ); } diff --git a/src/views/all-processes-panel/all-processes-panel.tsx b/src/views/all-processes-panel/all-processes-panel.tsx index f9fab44d..928b4fff 100644 --- a/src/views/all-processes-panel/all-processes-panel.tsx +++ b/src/views/all-processes-panel/all-processes-panel.tsx @@ -33,7 +33,7 @@ import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from 'st import { getProcess } from 'store/processes/process'; import { ResourcesState } from 'store/resources/resources'; -type CssRules = "toolbar" | "button"; +type CssRules = "toolbar" | "button" | "root"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ toolbar: { @@ -43,6 +43,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ button: { marginLeft: theme.spacing.unit }, + root: { + width: '100%', + } }); export enum AllProcessesPanelColumnNames { @@ -142,18 +145,17 @@ export const AllProcessesPanel = withStyles(styles)( } render() { - return - } />; + dataTableDefaultView={ } /> + } } ) diff --git a/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx index 703bbec5..8f87cb26 100644 --- a/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx +++ b/src/views/api-client-authorization-panel/api-client-authorization-panel-root.tsx @@ -4,10 +4,10 @@ import React from 'react'; import { - StyleRulesCallback, WithStyles, withStyles, Card, CardContent, Grid, Tooltip, IconButton + StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; import { ArvadosTheme } from 'common/custom-theme'; -import { HelpIcon, ShareMeIcon } from 'components/icon/icon'; +import { ShareMeIcon } from 'components/icon/icon'; import { createTree } from 'models/tree'; import { DataColumns } from 'components/data-table/data-table'; import { SortDirection } from 'components/data-table/data-column'; @@ -20,21 +20,11 @@ import { TokenLastUsedAt, TokenLastUsedByIpAddress, TokenScopes, TokenUserId } from 'views-components/data-explorer/renderers'; -type CssRules = 'card' | 'cardContent' | 'helpIconGrid'; +type CssRules = 'root'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ - card: { + root: { width: '100%', - overflow: 'auto' - }, - cardContent: { - padding: 0, - '&:last-child': { - paddingBottom: 0 - } - }, - helpIconGrid: { - textAlign: 'right' } }); @@ -132,7 +122,6 @@ export interface ApiClientAuthorizationPanelRootActionProps { onItemClick: (item: string) => void; onContextMenu: (event: React.MouseEvent, item: string) => void; onItemDoubleClick: (item: string) => void; - openHelpDialog: () => void; } export interface ApiClientAuthorizationPanelRootDataProps { @@ -143,33 +132,18 @@ type ApiClientAuthorizationPanelRootProps = ApiClientAuthorizationPanelRootActio & ApiClientAuthorizationPanelRootDataProps & WithStyles; export const ApiClientAuthorizationPanelRoot = withStyles(styles)( - ({ classes, onItemDoubleClick, onItemClick, onContextMenu, openHelpDialog }: ApiClientAuthorizationPanelRootProps) => - - - - - - - - - - - - - } /> - - - - + ({ classes, onItemDoubleClick, onItemClick, onContextMenu }: ApiClientAuthorizationPanelRootProps) => +
+ } />
); \ No newline at end of file diff --git a/src/views/api-client-authorization-panel/api-client-authorization-panel.tsx b/src/views/api-client-authorization-panel/api-client-authorization-panel.tsx index 89254dcc..9604bf50 100644 --- a/src/views/api-client-authorization-panel/api-client-authorization-panel.tsx +++ b/src/views/api-client-authorization-panel/api-client-authorization-panel.tsx @@ -11,7 +11,6 @@ import { ApiClientAuthorizationPanelRootActionProps } from 'views/api-client-authorization-panel/api-client-authorization-panel-root'; import { openApiClientAuthorizationContextMenu } from 'store/context-menu/context-menu-actions'; -import { openApiClientAuthorizationsHelpDialog } from 'store/api-client-authorizations/api-client-authorizations-actions'; const mapStateToProps = (state: RootState): ApiClientAuthorizationPanelRootDataProps => { return { @@ -25,9 +24,6 @@ const mapDispatchToProps = (dispatch: Dispatch): ApiClientAuthorizationPanelRoot }, onItemClick: (resourceUuid: string) => { return; }, onItemDoubleClick: uuid => { return; }, - openHelpDialog: () => { - dispatch(openApiClientAuthorizationsHelpDialog()); - } }); export const ApiClientAuthorizationPanel = connect(mapStateToProps, mapDispatchToProps)(ApiClientAuthorizationPanelRoot); \ No newline at end of file diff --git a/src/views/collection-content-address-panel/collection-content-address-panel.tsx b/src/views/collection-content-address-panel/collection-content-address-panel.tsx index 88638085..f1278049 100644 --- a/src/views/collection-content-address-panel/collection-content-address-panel.tsx +++ b/src/views/collection-content-address-panel/collection-content-address-panel.tsx @@ -7,7 +7,6 @@ import { StyleRulesCallback, WithStyles, withStyles, - Grid, Button } from '@material-ui/core'; import { CollectionIcon } from 'components/icon/icon'; @@ -38,7 +37,7 @@ import { getResource, ResourcesState } from 'store/resources/resources'; import { RootState } from 'store/store'; import { CollectionResource } from 'models/collection'; -type CssRules = 'backLink' | 'backIcon' | 'card' | 'title' | 'iconHeader' | 'link'; +type CssRules = 'backLink' | 'backIcon' | 'root' | 'content'; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ backLink: { @@ -53,24 +52,13 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ backIcon: { marginRight: theme.spacing.unit }, - card: { - width: '100%' + root: { + width: '100%', }, - title: { - color: theme.palette.grey["700"] + content: { + // reserve space for the content address bar + height: `calc(100% - ${theme.spacing.unit * 7}px)`, }, - iconHeader: { - fontSize: '1.875rem', - color: theme.customs.colors.green700 - }, - link: { - fontSize: '0.875rem', - color: theme.palette.primary.main, - textAlign: 'right', - '&:hover': { - cursor: 'pointer' - } - } }); enum CollectionContentAddressPanelColumnNames { @@ -162,14 +150,14 @@ export const CollectionsContentAddressPanel = withStyles(styles)( connect(mapStateToProps, mapDispatchToProps)( class extends React.Component> { render() { - return + return
- - } />; - ; + } />
+ ; } } ) diff --git a/src/views/collection-panel/collection-panel.tsx b/src/views/collection-panel/collection-panel.tsx index 4270cbbd..794e093f 100644 --- a/src/views/collection-panel/collection-panel.tsx +++ b/src/views/collection-panel/collection-panel.tsx @@ -4,15 +4,20 @@ import React from 'react'; import { - StyleRulesCallback, WithStyles, withStyles, - IconButton, Grid, Tooltip, Typography, ExpansionPanel, - ExpansionPanelSummary, ExpansionPanelDetails + StyleRulesCallback, + WithStyles, + withStyles, + IconButton, + Grid, + Tooltip, + Typography, + Card, CardHeader, CardContent, } from '@material-ui/core'; import { connect, DispatchProp } from "react-redux"; import { RouteComponentProps } from 'react-router'; import { ArvadosTheme } from 'common/custom-theme'; import { RootState } from 'store/store'; -import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon, CollectionOldVersionIcon } from 'components/icon/icon'; +import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, CollectionOldVersionIcon } from 'components/icon/icon'; import { DetailsAttribute } from 'components/details-attribute/details-attribute'; import { CollectionResource, getCollectionUrl } from 'models/collection'; import { CollectionPanelFiles } from 'views-components/collection-panel-files/collection-panel-files'; @@ -33,9 +38,12 @@ import { COLLECTION_PANEL_LOAD_FILES, loadCollectionFiles, COLLECTION_PANEL_LOAD import { Link } from 'react-router-dom'; import { Link as ButtonLink } from '@material-ui/core'; import { ResourceOwnerWithName, ResponsiblePerson } from 'views-components/data-explorer/renderers'; +import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view'; type CssRules = 'root' | 'button' + | 'infoCard' + | 'propertiesCard' | 'filesCard' | 'iconHeader' | 'tag' @@ -49,16 +57,21 @@ type CssRules = 'root' const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { - display: 'flex', - flexFlow: 'column', - height: 'calc(100vh - 130px)', // (100% viewport height) - (top bar + breadcrumbs) + width: '100%', }, button: { cursor: 'pointer' }, + infoCard: { + paddingLeft: theme.spacing.unit * 2, + paddingRight: theme.spacing.unit * 2, + paddingBottom: theme.spacing.unit * 2, + }, + propertiesCard: { + padding: 0, + }, filesCard: { - marginBottom: theme.spacing.unit * 2, - flex: 1, + padding: 0, }, iconHeader: { fontSize: '1.875rem', @@ -120,7 +133,7 @@ export const CollectionPanel = withStyles(styles)( isWritable = true; } else { const itemOwner = getResource(item.ownerUuid)(state.resources); - if (itemOwner) { + if (itemOwner && itemOwner.writableBy) { isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0; } } @@ -133,10 +146,15 @@ export const CollectionPanel = withStyles(styles)( class extends React.Component { render() { const { classes, item, dispatch, isWritable, isOldVersion, isLoadingFiles, tooManyFiles } = this.props; + const panelsData: MPVPanelState[] = [ + {name: "Details"}, + {name: "Properties"}, + {name: "Files"}, + ]; return item - ?
- - }> + ? + + @@ -165,8 +183,6 @@ export const CollectionPanel = withStyles(styles)( - - @@ -185,15 +201,12 @@ export const CollectionPanel = withStyles(styles)( } - - - - - }> - {"Properties"} - - - + + + + + + {isWritable && } @@ -218,21 +231,23 @@ export const CollectionPanel = withStyles(styles)( :
No properties set on this collection.
}
-
-
-
-
- { - dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true)); - dispatch(loadCollectionFiles(this.props.item.uuid)); - } - } /> -
-
+
+ + + + + { + dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true)); + dispatch(loadCollectionFiles(this.props.item.uuid)); + } + } /> + + + : null; } diff --git a/src/views/favorite-panel/favorite-panel.tsx b/src/views/favorite-panel/favorite-panel.tsx index 404baeb9..0b6532c1 100644 --- a/src/views/favorite-panel/favorite-panel.tsx +++ b/src/views/favorite-panel/favorite-panel.tsx @@ -41,7 +41,7 @@ import { getProperty } from 'store/properties/properties'; import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action'; import { CollectionResource } from 'models/collection'; -type CssRules = "toolbar" | "button"; +type CssRules = "toolbar" | "button" | "root"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ toolbar: { @@ -51,6 +51,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ button: { marginLeft: theme.spacing.unit }, + root: { + width: '100%', + }, }); export enum FavoritePanelColumnNames { @@ -176,7 +179,7 @@ export const FavoritePanel = withStyles(styles)( } render() { - return - } />; + } />; } } ) diff --git a/src/views/group-details-panel/group-details-panel.tsx b/src/views/group-details-panel/group-details-panel.tsx index d0f79736..ce3f34c7 100644 --- a/src/views/group-details-panel/group-details-panel.tsx +++ b/src/views/group-details-panel/group-details-panel.tsx @@ -7,67 +7,129 @@ import { connect } from 'react-redux'; import { DataExplorer } from "views-components/data-explorer/data-explorer"; import { DataColumns } from 'components/data-table/data-table'; -import { ResourceUuid, ResourceFirstName, ResourceLastName, ResourceEmail, ResourceUsername } from 'views-components/data-explorer/renderers'; +import { ResourceLinkHeadUuid, ResourceLinkTailUsername, ResourceLinkHeadPermissionLevel, ResourceLinkTailPermissionLevel, ResourceLinkHead, ResourceLinkTail, ResourceLinkDelete, ResourceLinkTailIsActive, ResourceLinkTailIsVisible } from 'views-components/data-explorer/renderers'; import { createTree } from 'models/tree'; import { noop } from 'lodash/fp'; import { RootState } from 'store/store'; -import { GROUP_DETAILS_PANEL_ID, openAddGroupMembersDialog } from 'store/group-details-panel/group-details-panel-actions'; +import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID, openAddGroupMembersDialog, getCurrentGroupDetailsPanelUuid } from 'store/group-details-panel/group-details-panel-actions'; import { openContextMenu } from 'store/context-menu/context-menu-actions'; import { ResourcesState, getResource } from 'store/resources/resources'; -import { ContextMenuKind } from 'views-components/context-menu/context-menu'; -import { PermissionResource } from 'models/permission'; -import { Grid, Button } from '@material-ui/core'; +import { Grid, Button, Tabs, Tab, Paper, WithStyles, withStyles, StyleRulesCallback } from '@material-ui/core'; import { AddIcon } from 'components/icon/icon'; +import { getUserUuid } from 'common/getuser'; +import { GroupResource, isBuiltinGroup } from 'models/group'; +import { ArvadosTheme } from 'common/custom-theme'; -export enum GroupDetailsPanelColumnNames { - FIRST_NAME = "First name", - LAST_NAME = "Last name", - UUID = "UUID", - EMAIL = "Email", +type CssRules = "root"; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + } +}); + +export enum GroupDetailsPanelMembersColumnNames { + FULL_NAME = "Name", USERNAME = "Username", + ACTIVE = "User Active", + VISIBLE = "Visible to other members", + PERMISSION = "Permission", + REMOVE = "Remove", +} + +export enum GroupDetailsPanelPermissionsColumnNames { + NAME = "Name", + PERMISSION = "Permission", + UUID = "UUID", + REMOVE = "Remove", } -export const groupDetailsPanelColumns: DataColumns = [ +export const groupDetailsMembersPanelColumns: DataColumns = [ { - name: GroupDetailsPanelColumnNames.FIRST_NAME, + name: GroupDetailsPanelMembersColumnNames.FULL_NAME, selected: true, configurable: true, filters: createTree(), - render: uuid => + render: uuid => }, { - name: GroupDetailsPanelColumnNames.LAST_NAME, + name: GroupDetailsPanelMembersColumnNames.USERNAME, selected: true, configurable: true, filters: createTree(), - render: uuid => + render: uuid => }, { - name: GroupDetailsPanelColumnNames.UUID, + name: GroupDetailsPanelMembersColumnNames.ACTIVE, selected: true, configurable: true, filters: createTree(), - render: uuid => + render: uuid => }, { - name: GroupDetailsPanelColumnNames.EMAIL, + name: GroupDetailsPanelMembersColumnNames.VISIBLE, selected: true, configurable: true, filters: createTree(), - render: uuid => + render: uuid => }, { - name: GroupDetailsPanelColumnNames.USERNAME, + name: GroupDetailsPanelMembersColumnNames.PERMISSION, selected: true, configurable: true, filters: createTree(), - render: uuid => + render: uuid => + }, + { + name: GroupDetailsPanelMembersColumnNames.REMOVE, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, +]; + +export const groupDetailsPermissionsPanelColumns: DataColumns = [ + { + name: GroupDetailsPanelPermissionsColumnNames.NAME, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, + { + name: GroupDetailsPanelPermissionsColumnNames.PERMISSION, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, + { + name: GroupDetailsPanelPermissionsColumnNames.UUID, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => + }, + { + name: GroupDetailsPanelPermissionsColumnNames.REMOVE, + selected: true, + configurable: true, + filters: createTree(), + render: uuid => }, ]; const mapStateToProps = (state: RootState) => { + const groupUuid = getCurrentGroupDetailsPanelUuid(state.properties); + const group = getResource(groupUuid || '')(state.resources); + const userUuid = getUserUuid(state); + return { - resources: state.resources + resources: state.resources, + groupCanManage: userUuid && !isBuiltinGroup(group?.uuid || '') + ? group?.writableBy?.includes(userUuid) + : false, }; }; @@ -80,47 +142,74 @@ export interface GroupDetailsPanelProps { onContextMenu: (event: React.MouseEvent, item: any) => void; onAddUser: () => void; resources: ResourcesState; + groupCanManage: boolean; } -export const GroupDetailsPanel = connect( +export const GroupDetailsPanel = withStyles(styles)(connect( mapStateToProps, mapDispatchToProps )( - class GroupDetailsPanel extends React.Component { + class GroupDetailsPanel extends React.Component> { + state = { + value: 0, + }; + + componentDidMount() { + this.setState({ value: 0 }); + } render() { + const { value } = this.state; return ( - - - - } /> + + + + + + {value === 0 && + + + + } + paperProps={{ + elevation: 0, + }} /> + } + {value === 1 && + + } + ); } - handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { - const resource = getResource(resourceUuid)(this.props.resources); - if (resource) { - this.props.onContextMenu(event, { - name: '', - uuid: resource.uuid, - ownerUuid: resource.ownerUuid, - kind: resource.kind, - menuKind: ContextMenuKind.GROUP_MEMBER - }); - } + handleChange = (event: React.MouseEvent, value: number) => { + this.setState({ value }); } - }); - + })); diff --git a/src/views/groups-panel/groups-panel.tsx b/src/views/groups-panel/groups-panel.tsx index 4d15118c..3251c729 100644 --- a/src/views/groups-panel/groups-panel.tsx +++ b/src/views/groups-panel/groups-panel.tsx @@ -4,11 +4,11 @@ import React from 'react'; import { connect } from 'react-redux'; -import { Grid, Button, Typography } from "@material-ui/core"; +import { Grid, Button, Typography, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core"; import { DataExplorer } from "views-components/data-explorer/data-explorer"; import { DataColumns } from 'components/data-table/data-table'; import { SortDirection } from 'components/data-table/data-column'; -import { ResourceOwner } from 'views-components/data-explorer/renderers'; +import { ResourceUuid } from 'views-components/data-explorer/renderers'; import { AddIcon } from 'components/icon/icon'; import { ResourceName } from 'views-components/data-explorer/renderers'; import { createTree } from 'models/tree'; @@ -21,11 +21,19 @@ import { RootState } from 'store/store'; import { openContextMenu } from 'store/context-menu/context-menu-actions'; import { ResourceKind } from 'models/resource'; import { LinkClass, LinkResource } from 'models/link'; -import { navigateToGroupDetails } from 'store/navigation/navigation-action'; +import { ArvadosTheme } from 'common/custom-theme'; + +type CssRules = "root"; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + } +}); export enum GroupsPanelColumnNames { GROUP = "Name", - OWNER = "Owner", + UUID = "UUID", MEMBERS = "Members", } @@ -39,11 +47,11 @@ export const groupsPanelColumns: DataColumns = [ render: uuid => }, { - name: GroupsPanelColumnNames.OWNER, + name: GroupsPanelColumnNames.UUID, selected: true, configurable: true, filters: createTree(), - render: uuid => , + render: uuid => , }, { name: GroupsPanelColumnNames.MEMBERS, @@ -62,42 +70,41 @@ const mapStateToProps = (state: RootState) => { const mapDispatchToProps = { onContextMenu: openContextMenu, - onRowDoubleClick: (uuid: string) => - navigateToGroupDetails(uuid), onNewGroup: openCreateGroupDialog, }; export interface GroupsPanelProps { onNewGroup: () => void; onContextMenu: (event: React.MouseEvent, item: any) => void; - onRowDoubleClick: (item: string) => void; resources: ResourcesState; } -export const GroupsPanel = connect( +export const GroupsPanel = withStyles(styles)(connect( mapStateToProps, mapDispatchToProps )( - class GroupsPanel extends React.Component { + class GroupsPanel extends React.Component> { render() { return ( - - } /> + } /> ); } @@ -105,15 +112,16 @@ export const GroupsPanel = connect( const resource = getResource(resourceUuid)(this.props.resources); if (resource) { this.props.onContextMenu(event, { - name: '', + name: resource.name, uuid: resource.uuid, + description: resource.description, ownerUuid: resource.ownerUuid, kind: resource.kind, menuKind: ContextMenuKind.GROUPS }); } } - }); + })); const GroupMembersCount = connect( @@ -122,7 +130,7 @@ const GroupMembersCount = connect( const permissions = filterResources((resource: LinkResource) => resource.kind === ResourceKind.LINK && resource.linkClass === LinkClass.PERMISSION && - resource.tailUuid === props.uuid + resource.headUuid === props.uuid )(state.resources); return { @@ -130,4 +138,4 @@ const GroupMembersCount = connect( }; } -)(Typography); +)((props: {children: number}) => ()); diff --git a/src/views/link-panel/link-panel-root.tsx b/src/views/link-panel/link-panel-root.tsx index 7a5f0503..b32208cd 100644 --- a/src/views/link-panel/link-panel-root.tsx +++ b/src/views/link-panel/link-panel-root.tsx @@ -11,10 +11,20 @@ import { DataTableDefaultView } from 'components/data-table-default-view/data-ta import { ResourcesState } from 'store/resources/resources'; import { ShareMeIcon } from 'components/icon/icon'; import { createTree } from 'models/tree'; -import { - ResourceLinkUuid, ResourceLinkHead, ResourceLinkTail, - ResourceLinkClass, ResourceLinkName } +import { + ResourceLinkUuid, ResourceLinkHead, ResourceLinkTail, + ResourceLinkClass, ResourceLinkName } from 'views-components/data-explorer/renderers'; +import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; +import { ArvadosTheme } from 'common/custom-theme'; + +type CssRules = "root"; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + } +}); export enum LinkPanelColumnNames { NAME = "Name", @@ -73,20 +83,20 @@ export interface LinkPanelRootActionProps { onItemDoubleClick: (item: string) => void; } -export type LinkPanelRootProps = LinkPanelRootDataProps & LinkPanelRootActionProps; +export type LinkPanelRootProps = LinkPanelRootDataProps & LinkPanelRootActionProps & WithStyles; -export const LinkPanelRoot = (props: LinkPanelRootProps) => { - return { + return
- }/>; -}; \ No newline at end of file + }/>
; +}); \ No newline at end of file diff --git a/src/views/process-panel/process-details-attributes.tsx b/src/views/process-panel/process-details-attributes.tsx new file mode 100644 index 00000000..4f26a71f --- /dev/null +++ b/src/views/process-panel/process-details-attributes.tsx @@ -0,0 +1,65 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from "react"; +import { Grid } from "@material-ui/core"; +import { formatDate } from "common/formatters"; +import { resourceLabel } from "common/labels"; +import { DetailsAttribute } from "components/details-attribute/details-attribute"; +import { ProcessResource } from "models/process"; +import { ResourceKind } from "models/resource"; +import { ResourceOwnerWithName } from "views-components/data-explorer/renderers"; + +type CssRules = 'label' | 'value'; + +export const ProcessDetailsAttributes = (props: { item: ProcessResource, twoCol?: boolean, classes?: Record }) => { + const item = props.item; + const classes = props.classes || { label: '', value: '', button: '' }; + const mdSize = props.twoCol ? 6 : 12; + return + + + + + } /> + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ; +}; diff --git a/src/views/process-panel/process-details-card.tsx b/src/views/process-panel/process-details-card.tsx new file mode 100644 index 00000000..18610781 --- /dev/null +++ b/src/views/process-panel/process-details-card.tsx @@ -0,0 +1,63 @@ +// Copyright (C) The Arvados Authors. All rights reserved. +// +// SPDX-License-Identifier: AGPL-3.0 + +import React from 'react'; +import { + StyleRulesCallback, + WithStyles, + withStyles, + Card, + CardHeader, + IconButton, + CardContent, + Tooltip, +} from '@material-ui/core'; +import { ArvadosTheme } from 'common/custom-theme'; +import { CloseIcon } from 'components/icon/icon'; +import { Process } from 'store/processes/process'; +import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view'; +import { ProcessDetailsAttributes } from './process-details-attributes'; + +type CssRules = 'card' | 'content' | 'title'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + card: { + height: '100%' + }, + content: { + '&:last-child': { + paddingBottom: theme.spacing.unit * 2, + } + }, + title: { + overflow: 'hidden', + paddingTop: theme.spacing.unit * 0.5 + }, +}); + +export interface ProcessDetailsCardDataProps { + process: Process; +} + +type ProcessDetailsCardProps = ProcessDetailsCardDataProps & WithStyles & MPVPanelProps; + +export const ProcessDetailsCard = withStyles(styles)( + ({ classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => { + return + + + } /> + + + + ; + } +); + diff --git a/src/views/process-panel/process-information-card.tsx b/src/views/process-panel/process-information-card.tsx index e70a0478..4c938017 100644 --- a/src/views/process-panel/process-information-card.tsx +++ b/src/views/process-panel/process-information-card.tsx @@ -8,13 +8,14 @@ import { CardHeader, IconButton, CardContent, Grid, Chip, Typography, Tooltip } from '@material-ui/core'; import { ArvadosTheme } from 'common/custom-theme'; -import { MoreOptionsIcon, ProcessIcon } from 'components/icon/icon'; +import { CloseIcon, MoreOptionsIcon, ProcessIcon } from 'components/icon/icon'; import { DetailsAttribute } from 'components/details-attribute/details-attribute'; import { Process } from 'store/processes/process'; import { getProcessStatus, getProcessStatusColor } from 'store/processes/process'; import { formatDate } from 'common/formatters'; import classNames from 'classnames'; import { ContainerState } from 'models/container'; +import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view'; type CssRules = 'card' | 'iconHeader' | 'label' | 'value' | 'chip' | 'link' | 'content' | 'title' | 'avatar' | 'cancelButton'; @@ -83,10 +84,10 @@ export interface ProcessInformationCardDataProps { cancelProcess: (uuid: string) => void; } -type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles; +type ProcessInformationCardProps = ProcessInformationCardDataProps & WithStyles & MPVPanelProps; export const ProcessInformationCard = withStyles(styles, { withTheme: true })( - ({ classes, process, onContextMenu, theme, openProcessInputDialog, navigateToOutput, openWorkflow, cancelProcess }: ProcessInformationCardProps) => { + ({ classes, process, onContextMenu, theme, openProcessInputDialog, navigateToOutput, openWorkflow, cancelProcess, doHidePanel, panelName }: ProcessInformationCardProps) => { const { container } = process; const startedAt = container ? formatDate(container.startedAt) : 'N/A'; const finishedAt = container ? formatDate(container.finishedAt) : 'N/A'; @@ -111,6 +112,10 @@ export const ProcessInformationCard = withStyles(styles, { withTheme: true })( + { doHidePanel && + + + } } title={ diff --git a/src/views/process-panel/process-panel-root.tsx b/src/views/process-panel/process-panel-root.tsx index e7f66573..deb5f1b0 100644 --- a/src/views/process-panel/process-panel-root.tsx +++ b/src/views/process-panel/process-panel-root.tsx @@ -3,13 +3,24 @@ // SPDX-License-Identifier: AGPL-3.0 import React from 'react'; -import { Grid } from '@material-ui/core'; +import { Grid, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core'; import { ProcessInformationCard } from './process-information-card'; import { DefaultView } from 'components/default-view/default-view'; import { ProcessIcon } from 'components/icon/icon'; import { Process } from 'store/processes/process'; import { SubprocessPanel } from 'views/subprocess-panel/subprocess-panel'; import { SubprocessFilterDataProps } from 'components/subprocess-filter/subprocess-filter'; +import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view'; +import { ArvadosTheme } from 'common/custom-theme'; +import { ProcessDetailsCard } from './process-details-card'; + +type CssRules = 'root'; + +const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ + root: { + width: '100%', + }, +}); export interface ProcessPanelRootDataProps { process?: Process; @@ -26,12 +37,18 @@ export interface ProcessPanelRootActionProps { cancelProcess: (uuid: string) => void; } -export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps; +export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles; + +const panelsData: MPVPanelState[] = [ + {name: "Info"}, + {name: "Details", visible: false}, + {name: "Subprocesses"}, +]; -export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) => +export const ProcessPanelRoot = withStyles(styles)(({ process, ...props }: ProcessPanelRootProps) => process - ? - + ? + props.onContextMenu(event, process)} @@ -40,11 +57,14 @@ export const ProcessPanelRoot = ({ process, ...props }: ProcessPanelRootProps) = openWorkflow={props.navigateToWorkflow} cancelProcess={props.cancelProcess} /> - - + + + + + - - + + : - ; + ); diff --git a/src/views/project-panel/project-panel.tsx b/src/views/project-panel/project-panel.tsx index 80663330..ab11593d 100644 --- a/src/views/project-panel/project-panel.tsx +++ b/src/views/project-panel/project-panel.tsx @@ -51,9 +51,7 @@ type CssRules = 'root' | "button"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ root: { - position: 'relative', width: '100%', - height: '100%' }, button: { marginLeft: theme.spacing.unit @@ -161,7 +159,7 @@ export const ProjectPanel = withStyles(styles)( data = dataExplorerItems; } - return
+ return
= (theme: ArvadosTheme) => ({ toolbar: { @@ -49,6 +49,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ button: { marginLeft: theme.spacing.unit }, + root: { + width: '100%', + }, }); export enum PublicFavoritePanelColumnNames { @@ -160,7 +163,7 @@ export const PublicFavoritePanel = withStyles(styles)( connect(mapStateToProps, mapDispatchToProps)( class extends React.Component { render() { - return - } />; + } />
; } } ) diff --git a/src/views/run-process-panel/inputs/float-array-input.tsx b/src/views/run-process-panel/inputs/float-array-input.tsx index 780cbc90..3f0a5334 100644 --- a/src/views/run-process-panel/inputs/float-array-input.tsx +++ b/src/views/run-process-panel/inputs/float-array-input.tsx @@ -30,7 +30,7 @@ const validationSelector = createSelector( ); const required = (value: string[]) => - value.length > 0 + value && value.length > 0 ? undefined : ERROR_MESSAGE; diff --git a/src/views/run-process-panel/inputs/int-array-input.tsx b/src/views/run-process-panel/inputs/int-array-input.tsx index 03cb07ea..8077f28a 100644 --- a/src/views/run-process-panel/inputs/int-array-input.tsx +++ b/src/views/run-process-panel/inputs/int-array-input.tsx @@ -30,7 +30,7 @@ const validationSelector = createSelector( ); const required = (value: string[]) => - value.length > 0 + value && value.length > 0 ? undefined : ERROR_MESSAGE; diff --git a/src/views/run-process-panel/inputs/string-array-input.tsx b/src/views/run-process-panel/inputs/string-array-input.tsx index cabbf749..8955009a 100644 --- a/src/views/run-process-panel/inputs/string-array-input.tsx +++ b/src/views/run-process-panel/inputs/string-array-input.tsx @@ -31,7 +31,7 @@ const validationSelector = createSelector( ); const required = (value: string[] = []) => - value.length > 0 + value && value.length > 0 ? undefined : ERROR_MESSAGE; diff --git a/src/views/run-process-panel/run-process-first-step.tsx b/src/views/run-process-panel/run-process-first-step.tsx index 906d3a37..ed6d5640 100644 --- a/src/views/run-process-panel/run-process-first-step.tsx +++ b/src/views/run-process-panel/run-process-first-step.tsx @@ -59,7 +59,7 @@ export const RunProcessFirstStep = withStyles(styles)( - + diff --git a/src/views/shared-with-me-panel/shared-with-me-panel.tsx b/src/views/shared-with-me-panel/shared-with-me-panel.tsx index eb3127a7..219410c5 100644 --- a/src/views/shared-with-me-panel/shared-with-me-panel.tsx +++ b/src/views/shared-with-me-panel/shared-with-me-panel.tsx @@ -20,7 +20,7 @@ import { } from 'store/context-menu/context-menu-actions'; import { GroupContentsResource } from 'services/groups-service/groups-service'; -type CssRules = "toolbar" | "button"; +type CssRules = "toolbar" | "button" | "root"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ toolbar: { @@ -30,6 +30,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ button: { marginLeft: theme.spacing.unit }, + root: { + width: '100%', + }, }); interface SharedWithMePanelDataProps { @@ -46,13 +49,13 @@ export const SharedWithMePanel = withStyles(styles)( }))( class extends React.Component { render() { - return } />; + dataTableDefaultView={} />
; } handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { diff --git a/src/views/subprocess-panel/subprocess-panel-root.tsx b/src/views/subprocess-panel/subprocess-panel-root.tsx index b8e1b081..41a8f66b 100644 --- a/src/views/subprocess-panel/subprocess-panel-root.tsx +++ b/src/views/subprocess-panel/subprocess-panel-root.tsx @@ -17,6 +17,7 @@ import { DataTableDefaultView } from 'components/data-table-default-view/data-ta import { createTree } from 'models/tree'; import { getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters'; import { ResourcesState } from 'store/resources/resources'; +import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view'; export enum SubprocessPanelColumnNames { NAME = "Name", @@ -80,7 +81,7 @@ const DEFAULT_VIEW_MESSAGES = [ 'The current process may not have any or none matches current filtering.' ]; -export const SubprocessPanelRoot = (props: SubprocessPanelProps) => { +export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps) => { return { - } />; + } + doHidePanel={props.doHidePanel} + doMaximizePanel={props.doMaximizePanel} + panelMaximized={props.panelMaximized} + panelName={props.panelName} />; }; \ No newline at end of file diff --git a/src/views/trash-panel/trash-panel.tsx b/src/views/trash-panel/trash-panel.tsx index b67b666c..d303c2f7 100644 --- a/src/views/trash-panel/trash-panel.tsx +++ b/src/views/trash-panel/trash-panel.tsx @@ -36,7 +36,7 @@ import { getTrashPanelTypeFilters } from 'store/resource-type-filters/resource-type-filters'; -type CssRules = "toolbar" | "button"; +type CssRules = "toolbar" | "button" | "root"; const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ toolbar: { @@ -46,6 +46,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ button: { marginLeft: theme.spacing.unit }, + root: { + width: '100%', + }, }); export enum TrashPanelColumnNames { @@ -146,7 +149,7 @@ export const TrashPanel = withStyles(styles)( }))( class extends React.Component { render() { - return - } />; + } />; } handleContextMenu = (event: React.MouseEvent, resourceUuid: string) => { diff --git a/src/views/user-panel/user-panel.tsx b/src/views/user-panel/user-panel.tsx index c86ca519..5fb979a2 100644 --- a/src/views/user-panel/user-panel.tsx +++ b/src/views/user-panel/user-panel.tsx @@ -30,7 +30,7 @@ import { ShareMeIcon, AddIcon } from 'components/icon/icon'; import { USERS_PANEL_ID, openUserCreateDialog } from 'store/users/users-actions'; import { noop } from 'lodash'; -type UserPanelRules = "button"; +type UserPanelRules = "button" | 'root' | 'content'; const styles = withStyles(theme => ({ button: { @@ -39,6 +39,13 @@ const styles = withStyles(theme => ({ textAlign: 'right', alignSelf: 'center' }, + root: { + width: '100%', + }, + content: { + // reserve space for the tab bar + height: `calc(100% - ${theme.spacing.unit * 7}px)`, + } })); export enum UserPanelColumnNames { @@ -149,13 +156,13 @@ export const UserPanel = compose( render() { const { value } = this.state; - return + return {value === 0 && - +
} /> - } +
}
; } diff --git a/src/views/workbench/workbench.tsx b/src/views/workbench/workbench.tsx index 9ce93bf2..25d70776 100644 --- a/src/views/workbench/workbench.tsx +++ b/src/views/workbench/workbench.tsx @@ -82,13 +82,11 @@ import { HelpApiClientAuthorizationDialog } from 'views-components/api-client-au import { UserManageDialog } from 'views-components/user-dialog/manage-dialog'; import { SetupShellAccountDialog } from 'views-components/dialog-forms/setup-shell-account-dialog'; import { GroupsPanel } from 'views/groups-panel/groups-panel'; -import { CreateGroupDialog } from 'views-components/dialog-forms/create-group-dialog'; import { RemoveGroupDialog } from 'views-components/groups-dialog/remove-dialog'; import { GroupAttributesDialog } from 'views-components/groups-dialog/attributes-dialog'; import { GroupDetailsPanel } from 'views/group-details-panel/group-details-panel'; import { RemoveGroupMemberDialog } from 'views-components/groups-dialog/member-remove-dialog'; import { GroupMemberAttributesDialog } from 'views-components/groups-dialog/member-attributes-dialog'; -import { AddGroupMembersDialog } from 'views-components/dialog-forms/add-group-member-dialog'; import { PartialCopyToCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-collection-dialog'; import { PublicFavoritePanel } from 'views/public-favorites-panel/public-favorites-panel'; import { LinkAccountPanel } from 'views/link-account-panel/link-account-panel'; @@ -129,6 +127,9 @@ const styles: StyleRulesCallback = (theme: ArvadosTheme) => ({ minWidth: 0, paddingLeft: theme.spacing.unit * 3, paddingRight: theme.spacing.unit * 3, + // Reserve vertical space for app bar + MainContentBar + minHeight: `calc(100vh - ${theme.spacing.unit * 16}px)`, + display: 'flex', } }); @@ -212,7 +213,6 @@ export const WorkbenchPanel = - @@ -223,7 +223,6 @@ export const WorkbenchPanel = - diff --git a/tools/arvados_config.yml b/tools/arvados_config.yml index 369046e6..55dc8a02 100644 --- a/tools/arvados_config.yml +++ b/tools/arvados_config.yml @@ -4,6 +4,7 @@ Clusters: SystemRootToken: systemusertesttoken1234567890aoeuidhtnsqjkxbmwvzpy API: RequestTimeout: 30s + VocabularyPath: "" TLS: Insecure: true Collections: diff --git a/public/vocabulary-example.json b/tools/example-vocabulary.json similarity index 100% rename from public/vocabulary-example.json rename to tools/example-vocabulary.json diff --git a/tools/run-integration-tests.sh b/tools/run-integration-tests.sh index 159bfc1c..bf4c3ba4 100755 --- a/tools/run-integration-tests.sh +++ b/tools/run-integration-tests.sh @@ -70,6 +70,7 @@ echo "ARVADOS_DIR is ${ARVADOS_DIR}" ARVADOS_LOG=${ARVADOS_DIR}/arvados.log ARVADOS_CONF=${WB2_DIR}/tools/arvados_config.yml +VOCABULARY_CONF=${WB2_DIR}/tools/example-vocabulary.json if [ ! -f "${WB2_DIR}/src/index.tsx" ]; then echo "ERROR: '${WB2_DIR}' isn't workbench2's directory" @@ -104,6 +105,9 @@ echo "Installing dev dependencies..." ~/go/bin/arvados-server install -type test || exit 1 echo "Launching arvados in test mode..." +VOC_DIR=$(mktemp -d | cut -d \/ -f3) # Removes the /tmp/ part +cp ${VOCABULARY_CONF} /tmp/${VOC_DIR}/voc.json +sed -i "s/VocabularyPath: \".*\"/VocabularyPath: \"\/tmp\/${VOC_DIR}\/voc.json\"/" ${ARVADOS_CONF} coproc arvboot (~/go/bin/arvados-server boot \ -type test \ -config ${ARVADOS_CONF} \