Merge branch 'main' into 15768-multi-select-operations Arvados-DCO-1.1-Signed-off...
authorLisa Knox <lisaknox83@gmail.com>
Tue, 10 Oct 2023 15:46:28 +0000 (11:46 -0400)
committerLisa Knox <lisaknox83@gmail.com>
Tue, 10 Oct 2023 15:46:28 +0000 (11:46 -0400)
22 files changed:
1  2 
cypress/integration/collection.spec.js
cypress/integration/process.spec.js
cypress/integration/project.spec.js
cypress/integration/search.spec.js
cypress/integration/virtual-machine-admin.spec.js
cypress/support/commands.js
src/components/collection-panel-files/collection-panel-files.tsx
src/components/data-explorer/data-explorer.tsx
src/index.tsx
src/services/collection-service/collection-service.ts
src/store/context-menu/context-menu-actions.ts
src/store/processes/processes-actions.ts
src/store/store.ts
src/store/workbench/workbench-actions.ts
src/views-components/context-menu/action-sets/collection-files-item-action-set.ts
src/views-components/context-menu/action-sets/workflow-action-set.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/data-explorer/renderers.tsx
src/views/process-panel/process-io-card.tsx
src/views/process-panel/process-panel-root.tsx
src/views/process-panel/process-panel.tsx
src/views/workbench/workbench.tsx

index 8145ec3ffb893eac7fe3ec9258a552ed2152bbbb,69e484173f80aa8fdcda1f4a95c46d572b41ce1d..35f36e1e0b90d0d21c9684603a383e04d850a704
@@@ -362,96 -368,119 +362,152 @@@ describe("Collection panel tests", func
                  cy.goToPath(`/collections/${this.testCollection.uuid}`);
  
                  const names = [
 -                    'bar', // initial name already set
 -                    '&',
 -                    'foo',
 -                    '&amp;',
 -                    'I ❤️ ⛵️',
 -                    '...',
 -                    '#..',
 -                    'some name with whitespaces',
 -                    'some name with #2',
 -                    'is this name legal? I hope it is',
 -                    'some_file.pdf#',
 -                    'some_file.pdf?',
 -                    '?some_file.pdf',
 -                    'some%file.pdf',
 -                    'some%2Ffile.pdf',
 -                    'some%22file.pdf',
 -                    'some%20file.pdf',
 +                    "bar", // initial name already set
 +                    "&",
 +                    "foo",
 +                    "&amp;",
 +                    "I ❤️ ⛵️",
 +                    "...",
 +                    "#..",
 +                    "some name with whitespaces",
 +                    "some name with #2",
 +                    "is this name legal? I hope it is",
 +                    "some_file.pdf#",
 +                    "some_file.pdf?",
 +                    "?some_file.pdf",
 +                    "some%file.pdf",
 +                    "some%2Ffile.pdf",
 +                    "some%22file.pdf",
 +                    "some%20file.pdf",
                      "G%C3%BCnter's%20file.pdf",
 -                    'table%&?*2',
 -                    'bar' // make sure we can go back to the original name as a last step
 +                    "table%&?*2",
 +                    "bar", // make sure we can go back to the original name as a last step
                  ];
 -                cy.intercept({method: 'PUT', url: '**/arvados/v1/collections/*'}).as('renameRequest');
++                cy.intercept({ method: "PUT", url: "**/arvados/v1/collections/*" }).as("renameRequest");
                  eachPair(names, (from, to) => {
 -                    cy.waitForDom().get('[data-cy=collection-files-panel]')
 -                        .contains(`${from}`).rightclick();
 -                    cy.get('[data-cy=context-menu]')
 -                        .contains('Rename')
 -                        .click();
 -                    cy.get('[data-cy=form-dialog]')
 -                        .should('contain', 'Rename')
 +                    cy.waitForDom().get("[data-cy=collection-files-panel]").contains(`${from}`).rightclick();
 +                    cy.get("[data-cy=context-menu]").contains("Rename").click();
 +                    cy.get("[data-cy=form-dialog]")
 +                        .should("contain", "Rename")
 +                        .within(() => {
 +                            cy.get("input").type("{selectall}{backspace}").type(to, { parseSpecialCharSequences: false });
 +                        });
 +                    cy.get("[data-cy=form-submit-btn]").click();
++                    cy.wait("@renameRequest");
 +                    cy.get("[data-cy=collection-files-panel]").should("not.contain", `${from}`).and("contain", `${to}`);
 +                });
 +            });
 +    });
 +
 +    it("renames a file to a different directory", function () {
 +        // Creates the collection using the admin token so we can set up
 +        // a bogus manifest text without block signatures.
 +        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("testCollection")
 +            .then(function () {
 +                cy.loginAs(activeUser);
 +                cy.goToPath(`/collections/${this.testCollection.uuid}`);
 +
 +                ["subdir", "G%C3%BCnter's%20file", "table%&?*2"].forEach(subdir => {
 +                    cy.waitForDom().get("[data-cy=collection-files-panel]").contains("bar").rightclick();
 +                    cy.get("[data-cy=context-menu]").contains("Rename").click();
 +                    cy.get("[data-cy=form-dialog]")
 +                        .should("contain", "Rename")
 +                        .within(() => {
 +                            cy.get("input").type(`{selectall}{backspace}${subdir}/foo`);
 +                        });
 +                    cy.get("[data-cy=form-submit-btn]").click();
 +                    cy.get("[data-cy=collection-files-panel]").should("not.contain", "bar").and("contain", subdir);
 +                    cy.get("[data-cy=collection-files-panel]").contains(subdir).click();
 +
 +                    // Rename 'subdir/foo' to 'bar'
 +                    cy.wait(1000);
 +                    cy.get("[data-cy=collection-files-panel]").contains("foo").rightclick();
 +                    cy.get("[data-cy=context-menu]").contains("Rename").click();
 +                    cy.get("[data-cy=form-dialog]")
 +                        .should("contain", "Rename")
                          .within(() => {
 -                            cy.get('input')
 -                                .type('{selectall}{backspace}')
 -                                .type(to, { parseSpecialCharSequences: false });
 +                            cy.get("input").should("have.value", `${subdir}/foo`).type(`{selectall}{backspace}bar`);
                          });
 -                    cy.get('[data-cy=form-submit-btn]').click();
 -                    cy.wait('@renameRequest');
 -                    cy.get('[data-cy=collection-files-panel]')
 -                        .should('not.contain', `${from}`)
 -                        .and('contain', `${to}`);
 -                })
 +                    cy.get("[data-cy=form-submit-btn]").click();
 +
 +                    // need to wait for dialog to dismiss
 +                    cy.get("[data-cy=form-dialog]").should("not.exist");
 +
 +                    cy.waitForDom().get("[data-cy=collection-files-panel]").contains("Home").click();
 +
 +                    cy.wait(2000);
 +                    cy.get("[data-cy=collection-files-panel]")
 +                        .should("contain", subdir) // empty dir kept
 +                        .and("contain", "bar");
 +
 +                    cy.get("[data-cy=collection-files-panel]").contains(subdir).rightclick();
 +                    cy.get("[data-cy=context-menu]").contains("Remove").click();
 +                    cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
 +                    cy.get("[data-cy=form-dialog]").should("not.exist");
 +                });
              });
      });
  
 -    it('renames a file to a different directory', function () {
++    it("renames a file to a different directory", function () {
+         // Creates the collection using the admin token so we can set up
+         // a bogus manifest text without block signatures.
+         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"
++            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+         })
 -            .as('testCollection').then(function () {
++            .as("testCollection")
++            .then(function () {
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
 -                ['subdir', 'G%C3%BCnter\'s%20file', 'table%&?*2'].forEach((subdir) => {
 -                    cy.waitForDom().get('[data-cy=collection-files-panel]')
 -                        .contains('bar').rightclick();
 -                    cy.get('[data-cy=context-menu]')
 -                        .contains('Rename')
 -                        .click();
 -                    cy.get('[data-cy=form-dialog]')
 -                        .should('contain', 'Rename')
++                ["subdir", "G%C3%BCnter's%20file", "table%&?*2"].forEach(subdir => {
++                    cy.waitForDom().get("[data-cy=collection-files-panel]").contains("bar").rightclick();
++                    cy.get("[data-cy=context-menu]").contains("Rename").click();
++                    cy.get("[data-cy=form-dialog]")
++                        .should("contain", "Rename")
+                         .within(() => {
 -                            cy.get('input').type(`{selectall}{backspace}${subdir}/foo`);
++                            cy.get("input").type(`{selectall}{backspace}${subdir}/foo`);
+                         });
 -                    cy.get('[data-cy=form-submit-btn]').click();
 -                    cy.get('[data-cy=collection-files-panel]')
 -                        .should('not.contain', 'bar')
 -                        .and('contain', subdir);
 -                    cy.get('[data-cy=collection-files-panel]').contains(subdir).click();
++                    cy.get("[data-cy=form-submit-btn]").click();
++                    cy.get("[data-cy=collection-files-panel]").should("not.contain", "bar").and("contain", subdir);
++                    cy.get("[data-cy=collection-files-panel]").contains(subdir).click();
+                     // Rename 'subdir/foo' to 'bar'
+                     cy.wait(1000);
 -                    cy.get('[data-cy=collection-files-panel]')
 -                        .contains('foo').rightclick();
 -                    cy.get('[data-cy=context-menu]')
 -                        .contains('Rename')
 -                        .click();
 -                    cy.get('[data-cy=form-dialog]')
 -                        .should('contain', 'Rename')
++                    cy.get("[data-cy=collection-files-panel]").contains("foo").rightclick();
++                    cy.get("[data-cy=context-menu]").contains("Rename").click();
++                    cy.get("[data-cy=form-dialog]")
++                        .should("contain", "Rename")
+                         .within(() => {
 -                            cy.get('input')
 -                                .should('have.value', `${subdir}/foo`)
 -                                .type(`{selectall}{backspace}bar`);
++                            cy.get("input").should("have.value", `${subdir}/foo`).type(`{selectall}{backspace}bar`);
+                         });
 -                    cy.get('[data-cy=form-submit-btn]').click();
++                    cy.get("[data-cy=form-submit-btn]").click();
+                     // need to wait for dialog to dismiss
 -                    cy.get('[data-cy=form-dialog]').should('not.exist');
++                    cy.get("[data-cy=form-dialog]").should("not.exist");
 -                    cy.waitForDom().get('[data-cy=collection-files-panel]')
 -                        .contains('Home')
 -                        .click();
++                    cy.waitForDom().get("[data-cy=collection-files-panel]").contains("Home").click();
+                     cy.wait(2000);
 -                    cy.get('[data-cy=collection-files-panel]')
 -                        .should('contain', subdir) // empty dir kept
 -                        .and('contain', 'bar');
 -
 -                    cy.get('[data-cy=collection-files-panel]')
 -                        .contains(subdir).rightclick();
 -                    cy.get('[data-cy=context-menu]')
 -                        .contains('Remove')
 -                        .click();
 -                    cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
 -                    cy.get('[data-cy=form-dialog]').should('not.exist');
++                    cy.get("[data-cy=collection-files-panel]")
++                        .should("contain", subdir) // empty dir kept
++                        .and("contain", "bar");
++
++                    cy.get("[data-cy=collection-files-panel]").contains(subdir).rightclick();
++                    cy.get("[data-cy=context-menu]").contains("Remove").click();
++                    cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
++                    cy.get("[data-cy=form-dialog]").should("not.exist");
+                 });
+             });
+     });
 -    it('shows collection owner', () => {
 +    it("shows collection owner", () => {
          cy.createCollection(adminUser.token, {
              name: `Test collection ${Math.floor(Math.random() * 999999)}`,
              owner_uuid: activeUser.user.uuid,
              name: colName,
              owner_uuid: activeUser.user.uuid,
              preserve_version: true,
 -            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
 +            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
          })
 -            .as('collection').then(function () {
 +            .as("collection")
 +            .then(function () {
                  // Visit collection, check basic information
 -                cy.loginAs(activeUser)
 +                cy.loginAs(activeUser);
                  cy.goToPath(`/collections/${this.collection.uuid}`);
  
 -                cy.get('[data-cy=collection-info-panel]').should('not.contain', 'This is an old version');
 -                cy.get('[data-cy=read-only-icon]').should('not.exist');
 -                cy.get('[data-cy=collection-version-number]').should('contain', '1');
 -                cy.get('[data-cy=collection-info-panel]').should('contain', colName);
 -                cy.get('[data-cy=collection-files-panel]').should('contain', 'foo').and('contain', 'bar');
 +                cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
 +                cy.get("[data-cy=read-only-icon]").should("not.exist");
 +                cy.get("[data-cy=collection-version-number]").should("contain", "1");
 +                cy.get("[data-cy=collection-info-panel]").should("contain", colName);
 +                cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
  
                  // Modify collection, expect version number change
 -                cy.get('[data-cy=collection-files-panel]').contains('foo').rightclick();
 -                cy.get('[data-cy=context-menu]').contains('Remove').click();
 -                cy.get('[data-cy=confirmation-dialog]').should('contain', 'Removing file');
 -                cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
 -                cy.get('[data-cy=collection-version-number]').should('contain', '2');
 -                cy.get('[data-cy=collection-files-panel]').should('not.contain', 'foo').and('contain', 'bar');
 +                cy.get("[data-cy=collection-files-panel]").contains("foo").rightclick();
 +                cy.get("[data-cy=context-menu]").contains("Remove").click();
 +                cy.get("[data-cy=confirmation-dialog]").should("contain", "Removing file");
 +                cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
 +                cy.get("[data-cy=collection-version-number]").should("contain", "2");
 +                cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
  
                  // Click on version number, check version browser. Click on past version.
 -                cy.get('[data-cy=collection-version-browser]').should('not.exist');
 -                cy.get('[data-cy=collection-version-number]').contains('2').click();
 -                cy.get('[data-cy=collection-version-browser]')
 -                    .should('contain', 'Nr').and('contain', 'Size').and('contain', 'Date')
 +                cy.get("[data-cy=collection-version-browser]").should("not.exist");
 +                cy.get("[data-cy=collection-version-number]").contains("2").click();
 +                cy.get("[data-cy=collection-version-browser]")
 +                    .should("contain", "Nr")
 +                    .and("contain", "Size")
 +                    .and("contain", "Date")
                      .within(() => {
                          // Version 1: 6 bytes in size
 -                        cy.get('[data-cy=collection-version-browser-select-1]')
 -                            .should('contain', '1')
 -                            .and('contain', '6 B')
 -                            .and('contain', adminUser.user.full_name);
 +                        cy.get("[data-cy=collection-version-browser-select-1]")
 +                            .should("contain", "1")
 +                            .and("contain", "6 B")
 +                            .and("contain", adminUser.user.full_name);
                          // Version 2: 3 bytes in size (one file removed)
 -                        cy.get('[data-cy=collection-version-browser-select-2]')
 -                            .should('contain', '2')
 -                            .and('contain', '3 B')
 -                            .and('contain', activeUser.user.full_name);
 -                        cy.get('[data-cy=collection-version-browser-select-3]')
 -                            .should('not.exist');
 -                        cy.get('[data-cy=collection-version-browser-select-1]')
 -                            .click();
 +                        cy.get("[data-cy=collection-version-browser-select-2]")
 +                            .should("contain", "2")
 +                            .and("contain", "3 B")
 +                            .and("contain", activeUser.user.full_name);
 +                        cy.get("[data-cy=collection-version-browser-select-3]").should("not.exist");
 +                        cy.get("[data-cy=collection-version-browser-select-1]").click();
                      });
 -                cy.get('[data-cy=collection-info-panel]').should('contain', 'This is an old version');
 -                cy.get('[data-cy=read-only-icon]').should('exist');
 -                cy.get('[data-cy=collection-version-number]').should('contain', '1');
 -                cy.get('[data-cy=collection-info-panel]').should('contain', colName);
 -                cy.get('[data-cy=collection-files-panel]')
 -                    .should('contain', 'foo').and('contain', 'bar');
 +                cy.get("[data-cy=collection-info-panel]").should("contain", "This is an old version");
 +                cy.get("[data-cy=read-only-icon]").should("exist");
 +                cy.get("[data-cy=collection-version-number]").should("contain", "1");
 +                cy.get("[data-cy=collection-info-panel]").should("contain", colName);
 +                cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
  
                  // Check that only old collection action are available on context menu
 -                cy.get('[data-cy=collection-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]')
 -                    .should('contain', 'Restore version')
 -                    .and('not.contain', 'Add to favorites');
 -                cy.get('body').click(); // Collapse the menu avoiding details panel expansion
 +                cy.get("[data-cy=collection-panel-options-btn]").click();
 +                cy.get("[data-cy=context-menu]").should("contain", "Restore version").and("not.contain", "Add to favorites");
 +                cy.get("body").click(); // Collapse the menu avoiding details panel expansion
  
                  // Click on "head version" link, confirm that it's the latest version.
 -                cy.get('[data-cy=collection-info-panel]').contains('head version').click();
 -                cy.get('[data-cy=collection-info-panel]')
 -                    .should('not.contain', 'This is an old version');
 -                cy.get('[data-cy=read-only-icon]').should('not.exist');
 -                cy.get('[data-cy=collection-version-number]').should('contain', '2');
 -                cy.get('[data-cy=collection-info-panel]').should('contain', colName);
 -                cy.get('[data-cy=collection-files-panel]').
 -                    should('not.contain', 'foo').and('contain', 'bar');
 +                cy.get("[data-cy=collection-info-panel]").contains("head version").click();
 +                cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
 +                cy.get("[data-cy=read-only-icon]").should("not.exist");
 +                cy.get("[data-cy=collection-version-number]").should("contain", "2");
 +                cy.get("[data-cy=collection-info-panel]").should("contain", colName);
 +                cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
  
                  // Check that old collection action isn't available on context menu
 -                cy.get('[data-cy=collection-panel-options-btn]').click()
 -                cy.get('[data-cy=context-menu]').should('not.contain', 'Restore version')
 -                cy.get('body').click(); // Collapse the menu avoiding details panel expansion
 +                cy.get("[data-cy=collection-panel-options-btn]").click();
 +                cy.get("[data-cy=context-menu]").should("not.contain", "Restore version");
 +                cy.get("body").click(); // Collapse the menu avoiding details panel expansion
  
                  // Make another change, confirm new version.
 -                cy.get('[data-cy=collection-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').contains('Edit collection').click();
 -                cy.get('[data-cy=form-dialog]')
 -                    .should('contain', 'Edit Collection')
 +                cy.get("[data-cy=collection-panel-options-btn]").click();
 +                cy.get("[data-cy=context-menu]").contains("Edit collection").click();
 +                cy.get("[data-cy=form-dialog]")
 +                    .should("contain", "Edit Collection")
                      .within(() => {
                          // appends some text
 -                        cy.get('input').first().type(' renamed');
 +                        cy.get("input").first().type(" renamed");
                      });
 -                cy.get('[data-cy=form-submit-btn]').click();
 -                cy.get('[data-cy=collection-info-panel]')
 -                    .should('not.contain', 'This is an old version');
 -                cy.get('[data-cy=read-only-icon]').should('not.exist');
 -                cy.get('[data-cy=collection-version-number]').should('contain', '3');
 -                cy.get('[data-cy=collection-info-panel]').should('contain', colName + ' renamed');
 -                cy.get('[data-cy=collection-files-panel]')
 -                    .should('not.contain', 'foo').and('contain', 'bar');
 -                cy.get('[data-cy=collection-version-browser-select-3]')
 -                    .should('contain', '3').and('contain', '3 B');
 +                cy.get("[data-cy=form-submit-btn]").click();
 +                cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
 +                cy.get("[data-cy=read-only-icon]").should("not.exist");
 +                cy.get("[data-cy=collection-version-number]").should("contain", "3");
 +                cy.get("[data-cy=collection-info-panel]").should("contain", colName + " renamed");
 +                cy.get("[data-cy=collection-files-panel]").should("not.contain", "foo").and("contain", "bar");
 +                cy.get("[data-cy=collection-version-browser-select-3]").should("contain", "3").and("contain", "3 B");
  
                  // Check context menus on version browser
-                 cy.get("[data-cy=collection-version-browser-select-3]").rightclick({ force: true });
+                 cy.waitForDom();
 -                cy.get('[data-cy=collection-version-browser-select-3]').rightclick()
 -                cy.get('[data-cy=context-menu]')
 -                    .should('contain', 'Add to favorites')
 -                    .and('contain', 'Make a copy')
 -                    .and('contain', 'Edit collection');
 -                cy.get('body').click();
++                cy.get("[data-cy=collection-version-browser-select-3]").rightclick();
 +                cy.get("[data-cy=context-menu]")
 +                    .should("contain", "Add to favorites")
 +                    .and("contain", "Make a copy")
 +                    .and("contain", "Edit collection");
 +                cy.get("body").click();
                  // (and now an old version...)
 -                cy.get('[data-cy=collection-version-browser-select-1]').rightclick()
 -                cy.get('[data-cy=context-menu]')
 -                    .should('not.contain', 'Add to favorites')
 -                    .and('contain', 'Make a copy')
 -                    .and('not.contain', 'Edit collection');
 -                cy.get('body').click();
 +                cy.get("[data-cy=collection-version-browser-select-1]").rightclick();
 +                cy.get("[data-cy=context-menu]")
 +                    .should("not.contain", "Add to favorites")
 +                    .and("contain", "Make a copy")
 +                    .and("not.contain", "Edit collection");
 +                cy.get("body").click();
  
                  // Restore first version
 -                cy.get('[data-cy=collection-version-browser]').within(() => {
 -                    cy.get('[data-cy=collection-version-browser-select-1]').click();
 +                cy.get("[data-cy=collection-version-browser]").within(() => {
 +                    cy.get("[data-cy=collection-version-browser-select-1]").click();
                  });
 -                cy.get('[data-cy=collection-panel-options-btn]').click()
 -                cy.get('[data-cy=context-menu]').contains('Restore version').click();
 -                cy.get('[data-cy=confirmation-dialog]').should('contain', 'Restore version');
 -                cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
 -                cy.get('[data-cy=collection-info-panel]')
 -                    .should('not.contain', 'This is an old version');
 -                cy.get('[data-cy=collection-version-number]').should('contain', '4');
 -                cy.get('[data-cy=collection-info-panel]').should('contain', colName);
 -                cy.get('[data-cy=collection-files-panel]')
 -                    .should('contain', 'foo').and('contain', 'bar');
 +                cy.get("[data-cy=collection-panel-options-btn]").click();
 +                cy.get("[data-cy=context-menu]").contains("Restore version").click();
 +                cy.get("[data-cy=confirmation-dialog]").should("contain", "Restore version");
 +                cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
 +                cy.get("[data-cy=collection-info-panel]").should("not.contain", "This is an old version");
 +                cy.get("[data-cy=collection-version-number]").should("contain", "4");
 +                cy.get("[data-cy=collection-info-panel]").should("contain", colName);
 +                cy.get("[data-cy=collection-files-panel]").should("contain", "foo").and("contain", "bar");
              });
      });
  
-     it("creates collection from selected files of another collection", () => {
 -    it('copies selected files into new collection', () => {
++    it("copies selected files into new collection", () => {
+         cy.createCollection(adminUser.token, {
+             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
+             owner_uuid: activeUser.user.uuid,
+             preserve_version: true,
 -            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
++            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
+         })
 -            .as('collection').then(function () {
++            .as("collection")
++            .then(function () {
+                 // Visit collection, check basic information
 -                cy.loginAs(activeUser)
++                cy.loginAs(activeUser);
+                 cy.goToPath(`/collections/${this.collection.uuid}`);
 -                cy.get('[data-cy=collection-files-panel]').within(() => {
 -                    cy.get('input[type=checkbox]').first().click();
++                cy.get("[data-cy=collection-files-panel]").within(() => {
++                    cy.get("input[type=checkbox]").first().click();
+                 });
 -                cy.get('[data-cy=collection-files-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').contains('Copy selected into new collection').click();
++                cy.get("[data-cy=collection-files-panel-options-btn]").click();
++                cy.get("[data-cy=context-menu]").contains("Copy selected into new collection").click();
 -                cy.get('[data-cy=form-dialog]').contains('Projects').click();
++                cy.get("[data-cy=form-dialog]").contains("Projects").click();
 -                cy.get('[data-cy=form-submit-btn]').click();
++                cy.get("[data-cy=form-submit-btn]").click();
 -                cy.waitForDom().get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click();
++                cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
 -                cy.waitForDom().get('main').contains(`Files extracted from: ${this.collection.name}`).click();
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'bar');
++                cy.waitForDom().get("main").contains(`Files extracted from: ${this.collection.name}`).click();
++                cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
+             });
+     });
 -    it('copies selected files into existing collection', () => {
++    it("copies selected files into existing collection", () => {
+         cy.createCollection(adminUser.token, {
+             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
+             owner_uuid: activeUser.user.uuid,
+             preserve_version: true,
 -            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
 -        }).as('sourceCollection')
++            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
++        }).as("sourceCollection");
+         cy.createCollection(adminUser.token, {
+             name: `Destination Collection ${Math.floor(Math.random() * 999999)}`,
+             owner_uuid: activeUser.user.uuid,
+             preserve_version: true,
 -            manifest_text: ""
 -        }).as('destinationCollection');
++            manifest_text: "",
++        }).as("destinationCollection");
 -        cy.getAll('@sourceCollection', '@destinationCollection').then(function ([sourceCollection, destinationCollection]) {
 -                // Visit collection, check basic information
 -                cy.loginAs(activeUser)
 -                cy.goToPath(`/collections/${sourceCollection.uuid}`);
++        cy.getAll("@sourceCollection", "@destinationCollection").then(function ([sourceCollection, destinationCollection]) {
++            // Visit collection, check basic information
++            cy.loginAs(activeUser);
++            cy.goToPath(`/collections/${sourceCollection.uuid}`);
 -                cy.get('[data-cy=collection-files-panel]').within(() => {
 -                    cy.get('input[type=checkbox]').first().click();
 -                });
++            cy.get("[data-cy=collection-files-panel]").within(() => {
++                cy.get("input[type=checkbox]").first().click();
++            });
 -                cy.get('[data-cy=collection-files-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').contains('Copy selected into existing collection').click();
++            cy.get("[data-cy=collection-files-panel-options-btn]").click();
++            cy.get("[data-cy=context-menu]").contains("Copy selected into existing collection").click();
 -                cy.get('[data-cy=form-dialog]').contains(destinationCollection.name).click();
++            cy.get("[data-cy=form-dialog]").contains(destinationCollection.name).click();
 -                cy.get('[data-cy=form-submit-btn]').click();
 -                cy.wait(2000);
++            cy.get("[data-cy=form-submit-btn]").click();
++            cy.wait(2000);
 -                cy.goToPath(`/collections/${destinationCollection.uuid}`);
++            cy.goToPath(`/collections/${destinationCollection.uuid}`);
 -                cy.get('main').contains(destinationCollection.name).should('exist');
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'bar');
 -            });
++            cy.get("main").contains(destinationCollection.name).should("exist");
++            cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
++        });
+     });
 -    it('copies selected files into separate collections', () => {
++    it("copies selected files into separate collections", () => {
+         cy.createCollection(adminUser.token, {
+             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
+             owner_uuid: activeUser.user.uuid,
+             preserve_version: true,
 -            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
 -        }).as('sourceCollection')
++            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
++        }).as("sourceCollection");
 -        cy.getAll('@sourceCollection').then(function ([sourceCollection]) {
 -                // Visit collection, check basic information
 -                cy.loginAs(activeUser)
 -                cy.goToPath(`/collections/${sourceCollection.uuid}`);
++        cy.getAll("@sourceCollection").then(function ([sourceCollection]) {
++            // Visit collection, check basic information
++            cy.loginAs(activeUser);
++            cy.goToPath(`/collections/${sourceCollection.uuid}`);
 -                // Select both files
 -                cy.waitForDom().get('[data-cy=collection-files-panel]').within(() => {
 -                    cy.get('input[type=checkbox]').first().click();
 -                    cy.get('input[type=checkbox]').last().click();
++            // Select both files
++            cy.waitForDom()
++                .get("[data-cy=collection-files-panel]")
++                .within(() => {
++                    cy.get("input[type=checkbox]").first().click();
++                    cy.get("input[type=checkbox]").last().click();
+                 });
 -                // Copy to separate collections
 -                cy.get('[data-cy=collection-files-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').contains('Copy selected into separate collections').click();
 -                cy.get('[data-cy=form-dialog]').contains('Projects').click();
 -                cy.get('[data-cy=form-submit-btn]').click();
 -
 -                // Verify created collections
 -                cy.waitForDom().get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click();
 -                cy.get('main').contains(`File copied from collection ${sourceCollection.name}/foo`).click();
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'foo');
 -                cy.get('.layout-pane-primary').contains('Projects').click();
 -                cy.get('main').contains(`File copied from collection ${sourceCollection.name}/bar`).click();
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'bar');
 -
 -                // Verify separate collection menu items not present when single file selected
 -                // Wait for dom for collection to re-render
 -                cy.waitForDom().get('[data-cy=collection-files-panel]').within(() => {
 -                    cy.get('input[type=checkbox]').first().click();
++            // Copy to separate collections
++            cy.get("[data-cy=collection-files-panel-options-btn]").click();
++            cy.get("[data-cy=context-menu]").contains("Copy selected into separate collections").click();
++            cy.get("[data-cy=form-dialog]").contains("Projects").click();
++            cy.get("[data-cy=form-submit-btn]").click();
++
++            // Verify created collections
++            cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
++            cy.get("main").contains(`File copied from collection ${sourceCollection.name}/foo`).click();
++            cy.get("[data-cy=collection-files-panel]").and("contain", "foo");
++            cy.get(".layout-pane-primary").contains("Projects").click();
++            cy.get("main").contains(`File copied from collection ${sourceCollection.name}/bar`).click();
++            cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
++
++            // Verify separate collection menu items not present when single file selected
++            // Wait for dom for collection to re-render
++            cy.waitForDom()
++                .get("[data-cy=collection-files-panel]")
++                .within(() => {
++                    cy.get("input[type=checkbox]").first().click();
+                 });
 -                cy.get('[data-cy=collection-files-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').should('not.contain', 'Copy selected into separate collections');
 -                cy.get('[data-cy=context-menu]').should('not.contain', 'Move selected into separate collections');
 -            });
++            cy.get("[data-cy=collection-files-panel-options-btn]").click();
++            cy.get("[data-cy=context-menu]").should("not.contain", "Copy selected into separate collections");
++            cy.get("[data-cy=context-menu]").should("not.contain", "Move selected into separate collections");
++        });
+     });
 -    it('moves selected files into new collection', () => {
++    it("moves selected files into new collection", () => {
          cy.createCollection(adminUser.token, {
              name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
              owner_uuid: activeUser.user.uuid,
              preserve_version: true,
 -            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
 +            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
          })
 -            .as('collection').then(function () {
 +            .as("collection")
 +            .then(function () {
                  // Visit collection, check basic information
 -                cy.loginAs(activeUser)
 +                cy.loginAs(activeUser);
                  cy.goToPath(`/collections/${this.collection.uuid}`);
  
 -                cy.get('[data-cy=collection-files-panel]').within(() => {
 -                    cy.get('input[type=checkbox]').first().click();
 +                cy.get("[data-cy=collection-files-panel]").within(() => {
 +                    cy.get("input[type=checkbox]").first().click();
                  });
  
 -                cy.get('[data-cy=collection-files-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').contains('Move selected into new collection').click();
 +                cy.get("[data-cy=collection-files-panel-options-btn]").click();
-                 cy.get("[data-cy=context-menu]").contains("Create a new collection with selected").click();
++                cy.get("[data-cy=context-menu]").contains("Move selected into new collection").click();
  
 -                cy.get('[data-cy=form-dialog]').contains('Projects').click();
 +                cy.get("[data-cy=form-dialog]").contains("Projects").click();
  
 -                cy.get('[data-cy=form-submit-btn]').click();
 +                cy.get("[data-cy=form-submit-btn]").click();
  
 -                cy.waitForDom().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");
 -                cy.get('main').contains(`Files moved from: ${this.collection.name}`).click();
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'bar');
++                cy.get("main").contains(`Files moved from: ${this.collection.name}`).click();
++                cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
+             });
+     });
 -    it('moves selected files into existing collection', () => {
++    it("moves selected files into existing collection", () => {
+         cy.createCollection(adminUser.token, {
+             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
+             owner_uuid: activeUser.user.uuid,
+             preserve_version: true,
 -            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
 -        }).as('sourceCollection')
++            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
++        }).as("sourceCollection");
+         cy.createCollection(adminUser.token, {
+             name: `Destination Collection ${Math.floor(Math.random() * 999999)}`,
+             owner_uuid: activeUser.user.uuid,
+             preserve_version: true,
 -            manifest_text: ""
 -        }).as('destinationCollection');
++            manifest_text: "",
++        }).as("destinationCollection");
 -        cy.getAll('@sourceCollection', '@destinationCollection').then(function ([sourceCollection, destinationCollection]) {
 -                // Visit collection, check basic information
 -                cy.loginAs(activeUser)
 -                cy.goToPath(`/collections/${sourceCollection.uuid}`);
++        cy.getAll("@sourceCollection", "@destinationCollection").then(function ([sourceCollection, destinationCollection]) {
++            // Visit collection, check basic information
++            cy.loginAs(activeUser);
++            cy.goToPath(`/collections/${sourceCollection.uuid}`);
 -                cy.get('[data-cy=collection-files-panel]').within(() => {
 -                    cy.get('input[type=checkbox]').first().click();
 -                });
++            cy.get("[data-cy=collection-files-panel]").within(() => {
++                cy.get("input[type=checkbox]").first().click();
++            });
 -                cy.get('[data-cy=collection-files-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').contains('Move selected into existing collection').click();
++            cy.get("[data-cy=collection-files-panel-options-btn]").click();
++            cy.get("[data-cy=context-menu]").contains("Move selected into existing collection").click();
 -                cy.get('[data-cy=form-dialog]').contains(destinationCollection.name).click();
++            cy.get("[data-cy=form-dialog]").contains(destinationCollection.name).click();
 -                cy.get('[data-cy=form-submit-btn]').click();
 -                cy.wait(2000);
++            cy.get("[data-cy=form-submit-btn]").click();
++            cy.wait(2000);
 -                cy.goToPath(`/collections/${destinationCollection.uuid}`);
++            cy.goToPath(`/collections/${destinationCollection.uuid}`);
 -                cy.get('main').contains(destinationCollection.name).should('exist');
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'bar');
 -            });
++            cy.get("main").contains(destinationCollection.name).should("exist");
++            cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
++        });
+     });
 -    it('moves selected files into separate collections', () => {
++    it("moves selected files into separate collections", () => {
+         cy.createCollection(adminUser.token, {
+             name: `Test Collection ${Math.floor(Math.random() * 999999)}`,
+             owner_uuid: activeUser.user.uuid,
+             preserve_version: true,
 -            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n"
 -        }).as('sourceCollection')
++            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo 0:3:bar\n",
++        }).as("sourceCollection");
 -        cy.getAll('@sourceCollection').then(function ([sourceCollection]) {
 -                // Visit collection, check basic information
 -                cy.loginAs(activeUser)
 -                cy.goToPath(`/collections/${sourceCollection.uuid}`);
 -
 -                // Select both files
 -                cy.get('[data-cy=collection-files-panel]').within(() => {
 -                    cy.get('input[type=checkbox]').first().click();
 -                    cy.get('input[type=checkbox]').last().click();
 -                });
++        cy.getAll("@sourceCollection").then(function ([sourceCollection]) {
++            // Visit collection, check basic information
++            cy.loginAs(activeUser);
++            cy.goToPath(`/collections/${sourceCollection.uuid}`);
 -                // Copy to separate collections
 -                cy.get('[data-cy=collection-files-panel-options-btn]').click();
 -                cy.get('[data-cy=context-menu]').contains('Move selected into separate collections').click();
 -                cy.get('[data-cy=form-dialog]').contains('Projects').click();
 -                cy.get('[data-cy=form-submit-btn]').click();
 -
 -                // Verify created collections
 -                cy.waitForDom().get('.layout-pane-primary', { timeout: 12000 }).contains('Projects').click();
 -                cy.get('main').contains(`File moved from collection ${sourceCollection.name}/foo`).click();
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'foo');
 -                cy.get('.layout-pane-primary').contains('Projects').click();
 -                cy.get('main').contains(`File moved from collection ${sourceCollection.name}/bar`).click();
 -                cy.get('[data-cy=collection-files-panel]')
 -                        .and('contain', 'bar');
++            // Select both files
++            cy.get("[data-cy=collection-files-panel]").within(() => {
++                cy.get("input[type=checkbox]").first().click();
++                cy.get("input[type=checkbox]").last().click();
              });
++
++            // Copy to separate collections
++            cy.get("[data-cy=collection-files-panel-options-btn]").click();
++            cy.get("[data-cy=context-menu]").contains("Move selected into separate collections").click();
++            cy.get("[data-cy=form-dialog]").contains("Projects").click();
++            cy.get("[data-cy=form-submit-btn]").click();
++
++            // Verify created collections
++            cy.waitForDom().get(".layout-pane-primary", { timeout: 12000 }).contains("Projects").click();
++            cy.get("main").contains(`File moved from collection ${sourceCollection.name}/foo`).click();
++            cy.get("[data-cy=collection-files-panel]").and("contain", "foo");
++            cy.get(".layout-pane-primary").contains("Projects").click();
++            cy.get("main").contains(`File moved from collection ${sourceCollection.name}/bar`).click();
++            cy.get("[data-cy=collection-files-panel]").and("contain", "bar");
++        });
      });
  
 -    it('creates new collection with properties on home project', function () {
 +    it("creates new collection with properties on home project", function () {
          cy.loginAs(activeUser);
          cy.goToPath(`/projects/${activeUser.user.uuid}`);
 -        cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
 -        cy.get('[data-cy=breadcrumb-last]').should('not.exist');
 +        cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
 +        cy.get("[data-cy=breadcrumb-last]").should("not.exist");
          // Create new collection
 -        cy.get('[data-cy=side-panel-button]').click();
 -        cy.get('[data-cy=side-panel-new-collection]').click();
 +        cy.get("[data-cy=side-panel-button]").click();
 +        cy.get("[data-cy=side-panel-new-collection]").click();
          // Name between brackets tests bugfix #17582
          const collName = `[Test collection (${Math.floor(999999 * Math.random())})]`;
  
index 4424b0350ee949094878f3fb9ee3b2e5b32be5c4,64f27c50b54cf53e602cd5afdd944b55a2c88f9b..438acbf14dca2608ddf23b9dc3822d4aedbf3dd2
@@@ -2,7 -2,9 +2,9 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
 -import { ContainerState } from 'models/container';
++import { ContainerState } from "models/container";
 -describe('Process tests', function() {
 +describe("Process tests", function () {
      let activeUser;
      let adminUser;
  
          });
      }
  
-     it("shows process logs", function () {
-         const crName = "test_container_request";
-         createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
-             cy.loginAs(activeUser);
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-details]").should("contain", crName);
-             cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
-             cy.createLog(activeUser.token, {
-                 object_uuid: containerRequest.container_uuid,
-                 properties: {
-                     text: "hello world",
-                 },
-                 event_type: "stdout",
-             }).then(function (log) {
-                 cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
 -    describe('Details panel', function() {
 -        it('shows process details', function() {
++    describe("Details panel", function () {
++        it("shows process details", function () {
+             createContainerRequest(
+                 activeUser,
+                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++                "arvados/jobs",
++                ["echo", "hello world"],
++                false,
++                "Committed"
++            ).then(function (containerRequest) {
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
 -                cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
 -                cy.get('[data-cy=process-details-attributes-runtime-user]').should('not.exist');
++                cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
++                cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
++                cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
+             });
+             // Fake submitted by another user
 -            cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
 -                req.reply((res) => {
 -                    res.body.modified_by_user_uuid = 'zzzzz-tpzed-000000000000000';
++            cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
++                req.reply(res => {
++                    res.body.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
+                 });
+             });
+             createContainerRequest(
+                 activeUser,
+                 `test_container_request ${Math.floor(Math.random() * 999999)}`,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++                "arvados/jobs",
++                ["echo", "hello world"],
++                false,
++                "Committed"
++            ).then(function (containerRequest) {
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
 -                cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`zzzzz-tpzed-000000000000000`);
 -                cy.get('[data-cy=process-details-attributes-runtime-user]').contains(`Active User (${activeUser.user.uuid})`);
++                cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
++                cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
++                cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
              });
          });
-     });
  
-     it("shows process details", function () {
-         createContainerRequest(
-             activeUser,
-             `test_container_request ${Math.floor(Math.random() * 999999)}`,
-             "arvados/jobs",
-             ["echo", "hello world"],
-             false,
-             "Committed"
-         ).then(function (containerRequest) {
-             cy.loginAs(activeUser);
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
-             cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`Active User (${activeUser.user.uuid})`);
-             cy.get("[data-cy=process-details-attributes-runtime-user]").should("not.exist");
 -        it('should show runtime status indicators', function() {
++        it("should show runtime status indicators", function () {
+             // Setup running container with runtime_status error & warning messages
 -            createContainerRequest(
 -                activeUser,
 -                'test_container_request',
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .as('containerRequest')
 -            .then(function(containerRequest) {
 -                expect(containerRequest.state).to.equal('Committed');
 -                expect(containerRequest.container_uuid).not.to.be.equal('');
 -
 -                cy.getContainer(activeUser.token, containerRequest.container_uuid)
 -                .then(function(queuedContainer) {
 -                    expect(queuedContainer.state).to.be.equal('Queued');
 -                });
 -                cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
 -                    state: 'Locked'
 -                }).then(function(lockedContainer) {
 -                    expect(lockedContainer.state).to.be.equal('Locked');
 -
 -                    cy.updateContainer(adminUser.token, lockedContainer.uuid, {
 -                        state: 'Running',
 -                        runtime_status: {
 -                            error: 'Something went wrong',
 -                            errorDetail: 'Process exited with status 1',
 -                            warning: 'Free disk space is low',
 -                        }
 -                    })
 -                    .as('runningContainer')
 -                    .then(function(runningContainer) {
 -                        expect(runningContainer.state).to.be.equal('Running');
 -                        expect(runningContainer.runtime_status).to.be.deep.equal({
 -                            'error': 'Something went wrong',
 -                            'errorDetail': 'Process exited with status 1',
 -                            'warning': 'Free disk space is low',
 -                        });
++            createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
++                .as("containerRequest")
++                .then(function (containerRequest) {
++                    expect(containerRequest.state).to.equal("Committed");
++                    expect(containerRequest.container_uuid).not.to.be.equal("");
++
++                    cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
++                        expect(queuedContainer.state).to.be.equal("Queued");
+                     });
 -                })
 -            });
++                    cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
++                        state: "Locked",
++                    }).then(function (lockedContainer) {
++                        expect(lockedContainer.state).to.be.equal("Locked");
++
++                        cy.updateContainer(adminUser.token, lockedContainer.uuid, {
++                            state: "Running",
++                            runtime_status: {
++                                error: "Something went wrong",
++                                errorDetail: "Process exited with status 1",
++                                warning: "Free disk space is low",
++                            },
++                        })
++                            .as("runningContainer")
++                            .then(function (runningContainer) {
++                                expect(runningContainer.state).to.be.equal("Running");
++                                expect(runningContainer.runtime_status).to.be.deep.equal({
++                                    error: "Something went wrong",
++                                    errorDetail: "Process exited with status 1",
++                                    warning: "Free disk space is low",
++                                });
++                            });
++                    });
++                });
+             // Test that the UI shows the error and warning messages
 -            cy.getAll('@containerRequest', '@runningContainer').then(function([containerRequest]) {
++            cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-runtime-status-error]')
 -                    .should('contain', 'Something went wrong')
 -                    .and('contain', 'Process exited with status 1');
 -                cy.get('[data-cy=process-runtime-status-warning]')
 -                    .should('contain', 'Free disk space is low')
 -                    .and('contain', 'No additional warning details available');
++                cy.get("[data-cy=process-runtime-status-error]")
++                    .should("contain", "Something went wrong")
++                    .and("contain", "Process exited with status 1");
++                cy.get("[data-cy=process-runtime-status-warning]")
++                    .should("contain", "Free disk space is low")
++                    .and("contain", "No additional warning details available");
+             });
 -
+             // Force container_count for testing
+             let containerCount = 2;
 -            cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
 -                req.reply((res) => {
++            cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
++                req.reply(res => {
+                     res.body.container_count = containerCount;
+                 });
+             });
 -            cy.getAll('@containerRequest').then(function([containerRequest]) {
++            cy.getAll("@containerRequest").then(function ([containerRequest]) {
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
 -                    .should('contain', 'Process retried 1 time');
++                cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time");
+             });
 -            cy.getAll('@containerRequest').then(function([containerRequest]) {
++            cy.getAll("@containerRequest").then(function ([containerRequest]) {
+                 containerCount = 3;
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
 -                    .should('contain', 'Process retried 2 times');
++                cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
+             });
          });
  
-         // Fake submitted by another user
-         cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
-             req.reply(res => {
-                 res.body.modified_by_user_uuid = "zzzzz-tpzed-000000000000000";
 -        it('allows copying processes', function() {
 -            const crName = 'first_container_request';
 -            const copiedCrName = 'copied_container_request';
 -            createContainerRequest(
 -                activeUser,
 -                crName,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++        it("allows copying processes", function () {
++            const crName = "first_container_request";
++            const copiedCrName = "copied_container_request";
++            createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-details]').should('contain', crName);
++                cy.get("[data-cy=process-details]").should("contain", crName);
 -                cy.get('[data-cy=process-details]').find('button[title="More options"]').click();
 -                cy.get('ul[data-cy=context-menu]').contains("Copy and re-run process").click();
++                cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
++                cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
+             });
 -            cy.get('[data-cy=form-dialog]').within(() => {
 -                cy.get('input[name=name]').clear().type(copiedCrName);
 -                cy.get('[data-cy=projects-tree-home-tree-picker]').click();
 -                cy.get('[data-cy=form-submit-btn]').click();
++            cy.get("[data-cy=form-dialog]").within(() => {
++                cy.get("input[name=name]").clear().type(copiedCrName);
++                cy.get("[data-cy=projects-tree-home-tree-picker]").click();
++                cy.get("[data-cy=form-submit-btn]").click();
              });
 -            cy.get('[data-cy=process-details]').should('contain', copiedCrName);
 -            cy.get('[data-cy=process-details]').find('button').contains('Run');
++            cy.get("[data-cy=process-details]").should("contain", copiedCrName);
++            cy.get("[data-cy=process-details]").find("button").contains("Run");
          });
  
-         createContainerRequest(
-             activeUser,
-             `test_container_request ${Math.floor(Math.random() * 999999)}`,
-             "arvados/jobs",
-             ["echo", "hello world"],
-             false,
-             "Committed"
-         ).then(function (containerRequest) {
-             cy.loginAs(activeUser);
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-details]").should("contain", containerRequest.name);
-             cy.get("[data-cy=process-details-attributes-modifiedby-user]").contains(`zzzzz-tpzed-000000000000000`);
-             cy.get("[data-cy=process-details-attributes-runtime-user]").contains(`Active User (${activeUser.user.uuid})`);
 -        const getFakeContainer = (fakeContainerUuid) => ({
++        const getFakeContainer = fakeContainerUuid => ({
+             href: `/containers/${fakeContainerUuid}`,
+             kind: "arvados#container",
+             etag: "ecfosljpnxfari9a8m7e4yv06",
+             uuid: fakeContainerUuid,
+             owner_uuid: "zzzzz-tpzed-000000000000000",
+             created_at: "2023-02-13T15:55:47.308915000Z",
+             modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
+             modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
+             modified_at: "2023-02-15T19:12:45.987086000Z",
+             command: [
 -            "arvados-cwl-runner",
 -            "--api=containers",
 -            "--local",
 -            "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
 -            "/var/lib/cwl/workflow.json#main",
 -            "/var/lib/cwl/cwl.input.json",
++                "arvados-cwl-runner",
++                "--api=containers",
++                "--local",
++                "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
++                "/var/lib/cwl/workflow.json#main",
++                "/var/lib/cwl/cwl.input.json",
+             ],
+             container_image: "4ad7d11381df349e464694762db14e04+303",
+             cwd: "/var/spool/cwl",
+             environment: {},
+             exit_code: null,
+             finished_at: null,
+             locked_by_uuid: null,
+             log: null,
+             output: null,
+             output_path: "/var/spool/cwl",
+             progress: null,
+             runtime_constraints: {
 -            API: true,
 -            cuda: {
 -                device_count: 0,
 -                driver_version: "",
 -                hardware_capability: "",
 -            },
 -            keep_cache_disk: 2147483648,
 -            keep_cache_ram: 0,
 -            ram: 1342177280,
 -            vcpus: 1,
++                API: true,
++                cuda: {
++                    device_count: 0,
++                    driver_version: "",
++                    hardware_capability: "",
++                },
++                keep_cache_disk: 2147483648,
++                keep_cache_ram: 0,
++                ram: 1342177280,
++                vcpus: 1,
+             },
+             runtime_status: {},
+             started_at: null,
+             auth_uuid: null,
+             scheduling_parameters: {
 -            max_run_time: 0,
 -            partitions: [],
 -            preemptible: false,
++                max_run_time: 0,
++                partitions: [],
++                preemptible: false,
+             },
+             runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
+             runtime_auth_scopes: ["all"],
+             lock_count: 2,
+             gateway_address: null,
+             interactive_session_started: false,
+             output_storage_classes: ["default"],
+             output_properties: {},
+             cost: 0.0,
+             subrequests_cost: 0.0,
          });
-     });
  
-     it("filters process logs by event type", function () {
-         const nodeInfoLogs = [
-             "Host Information",
-             "Linux compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p 5.4.0-1059-azure #62~18.04.1-Ubuntu SMP Tue Sep 14 17:53:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux",
-             "CPU Information",
-             "processor  : 0",
-             "vendor_id  : GenuineIntel",
-             "cpu family : 6",
-             "model      : 79",
-             "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
-         ];
-         const crunchRunLogs = [
-             "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
-             "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
-             "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
-             "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
-             "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
-         ];
-         const stdoutLogs = [
-             "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
-             "Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
-             "In hac habitasse platea dictumst.",
-             "Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
-             "Interdum et malesuada fames ac ante ipsum primis in faucibus.",
-             "Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
-             "Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
-             "Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
-             "Donec vitae leo id augue gravida bibendum.",
-             "Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
-             "Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.",
-             "Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
-             "Sed at enim sit amet nulla tincidunt mattis. Aenean eget aliquet ex, non ultrices ex. Nulla ex tortor, vestibulum aliquam tempor ac, aliquam vel est.",
-             "Fusce auctor faucibus libero id venenatis. Etiam sodales, odio eu cursus efficitur, quam sem blandit ex, quis porttitor enim dui quis lectus. In id tincidunt felis.",
-             "Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
-             "Duis tristique semper dolor, vitae pulvinar risus.",
-             "Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
-             "Nulla eget mollis ipsum.",
-         ];
 -        it('shows cancel button when appropriate', function() {
++        it("shows cancel button when appropriate", function () {
+             // Ignore collection requests
 -            cy.intercept({method: 'GET', url: `**/arvados/v1/collections/*`}, {
 -                statusCode: 200,
 -                body: {}
 -            });
++            cy.intercept(
++                { method: "GET", url: `**/arvados/v1/collections/*` },
++                {
++                    statusCode: 200,
++                    body: {},
++                }
++            );
  
-         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
-             containerRequest
-         ) {
-             cy.logsForContainer(activeUser.token, containerRequest.container_uuid, "node-info", nodeInfoLogs).as("nodeInfoLogs");
-             cy.logsForContainer(activeUser.token, containerRequest.container_uuid, "crunch-run", crunchRunLogs).as("crunchRunLogs");
-             cy.logsForContainer(activeUser.token, containerRequest.container_uuid, "stdout", stdoutLogs).as("stdoutLogs");
-             cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
-                 cy.loginAs(activeUser);
+             // Uncommitted container
+             const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
 -            createContainerRequest(
 -                activeUser,
 -                crUncommitted,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Uncommitted')
 -            .then(function(containerRequest) {
++            createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
++                containerRequest
++            ) {
+                 // Navigate to process and verify run / cancel button
                  cy.goToPath(`/processes/${containerRequest.uuid}`);
-                 // Should show main logs by default
-                 cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
-                 cy.get("[data-cy=process-logs]")
-                     .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
-                     .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
-                     .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
-                 // Select 'All logs'
-                 cy.get("[data-cy=process-logs-filter]").click();
-                 cy.get("body").contains("li", "All logs").click();
-                 cy.get("[data-cy=process-logs]")
-                     .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
-                     .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
-                     .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
-                 // Select 'node-info' logs
-                 cy.get("[data-cy=process-logs-filter]").click();
-                 cy.get("body").contains("li", "node-info").click();
-                 cy.get("[data-cy=process-logs]")
-                     .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
-                     .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
-                     .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
-                 // Select 'stdout' logs
-                 cy.get("[data-cy=process-logs-filter]").click();
-                 cy.get("body").contains("li", "stdout").click();
-                 cy.get("[data-cy=process-logs]")
-                     .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
-                     .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
-                     .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+                 cy.waitForDom();
 -                cy.get('[data-cy=process-details]').should('contain', crUncommitted);
 -                cy.get('[data-cy=process-run-button]').should('exist');
 -                cy.get('[data-cy=process-cancel-button]').should('not.exist');
++                cy.get("[data-cy=process-details]").should("contain", crUncommitted);
++                cy.get("[data-cy=process-run-button]").should("exist");
++                cy.get("[data-cy=process-cancel-button]").should("not.exist");
+             });
+             // Queued container
+             const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
 -            const fakeCrUuid = 'zzzzz-dz642-000000000000001';
 -            createContainerRequest(
 -                activeUser,
 -                crQueued,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++            const fakeCrUuid = "zzzzz-dz642-000000000000001";
++            createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
++                containerRequest
++            ) {
+                 // Fake container uuid
 -                cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
 -                    req.reply((res) => {
++                cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
++                    req.reply(res => {
+                         res.body.output_uuid = fakeCrUuid;
+                         res.body.priority = 500;
+                         res.body.state = "Committed";
+                     });
+                 });
+                 // Fake container
+                 const container = getFakeContainer(fakeCrUuid);
 -                cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrUuid}`}, {
 -                    statusCode: 200,
 -                    body: {...container, state: "Queued", priority: 500}
 -                });
++                cy.intercept(
++                    { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
++                    {
++                        statusCode: 200,
++                        body: { ...container, state: "Queued", priority: 500 },
++                    }
++                );
+                 // Navigate to process and verify cancel button
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
+                 cy.waitForDom();
 -                cy.get('[data-cy=process-details]').should('contain', crQueued);
 -                cy.get('[data-cy=process-cancel-button]').contains('Cancel');
++                cy.get("[data-cy=process-details]").should("contain", crQueued);
++                cy.get("[data-cy=process-cancel-button]").contains("Cancel");
+             });
+             // Locked container
+             const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
 -            const fakeCrLockedUuid = 'zzzzz-dz642-000000000000002';
 -            createContainerRequest(
 -                activeUser,
 -                crLocked,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++            const fakeCrLockedUuid = "zzzzz-dz642-000000000000002";
++            createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
++                containerRequest
++            ) {
+                 // Fake container uuid
 -                cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
 -                    req.reply((res) => {
++                cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
++                    req.reply(res => {
+                         res.body.output_uuid = fakeCrLockedUuid;
+                         res.body.priority = 500;
+                         res.body.state = "Committed";
+                     });
+                 });
+                 // Fake container
+                 const container = getFakeContainer(fakeCrLockedUuid);
 -                cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrLockedUuid}`}, {
 -                    statusCode: 200,
 -                    body: {...container, state: "Locked", priority: 500}
 -                });
++                cy.intercept(
++                    { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
++                    {
++                        statusCode: 200,
++                        body: { ...container, state: "Locked", priority: 500 },
++                    }
++                );
+                 // Navigate to process and verify cancel button
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
+                 cy.waitForDom();
 -                cy.get('[data-cy=process-details]').should('contain', crLocked);
 -                cy.get('[data-cy=process-cancel-button]').contains('Cancel');
++                cy.get("[data-cy=process-details]").should("contain", crLocked);
++                cy.get("[data-cy=process-cancel-button]").contains("Cancel");
+             });
+             // On Hold container
+             const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
 -            const fakeCrOnHoldUuid = 'zzzzz-dz642-000000000000003';
 -            createContainerRequest(
 -                activeUser,
 -                crOnHold,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++            const fakeCrOnHoldUuid = "zzzzz-dz642-000000000000003";
++            createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
++                containerRequest
++            ) {
+                 // Fake container uuid
 -                cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
 -                    req.reply((res) => {
++                cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
++                    req.reply(res => {
+                         res.body.output_uuid = fakeCrOnHoldUuid;
+                         res.body.priority = 0;
+                         res.body.state = "Committed";
+                     });
+                 });
+                 // Fake container
+                 const container = getFakeContainer(fakeCrOnHoldUuid);
 -                cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrOnHoldUuid}`}, {
 -                    statusCode: 200,
 -                    body: {...container, state: "Queued", priority: 0}
 -                });
++                cy.intercept(
++                    { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
++                    {
++                        statusCode: 200,
++                        body: { ...container, state: "Queued", priority: 0 },
++                    }
++                );
+                 // Navigate to process and verify cancel button
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
+                 cy.waitForDom();
 -                cy.get('[data-cy=process-details]').should('contain', crOnHold);
 -                cy.get('[data-cy=process-run-button]').should('exist');
 -                cy.get('[data-cy=process-cancel-button]').should('not.exist');
++                cy.get("[data-cy=process-details]").should("contain", crOnHold);
++                cy.get("[data-cy=process-run-button]").should("exist");
++                cy.get("[data-cy=process-cancel-button]").should("not.exist");
              });
          });
 -
      });
  
-     it("should show runtime status indicators", function () {
-         // Setup running container with runtime_status error & warning messages
-         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed")
-             .as("containerRequest")
-             .then(function (containerRequest) {
-                 expect(containerRequest.state).to.equal("Committed");
-                 expect(containerRequest.container_uuid).not.to.be.equal("");
 -
 -    describe('Logs panel', function() {
 -        it('shows live process logs', function() {
 -            cy.intercept({method: 'GET', url: '**/arvados/v1/containers/*'}, (req) => {
 -                req.reply((res) => {
++    describe("Logs panel", function () {
++        it("shows live process logs", function () {
++            cy.intercept({ method: "GET", url: "**/arvados/v1/containers/*" }, req => {
++                req.reply(res => {
+                     res.body.state = ContainerState.RUNNING;
+                 });
+             });
 -            const crName = 'test_container_request';
 -            createContainerRequest(
 -                activeUser,
 -                crName,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++            const crName = "test_container_request";
++            createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
+                 // Create empty log file before loading process page
 -                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
 -                    ""
 -                ])
++                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [""]);
  
-                 cy.getContainer(activeUser.token, containerRequest.container_uuid).then(function (queuedContainer) {
-                     expect(queuedContainer.state).to.be.equal("Queued");
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-details]').should('contain', crName);
 -                cy.get('[data-cy=process-logs]')
 -                    .should('contain', 'No logs yet')
 -                    .and('not.contain', 'hello world');
++                cy.get("[data-cy=process-details]").should("contain", crName);
++                cy.get("[data-cy=process-logs]").should("contain", "No logs yet").and("not.contain", "hello world");
+                 // Append a log line
 -                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
 -                    "2023-07-18T20:14:48.128642814Z hello world"
 -                ]).then(() => {
 -                    cy.get('[data-cy=process-logs]', {timeout: 7000})
 -                        .should('not.contain', 'No logs yet')
 -                        .and('contain', 'hello world');
++                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", ["2023-07-18T20:14:48.128642814Z hello world"]).then(() => {
++                    cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello world");
                  });
-                 cy.updateContainer(adminUser.token, containerRequest.container_uuid, {
-                     state: "Locked",
-                 }).then(function (lockedContainer) {
-                     expect(lockedContainer.state).to.be.equal("Locked");
-                     cy.updateContainer(adminUser.token, lockedContainer.uuid, {
-                         state: "Running",
-                         runtime_status: {
-                             error: "Something went wrong",
-                             errorDetail: "Process exited with status 1",
-                             warning: "Free disk space is low",
-                         },
-                     })
-                         .as("runningContainer")
-                         .then(function (runningContainer) {
-                             expect(runningContainer.state).to.be.equal("Running");
-                             expect(runningContainer.runtime_status).to.be.deep.equal({
-                                 error: "Something went wrong",
-                                 errorDetail: "Process exited with status 1",
-                                 warning: "Free disk space is low",
-                             });
-                         });
+                 // Append new log line to different file
 -                cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
 -                    "2023-07-18T20:14:49.128642814Z hello new line"
 -                ]).then(() => {
 -                    cy.get('[data-cy=process-logs]', {timeout: 7000})
 -                        .should('not.contain', 'No logs yet')
 -                        .and('contain', 'hello new line');
++                cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:49.128642814Z hello new line"]).then(() => {
++                    cy.get("[data-cy=process-logs]", { timeout: 7000 }).should("not.contain", "No logs yet").and("contain", "hello new line");
                  });
              });
-         // Test that the UI shows the error and warning messages
-         cy.getAll("@containerRequest", "@runningContainer").then(function ([containerRequest]) {
-             cy.loginAs(activeUser);
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-runtime-status-error]").should("contain", "Something went wrong").and("contain", "Process exited with status 1");
-             cy.get("[data-cy=process-runtime-status-warning]")
-                 .should("contain", "Free disk space is low")
-                 .and("contain", "No additional warning details available");
          });
  
-         // Force container_count for testing
-         let containerCount = 2;
-         cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
-             req.reply(res => {
-                 res.body.container_count = containerCount;
 -        it('filters process logs by event type', function() {
++        it("filters process logs by event type", function () {
+             const nodeInfoLogs = [
 -                'Host Information',
 -                'Linux compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p 5.4.0-1059-azure #62~18.04.1-Ubuntu SMP Tue Sep 14 17:53:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux',
 -                'CPU Information',
 -                'processor  : 0',
 -                'vendor_id  : GenuineIntel',
 -                'cpu family : 6',
 -                'model      : 79',
 -                'model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz'
++                "Host Information",
++                "Linux compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p 5.4.0-1059-azure #62~18.04.1-Ubuntu SMP Tue Sep 14 17:53:18 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux",
++                "CPU Information",
++                "processor  : 0",
++                "vendor_id  : GenuineIntel",
++                "cpu family : 6",
++                "model      : 79",
++                "model name : Intel(R) Xeon(R) CPU E5-2673 v4 @ 2.30GHz",
+             ];
+             const crunchRunLogs = [
 -                '2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection',
 -                '2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started',
 -                '2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)',
 -                '2022-03-22T13:56:26.244862836Z Executing container \'zzzzz-dz642-1wokwvcct9s9du3\' using docker runtime',
 -                '2022-03-22T13:56:26.245037738Z Executing on host \'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p\'',
++                "2022-03-22T13:56:22.542417997Z using local keepstore process (pid 3733) at http://localhost:46837, writing logs to keepstore.txt in log collection",
++                "2022-03-22T13:56:26.237571754Z crunch-run 2.4.0~dev20220321141729 (go1.17.1) started",
++                "2022-03-22T13:56:26.244704134Z crunch-run process has uid=0(root) gid=0(root) groups=0(root)",
++                "2022-03-22T13:56:26.244862836Z Executing container 'zzzzz-dz642-1wokwvcct9s9du3' using docker runtime",
++                "2022-03-22T13:56:26.245037738Z Executing on host 'compute-99cb150b26149780de44b929577e1aed-19rgca8vobuvc4p'",
+             ];
+             const stdoutLogs = [
 -                '2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.',
 -                '2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.',
 -                '2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.',
 -                '2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.',
 -                '2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.',
 -                '2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.',
 -                '2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.',
 -                '2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.',
 -                '2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.',
 -                '2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.',
 -                '2022-03-22T13:56:22.542418097Z Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.',
 -                '2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.',
 -                '2022-03-22T13:56:22.542418117Z Sed at enim sit amet nulla tincidunt mattis. Aenean eget aliquet ex, non ultrices ex. Nulla ex tortor, vestibulum aliquam tempor ac, aliquam vel est.',
 -                '2022-03-22T13:56:22.542418127Z Fusce auctor faucibus libero id venenatis. Etiam sodales, odio eu cursus efficitur, quam sem blandit ex, quis porttitor enim dui quis lectus. In id tincidunt felis.',
 -                '2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.',
 -                '2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.',
 -                '2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.',
 -                '2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.',
++                "2022-03-22T13:56:22.542417987Z Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec dui nisi, hendrerit porta sapien a, pretium dignissim purus.",
++                "2022-03-22T13:56:22.542417997Z Integer viverra, mauris finibus aliquet ultricies, dui mauris cursus justo, ut venenatis nibh ex eget neque.",
++                "2022-03-22T13:56:22.542418007Z In hac habitasse platea dictumst.",
++                "2022-03-22T13:56:22.542418027Z Fusce fringilla turpis id accumsan faucibus. Donec congue congue ex non posuere. In semper mi quis tristique rhoncus.",
++                "2022-03-22T13:56:22.542418037Z Interdum et malesuada fames ac ante ipsum primis in faucibus.",
++                "2022-03-22T13:56:22.542418047Z Quisque fermentum tortor ex, ut suscipit velit feugiat faucibus.",
++                "2022-03-22T13:56:22.542418057Z Donec vitae porta risus, at luctus nulla. Mauris gravida iaculis ipsum, id sagittis tortor egestas ac.",
++                "2022-03-22T13:56:22.542418067Z Maecenas condimentum volutpat nulla. Integer lacinia maximus risus eu posuere.",
++                "2022-03-22T13:56:22.542418077Z Donec vitae leo id augue gravida bibendum.",
++                "2022-03-22T13:56:22.542418087Z Nam libero libero, pretium ac faucibus elementum, mattis nec ex.",
++                "2022-03-22T13:56:22.542418097Z Nullam id laoreet nibh. Vivamus tellus metus, pretium quis justo ut, bibendum varius metus. Pellentesque vitae accumsan lorem, quis tincidunt augue.",
++                "2022-03-22T13:56:22.542418107Z Aliquam viverra nisi nulla, et efficitur dolor mattis in.",
++                "2022-03-22T13:56:22.542418117Z Sed at enim sit amet nulla tincidunt mattis. Aenean eget aliquet ex, non ultrices ex. Nulla ex tortor, vestibulum aliquam tempor ac, aliquam vel est.",
++                "2022-03-22T13:56:22.542418127Z Fusce auctor faucibus libero id venenatis. Etiam sodales, odio eu cursus efficitur, quam sem blandit ex, quis porttitor enim dui quis lectus. In id tincidunt felis.",
++                "2022-03-22T13:56:22.542418137Z Phasellus non ex quis arcu tempus faucibus molestie in sapien.",
++                "2022-03-22T13:56:22.542418147Z Duis tristique semper dolor, vitae pulvinar risus.",
++                "2022-03-22T13:56:22.542418157Z Aliquam tortor elit, luctus nec tortor eget, porta tristique nulla.",
++                "2022-03-22T13:56:22.542418167Z Nulla eget mollis ipsum.",
+             ];
 -            createContainerRequest(
 -                activeUser,
 -                'test_container_request',
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
 -                cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as('nodeInfoLogs');
 -                cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as('crunchRunLogs');
 -                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as('stdoutLogs');
 -
 -                cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
++            createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (
++                containerRequest
++            ) {
++                cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", nodeInfoLogs).as("nodeInfoLogs");
++                cy.appendLog(adminUser.token, containerRequest.uuid, "crunch-run.txt", crunchRunLogs).as("crunchRunLogs");
++                cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", stdoutLogs).as("stdoutLogs");
++
++                cy.getAll("@stdoutLogs", "@nodeInfoLogs", "@crunchRunLogs").then(function () {
+                     cy.loginAs(activeUser);
+                     cy.goToPath(`/processes/${containerRequest.uuid}`);
+                     // Should show main logs by default
 -                    cy.get('[data-cy=process-logs-filter]', {timeout: 7000}).should('contain', 'Main logs');
 -                    cy.get('[data-cy=process-logs]')
 -                        .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
 -                        .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
 -                        .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
++                    cy.get("[data-cy=process-logs-filter]", { timeout: 7000 }).should("contain", "Main logs");
++                    cy.get("[data-cy=process-logs]")
++                        .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
++                        .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
++                        .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+                     // Select 'All logs'
 -                    cy.get('[data-cy=process-logs-filter]').click();
 -                    cy.get('body').contains('li', 'All logs').click();
 -                    cy.get('[data-cy=process-logs]')
 -                        .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
 -                        .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
 -                        .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
++                    cy.get("[data-cy=process-logs-filter]").click();
++                    cy.get("body").contains("li", "All logs").click();
++                    cy.get("[data-cy=process-logs]")
++                        .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
++                        .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
++                        .and("contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+                     // Select 'node-info' logs
 -                    cy.get('[data-cy=process-logs-filter]').click();
 -                    cy.get('body').contains('li', 'node-info').click();
 -                    cy.get('[data-cy=process-logs]')
 -                        .should('not.contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
 -                        .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
 -                        .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
++                    cy.get("[data-cy=process-logs-filter]").click();
++                    cy.get("body").contains("li", "node-info").click();
++                    cy.get("[data-cy=process-logs]")
++                        .should("not.contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
++                        .and("contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
++                        .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+                     // Select 'stdout' logs
 -                    cy.get('[data-cy=process-logs-filter]').click();
 -                    cy.get('body').contains('li', 'stdout').click();
 -                    cy.get('[data-cy=process-logs]')
 -                        .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
 -                        .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
 -                        .and('not.contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
++                    cy.get("[data-cy=process-logs-filter]").click();
++                    cy.get("body").contains("li", "stdout").click();
++                    cy.get("[data-cy=process-logs]")
++                        .should("contain", stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
++                        .and("not.contain", nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
++                        .and("not.contain", crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+                 });
              });
          });
  
-         cy.getAll("@containerRequest").then(function ([containerRequest]) {
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 1 time");
 -        it('sorts combined logs', function() {
 -            const crName = 'test_container_request';
 -            createContainerRequest(
 -                activeUser,
 -                crName,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
++        it("sorts combined logs", function () {
++            const crName = "test_container_request";
++            createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
+                 cy.appendLog(adminUser.token, containerRequest.uuid, "node-info.txt", [
+                     "3: nodeinfo 1",
+                     "2: nodeinfo 2",
+                     "1: nodeinfo 3",
+                     "2: nodeinfo 4",
+                     "3: nodeinfo 5",
 -                ]).as('node-info');
++                ]).as("node-info");
+                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
+                     "2023-07-18T20:14:48.128642814Z first",
 -                    "2023-07-18T20:14:49.128642814Z third"
 -                ]).as('stdout');
++                    "2023-07-18T20:14:49.128642814Z third",
++                ]).as("stdout");
 -                cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", [
 -                    "2023-07-18T20:14:48.528642814Z second"
 -                ]).as('stderr');
++                cy.appendLog(adminUser.token, containerRequest.uuid, "stderr.txt", ["2023-07-18T20:14:48.528642814Z second"]).as("stderr");
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-details]').should('contain', crName);
 -                cy.get('[data-cy=process-logs]')
 -                    .should('contain', 'No logs yet');
++                cy.get("[data-cy=process-details]").should("contain", crName);
++                cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
 -                cy.getAll('@node-info', '@stdout', '@stderr').then(() => {
++                cy.getAll("@node-info", "@stdout", "@stderr").then(() => {
+                     // Verify sorted main logs
 -                    cy.get('[data-cy=process-logs] pre', {timeout: 7000})
 -                        .eq(0).should('contain', '2023-07-18T20:14:48.128642814Z first');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(1).should('contain', '2023-07-18T20:14:48.528642814Z second');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(2).should('contain', '2023-07-18T20:14:49.128642814Z third');
++                    cy.get("[data-cy=process-logs] pre", { timeout: 7000 }).eq(0).should("contain", "2023-07-18T20:14:48.128642814Z first");
++                    cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2023-07-18T20:14:48.528642814Z second");
++                    cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "2023-07-18T20:14:49.128642814Z third");
+                     // Switch to All logs
 -                    cy.get('[data-cy=process-logs-filter]').click();
 -                    cy.get('body').contains('li', 'All logs').click();
++                    cy.get("[data-cy=process-logs-filter]").click();
++                    cy.get("body").contains("li", "All logs").click();
+                     // Verify non-sorted lines were preserved
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(0).should('contain', '3: nodeinfo 1');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(1).should('contain', '2: nodeinfo 2');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(2).should('contain', '1: nodeinfo 3');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(3).should('contain', '2: nodeinfo 4');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(4).should('contain', '3: nodeinfo 5');
++                    cy.get("[data-cy=process-logs] pre").eq(0).should("contain", "3: nodeinfo 1");
++                    cy.get("[data-cy=process-logs] pre").eq(1).should("contain", "2: nodeinfo 2");
++                    cy.get("[data-cy=process-logs] pre").eq(2).should("contain", "1: nodeinfo 3");
++                    cy.get("[data-cy=process-logs] pre").eq(3).should("contain", "2: nodeinfo 4");
++                    cy.get("[data-cy=process-logs] pre").eq(4).should("contain", "3: nodeinfo 5");
+                     // Verify sorted logs
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(5).should('contain', '2023-07-18T20:14:48.128642814Z first');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(6).should('contain', '2023-07-18T20:14:48.528642814Z second');
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(7).should('contain', '2023-07-18T20:14:49.128642814Z third');
++                    cy.get("[data-cy=process-logs] pre").eq(5).should("contain", "2023-07-18T20:14:48.128642814Z first");
++                    cy.get("[data-cy=process-logs] pre").eq(6).should("contain", "2023-07-18T20:14:48.528642814Z second");
++                    cy.get("[data-cy=process-logs] pre").eq(7).should("contain", "2023-07-18T20:14:49.128642814Z third");
+                 });
+             });
          });
  
-         cy.getAll("@containerRequest").then(function ([containerRequest]) {
-             containerCount = 3;
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-runtime-status-retry-warning]", { timeout: 7000 }).should("contain", "Process retried 2 times");
 -        it('correctly generates sniplines', function() {
++        it("correctly generates sniplines", function () {
+             const SNIPLINE = `================ ✀ ================ ✀ ========= Some log(s) were skipped ========= ✀ ================ ✀ ================`;
 -            const crName = 'test_container_request';
 -            createContainerRequest(
 -                activeUser,
 -                crName,
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .then(function(containerRequest) {
 -
++            const crName = "test_container_request";
++            createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
+                 cy.appendLog(adminUser.token, containerRequest.uuid, "stdout.txt", [
 -                    'X'.repeat(63999) + '_' +
 -                    'O'.repeat(100) +
 -                    '_' + 'X'.repeat(63999)
 -                ]).as('stdout');
++                    "X".repeat(63999) + "_" + "O".repeat(100) + "_" + "X".repeat(63999),
++                ]).as("stdout");
+                 cy.loginAs(activeUser);
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-details]').should('contain', crName);
 -                cy.get('[data-cy=process-logs]')
 -                    .should('contain', 'No logs yet');
++                cy.get("[data-cy=process-details]").should("contain", crName);
++                cy.get("[data-cy=process-logs]").should("contain", "No logs yet");
+                 // Switch to stdout since lines are unsortable (no timestamp)
 -                cy.get('[data-cy=process-logs-filter]').click();
 -                cy.get('body').contains('li', 'stdout').click();
++                cy.get("[data-cy=process-logs-filter]").click();
++                cy.get("body").contains("li", "stdout").click();
 -                cy.getAll('@stdout').then(() => {
++                cy.getAll("@stdout").then(() => {
+                     // Verify first 64KB and snipline
 -                    cy.get('[data-cy=process-logs] pre', {timeout: 7000})
 -                        .eq(0).should('contain', 'X'.repeat(63999) + '_\n' + SNIPLINE);
++                    cy.get("[data-cy=process-logs] pre", { timeout: 7000 })
++                        .eq(0)
++                        .should("contain", "X".repeat(63999) + "_\n" + SNIPLINE);
+                     // Verify last 64KB
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .eq(1).should('contain', '_' + 'X'.repeat(63999));
++                    cy.get("[data-cy=process-logs] pre")
++                        .eq(1)
++                        .should("contain", "_" + "X".repeat(63999));
+                     // Verify none of the Os got through
 -                    cy.get('[data-cy=process-logs] pre')
 -                        .should('not.contain', 'O');
++                    cy.get("[data-cy=process-logs] pre").should("not.contain", "O");
+                 });
+             });
          });
 -
      });
  
-     const testInputs = [
-         {
-             definition: {
-                 id: "#main/input_file",
-                 label: "Label Description",
-                 type: "File",
 -    describe('I/O panel', function() {
++    describe("I/O panel", function () {
+         const testInputs = [
+             {
+                 definition: {
 -                    "id": "#main/input_file",
 -                    "label": "Label Description",
 -                    "type": "File"
++                    id: "#main/input_file",
++                    label: "Label Description",
++                    type: "File",
+                 },
+                 input: {
 -                    "input_file": {
 -                        "basename": "input1.tar",
 -                        "class": "File",
 -                        "location": "keep:00000000000000000000000000000000+01/input1.tar",
 -                        "secondaryFiles": [
++                    input_file: {
++                        basename: "input1.tar",
++                        class: "File",
++                        location: "keep:00000000000000000000000000000000+01/input1.tar",
++                        secondaryFiles: [
+                             {
 -                                "basename": "input1-2.txt",
 -                                "class": "File",
 -                                "location": "keep:00000000000000000000000000000000+01/input1-2.txt"
++                                basename: "input1-2.txt",
++                                class: "File",
++                                location: "keep:00000000000000000000000000000000+01/input1-2.txt",
+                             },
+                             {
 -                                "basename": "input1-3.txt",
 -                                "class": "File",
 -                                "location": "keep:00000000000000000000000000000000+01/input1-3.txt"
++                                basename: "input1-3.txt",
++                                class: "File",
++                                location: "keep:00000000000000000000000000000000+01/input1-3.txt",
+                             },
+                             {
 -                                "basename": "input1-4.txt",
 -                                "class": "File",
 -                                "location": "keep:00000000000000000000000000000000+01/input1-4.txt"
 -                            }
 -                        ]
 -                    }
 -                }
++                                basename: "input1-4.txt",
++                                class: "File",
++                                location: "keep:00000000000000000000000000000000+01/input1-4.txt",
++                            },
++                        ],
++                    },
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/input_dir",
 -                    "doc": "Doc Description",
 -                    "type": "Directory"
++                    id: "#main/input_dir",
++                    doc: "Doc Description",
++                    type: "Directory",
+                 },
+                 input: {
 -                    "input_dir": {
 -                        "basename": "11111111111111111111111111111111+01",
 -                        "class": "Directory",
 -                        "location": "keep:11111111111111111111111111111111+01"
 -                    }
 -                }
++                    input_dir: {
++                        basename: "11111111111111111111111111111111+01",
++                        class: "Directory",
++                        location: "keep:11111111111111111111111111111111+01",
++                    },
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/input_bool",
 -                    "doc": ["Doc desc 1", "Doc desc 2"],
 -                    "type": "boolean"
++                    id: "#main/input_bool",
++                    doc: ["Doc desc 1", "Doc desc 2"],
++                    type: "boolean",
+                 },
+                 input: {
 -                    "input_bool": true,
 -                }
++                    input_bool: true,
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/input_int",
 -                    "type": "int"
++                    id: "#main/input_int",
++                    type: "int",
+                 },
+                 input: {
 -                    "input_int": 1,
 -                }
++                    input_int: 1,
++                },
              },
-             input: {
-                 input_file: {
-                     basename: "input1.tar",
-                     class: "File",
-                     location: "keep:00000000000000000000000000000000+01/input1.tar",
-                     secondaryFiles: [
+             {
+                 definition: {
 -                    "id": "#main/input_long",
 -                    "type": "long"
++                    id: "#main/input_long",
++                    type: "long",
+                 },
+                 input: {
 -                    "input_long" : 1,
 -                }
++                    input_long: 1,
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/input_float",
 -                    "type": "float"
++                    id: "#main/input_float",
++                    type: "float",
+                 },
+                 input: {
 -                    "input_float": 1.5,
 -                }
++                    input_float: 1.5,
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/input_double",
 -                    "type": "double"
++                    id: "#main/input_double",
++                    type: "double",
+                 },
+                 input: {
 -                    "input_double": 1.3,
 -                }
++                    input_double: 1.3,
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/input_string",
 -                    "type": "string"
++                    id: "#main/input_string",
++                    type: "string",
+                 },
+                 input: {
 -                    "input_string": "Hello World",
 -                }
++                    input_string: "Hello World",
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/input_file_array",
 -                    "type": {
 -                      "items": "File",
 -                      "type": "array"
 -                    }
++                    id: "#main/input_file_array",
++                    type: {
++                        items: "File",
++                        type: "array",
++                    },
+                 },
+                 input: {
 -                    "input_file_array": [
++                    input_file_array: [
                          {
-                             basename: "input1-2.txt",
 -                            "basename": "input2.tar",
 -                            "class": "File",
 -                            "location": "keep:00000000000000000000000000000000+02/input2.tar"
++                            basename: "input2.tar",
 +                            class: "File",
-                             location: "keep:00000000000000000000000000000000+01/input1-2.txt",
++                            location: "keep:00000000000000000000000000000000+02/input2.tar",
                          },
                          {
-                             basename: "input1-3.txt",
 -                            "basename": "input3.tar",
 -                            "class": "File",
 -                            "location": "keep:00000000000000000000000000000000+03/input3.tar",
 -                            "secondaryFiles": [
++                            basename: "input3.tar",
 +                            class: "File",
-                             location: "keep:00000000000000000000000000000000+01/input1-3.txt",
++                            location: "keep:00000000000000000000000000000000+03/input3.tar",
++                            secondaryFiles: [
+                                 {
 -                                    "basename": "input3-2.txt",
 -                                    "class": "File",
 -                                    "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
 -                                }
 -                            ]
++                                    basename: "input3-2.txt",
++                                    class: "File",
++                                    location: "keep:00000000000000000000000000000000+03/input3-2.txt",
++                                },
++                            ],
                          },
                          {
-                             basename: "input1-4.txt",
-                             class: "File",
-                             location: "keep:00000000000000000000000000000000+01/input1-4.txt",
 -                            "$import": "import_path"
 -                        }
 -                    ]
 -                }
++                            $import: "import_path",
 +                        },
 +                    ],
 +                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_dir",
-                 doc: "Doc Description",
-                 type: "Directory",
-             },
-             input: {
-                 input_dir: {
-                     basename: "11111111111111111111111111111111+01",
-                     class: "Directory",
-                     location: "keep:11111111111111111111111111111111+01",
+             {
+                 definition: {
 -                    "id": "#main/input_dir_array",
 -                    "type": {
 -                      "items": "Directory",
 -                      "type": "array"
 -                    }
++                    id: "#main/input_dir_array",
++                    type: {
++                        items: "Directory",
++                        type: "array",
++                    },
+                 },
+                 input: {
 -                    "input_dir_array": [
++                    input_dir_array: [
+                         {
 -                            "basename": "11111111111111111111111111111111+02",
 -                            "class": "Directory",
 -                            "location": "keep:11111111111111111111111111111111+02"
++                            basename: "11111111111111111111111111111111+02",
++                            class: "Directory",
++                            location: "keep:11111111111111111111111111111111+02",
+                         },
+                         {
 -                            "basename": "11111111111111111111111111111111+03",
 -                            "class": "Directory",
 -                            "location": "keep:11111111111111111111111111111111+03"
++                            basename: "11111111111111111111111111111111+03",
++                            class: "Directory",
++                            location: "keep:11111111111111111111111111111111+03",
+                         },
+                         {
 -                            "$import": "import_path"
 -                        }
 -                    ]
 -                }
++                            $import: "import_path",
++                        },
++                    ],
 +                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_bool",
-                 doc: ["Doc desc 1", "Doc desc 2"],
-                 type: "boolean",
-             },
-             input: {
-                 input_bool: true,
+             {
+                 definition: {
 -                    "id": "#main/input_int_array",
 -                    "type": {
 -                      "items": "int",
 -                      "type": "array"
 -                    }
++                    id: "#main/input_int_array",
++                    type: {
++                        items: "int",
++                        type: "array",
++                    },
+                 },
+                 input: {
 -                    "input_int_array": [
++                    input_int_array: [
+                         1,
+                         3,
+                         5,
+                         {
 -                            "$import": "import_path"
 -                        }
 -                    ]
 -                }
++                            $import: "import_path",
++                        },
++                    ],
++                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_int",
-                 type: "int",
+             {
+                 definition: {
 -                    "id": "#main/input_long_array",
 -                    "type": {
 -                      "items": "long",
 -                      "type": "array"
 -                    }
++                    id: "#main/input_long_array",
++                    type: {
++                        items: "long",
++                        type: "array",
++                    },
+                 },
+                 input: {
 -                    "input_long_array": [
++                    input_long_array: [
+                         10,
+                         20,
+                         {
 -                            "$import": "import_path"
 -                        }
 -                    ]
 -                }
++                            $import: "import_path",
++                        },
++                    ],
++                },
              },
-             input: {
-                 input_int: 1,
+             {
+                 definition: {
 -                    "id": "#main/input_float_array",
 -                    "type": {
 -                      "items": "float",
 -                      "type": "array"
 -                    }
++                    id: "#main/input_float_array",
++                    type: {
++                        items: "float",
++                        type: "array",
++                    },
+                 },
+                 input: {
 -                    "input_float_array": [
++                    input_float_array: [
+                         10.2,
+                         10.4,
+                         10.6,
+                         {
 -                            "$import": "import_path"
 -                        }
 -                    ]
 -                }
++                            $import: "import_path",
++                        },
++                    ],
++                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_long",
-                 type: "long",
+             {
+                 definition: {
 -                    "id": "#main/input_double_array",
 -                    "type": {
 -                      "items": "double",
 -                      "type": "array"
 -                    }
++                    id: "#main/input_double_array",
++                    type: {
++                        items: "double",
++                        type: "array",
++                    },
+                 },
+                 input: {
 -                    "input_double_array": [
++                    input_double_array: [
+                         20.1,
+                         20.2,
+                         20.3,
+                         {
 -                            "$import": "import_path"
 -                        }
 -                    ]
 -                }
++                            $import: "import_path",
++                        },
++                    ],
++                },
              },
-             input: {
-                 input_long: 1,
+             {
+                 definition: {
 -                    "id": "#main/input_string_array",
 -                    "type": {
 -                      "items": "string",
 -                      "type": "array"
 -                    }
++                    id: "#main/input_string_array",
++                    type: {
++                        items: "string",
++                        type: "array",
++                    },
+                 },
+                 input: {
 -                    "input_string_array": [
++                    input_string_array: [
+                         "Hello",
+                         "World",
+                         "!",
+                         {
 -                            "$import": "import_path"
 -                        }
 -                    ]
 -                }
++                            $import: "import_path",
++                        },
++                    ],
++                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_float",
-                 type: "float",
+             {
+                 definition: {
 -                    "id": "#main/input_bool_include",
 -                    "type": "boolean"
++                    id: "#main/input_bool_include",
++                    type: "boolean",
+                 },
+                 input: {
 -                    "input_bool_include": {
 -                        "$include": "include_path"
 -                    }
 -                }
++                    input_bool_include: {
++                        $include: "include_path",
++                    },
++                },
              },
-             input: {
-                 input_float: 1.5,
+             {
+                 definition: {
 -                    "id": "#main/input_int_include",
 -                    "type": "int"
++                    id: "#main/input_int_include",
++                    type: "int",
+                 },
+                 input: {
 -                    "input_int_include": {
 -                        "$include": "include_path"
 -                    }
 -                }
++                    input_int_include: {
++                        $include: "include_path",
++                    },
++                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_double",
-                 type: "double",
+             {
+                 definition: {
 -                    "id": "#main/input_float_include",
 -                    "type": "float"
++                    id: "#main/input_float_include",
++                    type: "float",
+                 },
+                 input: {
 -                    "input_float_include": {
 -                        "$include": "include_path"
 -                    }
 -                }
++                    input_float_include: {
++                        $include: "include_path",
++                    },
++                },
              },
-             input: {
-                 input_double: 1.3,
+             {
+                 definition: {
 -                    "id": "#main/input_string_include",
 -                    "type": "string"
++                    id: "#main/input_string_include",
++                    type: "string",
+                 },
+                 input: {
 -                    "input_string_include": {
 -                        "$include": "include_path"
 -                    }
 -                }
++                    input_string_include: {
++                        $include: "include_path",
++                    },
++                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_string",
-                 type: "string",
+             {
+                 definition: {
 -                    "id": "#main/input_file_include",
 -                    "type": "File"
++                    id: "#main/input_file_include",
++                    type: "File",
+                 },
+                 input: {
 -                    "input_file_include": {
 -                        "$include": "include_path"
 -                    }
 -                }
++                    input_file_include: {
++                        $include: "include_path",
++                    },
++                },
              },
-             input: {
-                 input_string: "Hello World",
+             {
+                 definition: {
 -                    "id": "#main/input_directory_include",
 -                    "type": "Directory"
++                    id: "#main/input_directory_include",
++                    type: "Directory",
+                 },
+                 input: {
 -                    "input_directory_include": {
 -                        "$include": "include_path"
 -                    }
 -                }
++                    input_directory_include: {
++                        $include: "include_path",
++                    },
++                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_file_array",
-                 type: {
-                     items: "File",
-                     type: "array",
+             {
+                 definition: {
 -                    "id": "#main/input_file_url",
 -                    "type": "File"
++                    id: "#main/input_file_url",
++                    type: "File",
+                 },
+                 input: {
 -                    "input_file_url": {
 -                        "basename": "index.html",
 -                        "class": "File",
 -                        "location": "http://example.com/index.html"
 -                      }
 -                }
 -            }
++                    input_file_url: {
++                        basename: "index.html",
++                        class: "File",
++                        location: "http://example.com/index.html",
++                    },
 +                },
 +            },
-             input: {
-                 input_file_array: [
-                     {
-                         basename: "input2.tar",
+         ];
+         const testOutputs = [
+             {
+                 definition: {
 -                    "id": "#main/output_file",
 -                    "label": "Label Description",
 -                    "type": "File"
++                    id: "#main/output_file",
++                    label: "Label Description",
++                    type: "File",
+                 },
+                 output: {
 -                    "output_file": {
 -                        "basename": "cat.png",
 -                        "class": "File",
 -                        "location": "cat.png"
 -                    }
 -                }
++                    output_file: {
++                        basename: "cat.png",
 +                        class: "File",
-                         location: "keep:00000000000000000000000000000000+02/input2.tar",
++                        location: "cat.png",
 +                    },
-                     {
-                         basename: "input3.tar",
++                },
+             },
+             {
+                 definition: {
 -                    "id": "#main/output_file_with_secondary",
 -                    "doc": "Doc Description",
 -                    "type": "File"
++                    id: "#main/output_file_with_secondary",
++                    doc: "Doc Description",
++                    type: "File",
+                 },
+                 output: {
 -                    "output_file_with_secondary": {
 -                        "basename": "main.dat",
 -                        "class": "File",
 -                        "location": "main.dat",
 -                        "secondaryFiles": [
++                    output_file_with_secondary: {
++                        basename: "main.dat",
 +                        class: "File",
-                         location: "keep:00000000000000000000000000000000+03/input3.tar",
++                        location: "main.dat",
 +                        secondaryFiles: [
                              {
-                                 basename: "input3-2.txt",
 -                                "basename": "secondary.dat",
 -                                "class": "File",
 -                                "location": "secondary.dat"
++                                basename: "secondary.dat",
 +                                class: "File",
-                                 location: "keep:00000000000000000000000000000000+03/input3-2.txt",
++                                location: "secondary.dat",
+                             },
+                             {
 -                                "basename": "secondary2.dat",
 -                                "class": "File",
 -                                "location": "secondary2.dat"
 -                            }
 -                        ]
 -                    }
 -                }
++                                basename: "secondary2.dat",
++                                class: "File",
++                                location: "secondary2.dat",
 +                            },
 +                        ],
 +                    },
-                     {
-                         $import: "import_path",
-                     },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_dir_array",
-                 type: {
-                     items: "Directory",
-                     type: "array",
 +                },
              },
-             input: {
-                 input_dir_array: [
-                     {
-                         basename: "11111111111111111111111111111111+02",
-                         class: "Directory",
-                         location: "keep:11111111111111111111111111111111+02",
-                     },
-                     {
-                         basename: "11111111111111111111111111111111+03",
+             {
+                 definition: {
 -                    "id": "#main/output_dir",
 -                    "doc": ["Doc desc 1", "Doc desc 2"],
 -                    "type": "Directory"
++                    id: "#main/output_dir",
++                    doc: ["Doc desc 1", "Doc desc 2"],
++                    type: "Directory",
+                 },
+                 output: {
 -                    "output_dir": {
 -                        "basename": "outdir1",
 -                        "class": "Directory",
 -                        "location": "outdir1"
 -                    }
 -                }
++                    output_dir: {
++                        basename: "outdir1",
 +                        class: "Directory",
-                         location: "keep:11111111111111111111111111111111+03",
++                        location: "outdir1",
 +                    },
-                     {
-                         $import: "import_path",
-                     },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_int_array",
-                 type: {
-                     items: "int",
-                     type: "array",
 +                },
              },
-             input: {
-                 input_int_array: [
-                     1,
-                     3,
-                     5,
-                     {
-                         $import: "import_path",
-                     },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_long_array",
-                 type: {
-                     items: "long",
-                     type: "array",
+             {
+                 definition: {
 -                    "id": "#main/output_bool",
 -                    "type": "boolean"
++                    id: "#main/output_bool",
++                    type: "boolean",
                  },
-             },
-             input: {
-                 input_long_array: [
-                     10,
-                     20,
-                     {
-                         $import: "import_path",
-                     },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_float_array",
-                 type: {
-                     items: "float",
-                     type: "array",
+                 output: {
 -                    "output_bool": true
 -                }
++                    output_bool: true,
 +                },
              },
-             input: {
-                 input_float_array: [
-                     10.2,
-                     10.4,
-                     10.6,
-                     {
-                         $import: "import_path",
-                     },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_double_array",
-                 type: {
-                     items: "double",
-                     type: "array",
+             {
+                 definition: {
 -                    "id": "#main/output_int",
 -                    "type": "int"
++                    id: "#main/output_int",
++                    type: "int",
                  },
-             },
-             input: {
-                 input_double_array: [
-                     20.1,
-                     20.2,
-                     20.3,
-                     {
-                         $import: "import_path",
-                     },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_string_array",
-                 type: {
-                     items: "string",
-                     type: "array",
+                 output: {
 -                    "output_int": 1
 -                }
++                    output_int: 1,
 +                },
              },
-             input: {
-                 input_string_array: [
-                     "Hello",
-                     "World",
-                     "!",
-                     {
-                         $import: "import_path",
-                     },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_bool_include",
-                 type: "boolean",
-             },
-             input: {
-                 input_bool_include: {
-                     $include: "include_path",
+             {
+                 definition: {
 -                    "id": "#main/output_long",
 -                    "type": "long"
++                    id: "#main/output_long",
++                    type: "long",
                  },
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_int_include",
-                 type: "int",
-             },
-             input: {
-                 input_int_include: {
-                     $include: "include_path",
+                 output: {
 -                    "output_long": 1
 -                }
++                    output_long: 1,
 +                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_float_include",
-                 type: "float",
-             },
-             input: {
-                 input_float_include: {
-                     $include: "include_path",
+             {
+                 definition: {
 -                    "id": "#main/output_float",
 -                    "type": "float"
++                    id: "#main/output_float",
++                    type: "float",
                  },
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_string_include",
-                 type: "string",
-             },
-             input: {
-                 input_string_include: {
-                     $include: "include_path",
+                 output: {
 -                    "output_float": 100.5
 -                }
++                    output_float: 100.5,
 +                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_file_include",
-                 type: "File",
-             },
-             input: {
-                 input_file_include: {
-                     $include: "include_path",
+             {
+                 definition: {
 -                    "id": "#main/output_double",
 -                    "type": "double"
++                    id: "#main/output_double",
++                    type: "double",
                  },
-             },
-         },
-         {
-             definition: {
-                 id: "#main/input_directory_include",
-                 type: "Directory",
-             },
-             input: {
-                 input_directory_include: {
-                     $include: "include_path",
+                 output: {
 -                    "output_double": 100.3
 -                }
++                    output_double: 100.3,
 +                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/input_file_url",
-                 type: "File",
-             },
-             input: {
-                 input_file_url: {
-                     basename: "index.html",
-                     class: "File",
-                     location: "http://example.com/index.html",
+             {
+                 definition: {
 -                    "id": "#main/output_string",
 -                    "type": "string"
++                    id: "#main/output_string",
++                    type: "string",
                  },
-             },
-         },
-     ];
-     const testOutputs = [
-         {
-             definition: {
-                 id: "#main/output_file",
-                 label: "Label Description",
-                 type: "File",
-             },
-             output: {
-                 output_file: {
-                     basename: "cat.png",
-                     class: "File",
-                     location: "cat.png",
+                 output: {
 -                    "output_string": "Hello output"
 -                }
++                    output_string: "Hello output",
 +                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/output_file_with_secondary",
-                 doc: "Doc Description",
-                 type: "File",
-             },
-             output: {
-                 output_file_with_secondary: {
-                     basename: "main.dat",
-                     class: "File",
-                     location: "main.dat",
-                     secondaryFiles: [
+             {
+                 definition: {
 -                    "id": "#main/output_file_array",
 -                    "type": {
 -                        "items": "File",
 -                        "type": "array"
 -                    }
++                    id: "#main/output_file_array",
++                    type: {
++                        items: "File",
++                        type: "array",
++                    },
+                 },
+                 output: {
 -                    "output_file_array": [
++                    output_file_array: [
                          {
-                             basename: "secondary.dat",
 -                            "basename": "output2.tar",
 -                            "class": "File",
 -                            "location": "output2.tar"
++                            basename: "output2.tar",
 +                            class: "File",
-                             location: "secondary.dat",
++                            location: "output2.tar",
                          },
                          {
-                             basename: "secondary2.dat",
 -                            "basename": "output3.tar",
 -                            "class": "File",
 -                            "location": "output3.tar"
 -                        }
 -                    ]
 -                }
++                            basename: "output3.tar",
 +                            class: "File",
-                             location: "secondary2.dat",
++                            location: "output3.tar",
 +                        },
 +                    ],
 +                },
              },
-         },
-         {
-             definition: {
-                 id: "#main/output_dir",
-                 doc: ["Doc desc 1", "Doc desc 2"],
-                 type: "Directory",
-             },
-             output: {
-                 output_dir: {
-                     basename: "outdir1",
-                     class: "Directory",
-                     location: "outdir1",
+             {
+                 definition: {
 -                    "id": "#main/output_dir_array",
 -                    "type": {
 -                        "items": "Directory",
 -                        "type": "array"
 -                    }
++                    id: "#main/output_dir_array",
++                    type: {
++                        items: "Directory",
++                        type: "array",
++                    },
                  },
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_bool",
-                 type: "boolean",
-             },
-             output: {
-                 output_bool: true,
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_int",
-                 type: "int",
-             },
-             output: {
-                 output_int: 1,
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_long",
-                 type: "long",
-             },
-             output: {
-                 output_long: 1,
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_float",
-                 type: "float",
-             },
-             output: {
-                 output_float: 100.5,
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_double",
-                 type: "double",
-             },
-             output: {
-                 output_double: 100.3,
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_string",
-                 type: "string",
-             },
-             output: {
-                 output_string: "Hello output",
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_file_array",
-                 type: {
-                     items: "File",
-                     type: "array",
+                 output: {
 -                    "output_dir_array": [
++                    output_dir_array: [
+                         {
 -                            "basename": "outdir2",
 -                            "class": "Directory",
 -                            "location": "outdir2"
++                            basename: "outdir2",
++                            class: "Directory",
++                            location: "outdir2",
+                         },
+                         {
 -                            "basename": "outdir3",
 -                            "class": "Directory",
 -                            "location": "outdir3"
 -                        }
 -                    ]
 -                }
++                            basename: "outdir3",
++                            class: "Directory",
++                            location: "outdir3",
++                        },
++                    ],
 +                },
              },
-             output: {
-                 output_file_array: [
-                     {
-                         basename: "output2.tar",
-                         class: "File",
-                         location: "output2.tar",
-                     },
-                     {
-                         basename: "output3.tar",
-                         class: "File",
-                         location: "output3.tar",
+             {
+                 definition: {
 -                    "id": "#main/output_int_array",
 -                    "type": {
 -                        "items": "int",
 -                        "type": "array"
 -                    }
++                    id: "#main/output_int_array",
++                    type: {
++                        items: "int",
++                        type: "array",
 +                    },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_dir_array",
-                 type: {
-                     items: "Directory",
-                     type: "array",
+                 },
+                 output: {
 -                    "output_int_array": [
 -                        10,
 -                        11,
 -                        12
 -                    ]
 -                }
++                    output_int_array: [10, 11, 12],
 +                },
              },
-             output: {
-                 output_dir_array: [
-                     {
-                         basename: "outdir2",
-                         class: "Directory",
-                         location: "outdir2",
-                     },
-                     {
-                         basename: "outdir3",
-                         class: "Directory",
-                         location: "outdir3",
+             {
+                 definition: {
 -                    "id": "#main/output_long_array",
 -                    "type": {
 -                        "items": "long",
 -                        "type": "array"
 -                    }
++                    id: "#main/output_long_array",
++                    type: {
++                        items: "long",
++                        type: "array",
 +                    },
-                 ],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_int_array",
-                 type: {
-                     items: "int",
-                     type: "array",
                  },
-             },
-             output: {
-                 output_int_array: [10, 11, 12],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_long_array",
-                 type: {
-                     items: "long",
-                     type: "array",
+                 output: {
 -                    "output_long_array": [
 -                        51,
 -                        52
 -                    ]
 -                }
++                    output_long_array: [51, 52],
 +                },
              },
-             output: {
-                 output_long_array: [51, 52],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_float_array",
-                 type: {
-                     items: "float",
-                     type: "array",
+             {
+                 definition: {
 -                    "id": "#main/output_float_array",
 -                    "type": {
 -                        "items": "float",
 -                        "type": "array"
 -                    }
++                    id: "#main/output_float_array",
++                    type: {
++                        items: "float",
++                        type: "array",
++                    },
                  },
-             },
-             output: {
-                 output_float_array: [100.2, 100.4, 100.6],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_double_array",
-                 type: {
-                     items: "double",
-                     type: "array",
+                 output: {
 -                    "output_float_array": [
 -                        100.2,
 -                        100.4,
 -                        100.6
 -                    ]
 -                }
++                    output_float_array: [100.2, 100.4, 100.6],
 +                },
              },
-             output: {
-                 output_double_array: [100.1, 100.2, 100.3],
-             },
-         },
-         {
-             definition: {
-                 id: "#main/output_string_array",
-                 type: {
-                     items: "string",
-                     type: "array",
+             {
+                 definition: {
 -                    "id": "#main/output_double_array",
 -                    "type": {
 -                        "items": "double",
 -                        "type": "array"
 -                    }
++                    id: "#main/output_double_array",
++                    type: {
++                        items: "double",
++                        type: "array",
++                    },
+                 },
+                 output: {
 -                    "output_double_array": [
 -                        100.1,
 -                        100.2,
 -                        100.3
 -                    ]
 -                }
++                    output_double_array: [100.1, 100.2, 100.3],
 +                },
              },
-             output: {
-                 output_string_array: ["Hello", "Output", "!"],
+             {
+                 definition: {
 -                    "id": "#main/output_string_array",
 -                    "type": {
 -                        "items": "string",
 -                        "type": "array"
 -                    }
++                    id: "#main/output_string_array",
++                    type: {
++                        items: "string",
++                        type: "array",
++                    },
+                 },
+                 output: {
 -                    "output_string_array": [
 -                        "Hello",
 -                        "Output",
 -                        "!"
 -                    ]
 -                }
 -            }
++                    output_string_array: ["Hello", "Output", "!"],
++                },
 +            },
-         },
-     ];
-     const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
-         cy.get("table tr")
-             .contains(name)
-             .parents("tr")
-             .within($mainRow => {
-                 label && cy.contains(label);
-                 if (multipleRows) {
-                     cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
-                     if (val) {
-                         if (Array.isArray(val)) {
-                             val.forEach(v => cy.get("@secondaryRows").contains(v));
-                         } else {
-                             cy.get("@secondaryRows").contains(val);
+         ];
+         const verifyIOParameter = (name, label, doc, val, collection, multipleRows) => {
 -            cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
 -                label && cy.contains(label);
 -
 -                if (multipleRows) {
 -                    cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as('secondaryRows');
 -                    if (val) {
 -                        if (Array.isArray(val)) {
 -                            val.forEach(v => cy.get('@secondaryRows').contains(v));
 -                        } else {
 -                            cy.get('@secondaryRows').contains(val);
++            cy.get("table tr")
++                .contains(name)
++                .parents("tr")
++                .within($mainRow => {
++                    label && cy.contains(label);
++
++                    if (multipleRows) {
++                        cy.get($mainRow).nextUntil('[data-cy="process-io-param"]').as("secondaryRows");
++                        if (val) {
++                            if (Array.isArray(val)) {
++                                val.forEach(v => cy.get("@secondaryRows").contains(v));
++                            } else {
++                                cy.get("@secondaryRows").contains(val);
++                            }
                          }
--                    }
--                    if (collection) {
-                         cy.get("@secondaryRows").contains(collection);
 -                        cy.get('@secondaryRows').contains(collection);
--                    }
--                } else {
--                    if (val) {
--                        if (Array.isArray(val)) {
--                            val.forEach(v => cy.contains(v));
--                        } else {
--                            cy.contains(val);
++                        if (collection) {
++                            cy.get("@secondaryRows").contains(collection);
++                        }
++                    } else {
++                        if (val) {
++                            if (Array.isArray(val)) {
++                                val.forEach(v => cy.contains(v));
++                            } else {
++                                cy.contains(val);
++                            }
++                        }
++                        if (collection) {
++                            cy.contains(collection);
                          }
                      }
--                    if (collection) {
--                        cy.contains(collection);
--                    }
--                }
-             });
-     };
--
-     const verifyIOParameterImage = (name, url) => {
-         cy.get("table tr")
-             .contains(name)
-             .parents("tr")
-             .within(() => {
-                 cy.get('[alt="Inline Preview"]')
-                     .should("be.visible")
-                     .and($img => {
-                         expect($img[0].naturalWidth).to.be.greaterThan(0);
-                         expect($img[0].src).contains(url);
-                     });
 -
 -            });
++                });
+         };
+         const verifyIOParameterImage = (name, url) => {
 -            cy.get('table tr').contains(name).parents('tr').within(() => {
 -                cy.get('[alt="Inline Preview"]')
 -                    .should('be.visible')
 -                    .and(($img) => {
 -                        expect($img[0].naturalWidth).to.be.greaterThan(0);
 -                        expect($img[0].src).contains(url);
 -                    })
 -            });
++            cy.get("table tr")
++                .contains(name)
++                .parents("tr")
++                .within(() => {
++                    cy.get('[alt="Inline Preview"]')
++                        .should("be.visible")
++                        .and($img => {
++                            expect($img[0].naturalWidth).to.be.greaterThan(0);
++                            expect($img[0].src).contains(url);
++                        });
++                });
+         };
 -        it('displays IO parameters with keep links and previews', function() {
++        it("displays IO parameters with keep links and previews", function () {
+             // Create output collection for real files
+             cy.createCollection(adminUser.token, {
+                 name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+                 owner_uuid: activeUser.user.uuid,
 -            }).then((testOutputCollection) => {
 -                        cy.loginAs(activeUser);
++            }).then(testOutputCollection => {
++                cy.loginAs(activeUser);
 -                        cy.goToPath(`/collections/${testOutputCollection.uuid}`);
++                cy.goToPath(`/collections/${testOutputCollection.uuid}`);
 -                        cy.get('[data-cy=upload-button]').click();
++                cy.get("[data-cy=upload-button]").click();
 -                        cy.fixture('files/cat.png', 'base64').then(content => {
 -                            cy.get('[data-cy=drag-and-drop]').upload(content, 'cat.png');
 -                            cy.get('[data-cy=form-submit-btn]').click();
 -                            cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
 -                            // Confirm final collection state.
 -                            cy.get('[data-cy=collection-files-panel]')
 -                                .contains('cat.png').should('exist');
 -                        });
++                cy.fixture("files/cat.png", "base64").then(content => {
++                    cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
++                    cy.get("[data-cy=form-submit-btn]").click();
++                    cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
++                    // Confirm final collection state.
++                    cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
++                });
 -                        cy.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
 -                    });
++                cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
 +            });
-     };
  
-     it("displays IO parameters with keep links and previews", function () {
-         // Create output collection for real files
-         cy.createCollection(adminUser.token, {
-             name: `Test collection ${Math.floor(Math.random() * 999999)}`,
-             owner_uuid: activeUser.user.uuid,
-         }).then(testOutputCollection => {
-             cy.loginAs(activeUser);
+             // Get updated collection pdh
 -            cy.getAll('@testOutputCollection').then(([testOutputCollection]) => {
++            cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
+                 // Add output uuid and inputs to container request
 -                cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
 -                    req.reply((res) => {
++                cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
++                    req.reply(res => {
+                         res.body.output_uuid = testOutputCollection.uuid;
+                         res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
 -                            content: testInputs.map((param) => (param.input)).reduce((acc, val) => (Object.assign(acc, val)), {})
++                            content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
+                         };
+                         res.body.mounts["/var/lib/cwl/workflow.json"] = {
+                             content: {
 -                                $graph: [{
 -                                    id: "#main",
 -                                    inputs: testInputs.map((input) => (input.definition)),
 -                                    outputs: testOutputs.map((output) => (output.definition))
 -                                }]
 -                            }
++                                $graph: [
++                                    {
++                                        id: "#main",
++                                        inputs: testInputs.map(input => input.definition),
++                                        outputs: testOutputs.map(output => output.definition),
++                                    },
++                                ],
++                            },
+                         };
+                     });
+                 });
  
-             cy.goToPath(`/collections/${testOutputCollection.uuid}`);
+                 // Stub fake output collection
 -                cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${testOutputCollection.uuid}*`}, {
 -                    statusCode: 200,
 -                    body: {
 -                        uuid: testOutputCollection.uuid,
 -                        portable_data_hash: testOutputCollection.portable_data_hash,
++                cy.intercept(
++                    { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
++                    {
++                        statusCode: 200,
++                        body: {
++                            uuid: testOutputCollection.uuid,
++                            portable_data_hash: testOutputCollection.portable_data_hash,
++                        },
+                     }
 -                });
++                );
  
-             cy.get("[data-cy=upload-button]").click();
+                 // Stub fake output json
 -                cy.intercept({method: 'GET', url: '**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json'}, {
 -                    statusCode: 200,
 -                    body: testOutputs.map((param) => (param.output)).reduce((acc, val) => (Object.assign(acc, val)), {})
 -                });
++                cy.intercept(
++                    { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
++                    {
++                        statusCode: 200,
++                        body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
++                    }
++                );
  
-             cy.fixture("files/cat.png", "base64").then(content => {
-                 cy.get("[data-cy=drag-and-drop]").upload(content, "cat.png");
-                 cy.get("[data-cy=form-submit-btn]").click();
-                 cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
-                 // Confirm final collection state.
-                 cy.get("[data-cy=collection-files-panel]").contains("cat.png").should("exist");
+                 // Stub webdav response, points to output json
 -                cy.intercept({method: 'PROPFIND', url: '*'}, {
 -                    fixture: 'webdav-propfind-outputs.xml',
 -                });
++                cy.intercept(
++                    { method: "PROPFIND", url: "*" },
++                    {
++                        fixture: "webdav-propfind-outputs.xml",
++                    }
++                );
              });
  
-             cy.getCollection(activeUser.token, testOutputCollection.uuid).as("testOutputCollection");
 -            createContainerRequest(
 -                activeUser,
 -                'test_container_request',
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .as('containerRequest');
++            createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
++                "containerRequest"
++            );
 -            cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
++            cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-io-card] h6').contains('Inputs')
 -                    .parents('[data-cy=process-io-card]').within(() => {
 -                        verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
 -                        verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
 -                        verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
 -                        verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
 -                        verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
 -                        verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
 -                        verifyIOParameter('input_int', null, null, '1');
 -                        verifyIOParameter('input_long', null, null, '1');
 -                        verifyIOParameter('input_float', null, null, '1.5');
 -                        verifyIOParameter('input_double', null, null, '1.3');
 -                        verifyIOParameter('input_string', null, null, 'Hello World');
 -                        verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
 -                        verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
 -                        verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
 -                        verifyIOParameter('input_file_array', null, null, 'Cannot display value', undefined, true);
 -                        verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
 -                        verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
 -                        verifyIOParameter('input_dir_array', null, null, 'Cannot display value', undefined, true);
 -                        verifyIOParameter('input_int_array', null, null, ["1", "3", "5", "Cannot display value"]);
 -                        verifyIOParameter('input_long_array', null, null, ["10", "20", "Cannot display value"]);
 -                        verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
 -                        verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
 -                        verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!", "Cannot display value"]);
 -                        verifyIOParameter('input_bool_include', null, null, "Cannot display value");
 -                        verifyIOParameter('input_int_include', null, null, "Cannot display value");
 -                        verifyIOParameter('input_float_include', null, null, "Cannot display value");
 -                        verifyIOParameter('input_string_include', null, null, "Cannot display value");
 -                        verifyIOParameter('input_file_include', null, null, "Cannot display value");
 -                        verifyIOParameter('input_directory_include', null, null, "Cannot display value");
 -                        verifyIOParameter('input_file_url', null, null, "http://example.com/index.html");
++                cy.get("[data-cy=process-io-card] h6")
++                    .contains("Inputs")
++                    .parents("[data-cy=process-io-card]")
++                    .within(() => {
++                        verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
++                        verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
++                        verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
++                        verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
++                        verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
++                        verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
++                        verifyIOParameter("input_int", null, null, "1");
++                        verifyIOParameter("input_long", null, null, "1");
++                        verifyIOParameter("input_float", null, null, "1.5");
++                        verifyIOParameter("input_double", null, null, "1.3");
++                        verifyIOParameter("input_string", null, null, "Hello World");
++                        verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
++                        verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
++                        verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
++                        verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
++                        verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
++                        verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
++                        verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
++                        verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
++                        verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
++                        verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
++                        verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
++                        verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
++                        verifyIOParameter("input_bool_include", null, null, "Cannot display value");
++                        verifyIOParameter("input_int_include", null, null, "Cannot display value");
++                        verifyIOParameter("input_float_include", null, null, "Cannot display value");
++                        verifyIOParameter("input_string_include", null, null, "Cannot display value");
++                        verifyIOParameter("input_file_include", null, null, "Cannot display value");
++                        verifyIOParameter("input_directory_include", null, null, "Cannot display value");
++                        verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
+                     });
 -                cy.get('[data-cy=process-io-card] h6').contains('Outputs')
 -                    .parents('[data-cy=process-io-card]').within((ctx) => {
++                cy.get("[data-cy=process-io-card] h6")
++                    .contains("Outputs")
++                    .parents("[data-cy=process-io-card]")
++                    .within(ctx => {
+                         cy.get(ctx).scrollIntoView();
 -                        cy.get('[data-cy="io-preview-image-toggle"]').click({waitForAnimations: false});
++                        cy.get('[data-cy="io-preview-image-toggle"]').click({ waitForAnimations: false });
+                         const outPdh = testOutputCollection.portable_data_hash;
 -                        verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
 -                        verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
 -                        verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
 -                        verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
 -                        verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
 -                        verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
 -                        verifyIOParameter('output_bool', null, null, 'true');
 -                        verifyIOParameter('output_int', null, null, '1');
 -                        verifyIOParameter('output_long', null, null, '1');
 -                        verifyIOParameter('output_float', null, null, '100.5');
 -                        verifyIOParameter('output_double', null, null, '100.3');
 -                        verifyIOParameter('output_string', null, null, 'Hello output');
 -                        verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
 -                        verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
 -                        verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
 -                        verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
 -                        verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
 -                        verifyIOParameter('output_long_array', null, null, ["51", "52"]);
 -                        verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
 -                        verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
 -                        verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
++                        verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
++                        verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
++                        verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
++                        verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
++                        verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
++                        verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
++                        verifyIOParameter("output_bool", null, null, "true");
++                        verifyIOParameter("output_int", null, null, "1");
++                        verifyIOParameter("output_long", null, null, "1");
++                        verifyIOParameter("output_float", null, null, "100.5");
++                        verifyIOParameter("output_double", null, null, "100.3");
++                        verifyIOParameter("output_string", null, null, "Hello output");
++                        verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
++                        verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
++                        verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
++                        verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
++                        verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
++                        verifyIOParameter("output_long_array", null, null, ["51", "52"]);
++                        verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
++                        verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
++                        verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
+                     });
+             });
          });
  
-         // Get updated collection pdh
-         cy.getAll("@testOutputCollection").then(([testOutputCollection]) => {
 -        it('displays IO parameters with no value', function() {
 -
 -            const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
 -            const fakeOutputPDH = '11111111111111111111111111111111+99/';
++        it("displays IO parameters with no value", function () {
++            const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
++            const fakeOutputPDH = "11111111111111111111111111111111+99/";
+             cy.loginAs(activeUser);
              // Add output uuid and inputs to container request
 -            cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
 -                req.reply((res) => {
 +            cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
 +                req.reply(res => {
-                     res.body.output_uuid = testOutputCollection.uuid;
+                     res.body.output_uuid = fakeOutputUUID;
                      res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
-                         content: testInputs.map(param => param.input).reduce((acc, val) => Object.assign(acc, val), {}),
 -                        content: {}
++                        content: {},
                      };
                      res.body.mounts["/var/lib/cwl/workflow.json"] = {
                          content: {
              });
  
              // Stub fake output collection
 -            cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
 -                statusCode: 200,
 -                body: {
 -                    uuid: fakeOutputUUID,
 -                    portable_data_hash: fakeOutputPDH,
 +            cy.intercept(
-                 { method: "GET", url: `**/arvados/v1/collections/${testOutputCollection.uuid}*` },
++                { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
 +                {
 +                    statusCode: 200,
 +                    body: {
-                         uuid: testOutputCollection.uuid,
-                         portable_data_hash: testOutputCollection.portable_data_hash,
++                        uuid: fakeOutputUUID,
++                        portable_data_hash: fakeOutputPDH,
 +                    },
                  }
 -            });
 +            );
  
              // Stub fake output json
 -            cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
 -                statusCode: 200,
 -                body: {}
 -            });
 +            cy.intercept(
-                 { method: "GET", url: "**/c%3Dzzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json" },
++                { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
 +                {
 +                    statusCode: 200,
-                     body: testOutputs.map(param => param.output).reduce((acc, val) => Object.assign(acc, val), {}),
-                 }
-             );
-             // Stub webdav response, points to output json
-             cy.intercept(
-                 { method: "PROPFIND", url: "*" },
-                 {
-                     fixture: "webdav-propfind-outputs.xml",
++                    body: {},
 +                }
 +            );
-         });
-         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
-             "containerRequest"
-         );
  
-         cy.getAll("@containerRequest", "@testOutputCollection").then(function ([containerRequest, testOutputCollection]) {
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-io-card] h6")
-                 .contains("Inputs")
-                 .parents("[data-cy=process-io-card]")
-                 .within(() => {
-                     verifyIOParameter("input_file", null, "Label Description", "input1.tar", "00000000000000000000000000000000+01");
-                     verifyIOParameter("input_file", null, "Label Description", "input1-2.txt", undefined, true);
-                     verifyIOParameter("input_file", null, "Label Description", "input1-3.txt", undefined, true);
-                     verifyIOParameter("input_file", null, "Label Description", "input1-4.txt", undefined, true);
-                     verifyIOParameter("input_dir", null, "Doc Description", "/", "11111111111111111111111111111111+01");
-                     verifyIOParameter("input_bool", null, "Doc desc 1, Doc desc 2", "true");
-                     verifyIOParameter("input_int", null, null, "1");
-                     verifyIOParameter("input_long", null, null, "1");
-                     verifyIOParameter("input_float", null, null, "1.5");
-                     verifyIOParameter("input_double", null, null, "1.3");
-                     verifyIOParameter("input_string", null, null, "Hello World");
-                     verifyIOParameter("input_file_array", null, null, "input2.tar", "00000000000000000000000000000000+02");
-                     verifyIOParameter("input_file_array", null, null, "input3.tar", undefined, true);
-                     verifyIOParameter("input_file_array", null, null, "input3-2.txt", undefined, true);
-                     verifyIOParameter("input_file_array", null, null, "Cannot display value", undefined, true);
-                     verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+02");
-                     verifyIOParameter("input_dir_array", null, null, "/", "11111111111111111111111111111111+03", true);
-                     verifyIOParameter("input_dir_array", null, null, "Cannot display value", undefined, true);
-                     verifyIOParameter("input_int_array", null, null, ["1", "3", "5", "Cannot display value"]);
-                     verifyIOParameter("input_long_array", null, null, ["10", "20", "Cannot display value"]);
-                     verifyIOParameter("input_float_array", null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
-                     verifyIOParameter("input_double_array", null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
-                     verifyIOParameter("input_string_array", null, null, ["Hello", "World", "!", "Cannot display value"]);
-                     verifyIOParameter("input_bool_include", null, null, "Cannot display value");
-                     verifyIOParameter("input_int_include", null, null, "Cannot display value");
-                     verifyIOParameter("input_float_include", null, null, "Cannot display value");
-                     verifyIOParameter("input_string_include", null, null, "Cannot display value");
-                     verifyIOParameter("input_file_include", null, null, "Cannot display value");
-                     verifyIOParameter("input_directory_include", null, null, "Cannot display value");
-                     verifyIOParameter("input_file_url", null, null, "http://example.com/index.html");
-                 });
-             cy.get("[data-cy=process-io-card] h6")
-                 .contains("Outputs")
-                 .parents("[data-cy=process-io-card]")
-                 .within(ctx => {
-                     cy.get(ctx).scrollIntoView();
-                     cy.get('[data-cy="io-preview-image-toggle"]').click({ waitForAnimations: false });
-                     const outPdh = testOutputCollection.portable_data_hash;
-                     verifyIOParameter("output_file", null, "Label Description", "cat.png", `${outPdh}`);
-                     verifyIOParameterImage("output_file", `/c=${outPdh}/cat.png`);
-                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "main.dat", `${outPdh}`);
-                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary.dat", undefined, true);
-                     verifyIOParameter("output_file_with_secondary", null, "Doc Description", "secondary2.dat", undefined, true);
-                     verifyIOParameter("output_dir", null, "Doc desc 1, Doc desc 2", "outdir1", `${outPdh}`);
-                     verifyIOParameter("output_bool", null, null, "true");
-                     verifyIOParameter("output_int", null, null, "1");
-                     verifyIOParameter("output_long", null, null, "1");
-                     verifyIOParameter("output_float", null, null, "100.5");
-                     verifyIOParameter("output_double", null, null, "100.3");
-                     verifyIOParameter("output_string", null, null, "Hello output");
-                     verifyIOParameter("output_file_array", null, null, "output2.tar", `${outPdh}`);
-                     verifyIOParameter("output_file_array", null, null, "output3.tar", undefined, true);
-                     verifyIOParameter("output_dir_array", null, null, "outdir2", `${outPdh}`);
-                     verifyIOParameter("output_dir_array", null, null, "outdir3", undefined, true);
-                     verifyIOParameter("output_int_array", null, null, ["10", "11", "12"]);
-                     verifyIOParameter("output_long_array", null, null, ["51", "52"]);
-                     verifyIOParameter("output_float_array", null, null, ["100.2", "100.4", "100.6"]);
-                     verifyIOParameter("output_double_array", null, null, ["100.1", "100.2", "100.3"]);
-                     verifyIOParameter("output_string_array", null, null, ["Hello", "Output", "!"]);
-                 });
-         });
-     });
-     it("displays IO parameters with no value", function () {
-         const fakeOutputUUID = "zzzzz-4zz18-abcdefghijklmno";
-         const fakeOutputPDH = "11111111111111111111111111111111+99/";
-         cy.loginAs(activeUser);
-         // Add output uuid and inputs to container request
-         cy.intercept({ method: "GET", url: "**/arvados/v1/container_requests/*" }, req => {
-             req.reply(res => {
-                 res.body.output_uuid = fakeOutputUUID;
-                 res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
-                     content: {},
-                 };
-                 res.body.mounts["/var/lib/cwl/workflow.json"] = {
-                     content: {
-                         $graph: [
-                             {
-                                 id: "#main",
-                                 inputs: testInputs.map(input => input.definition),
-                                 outputs: testOutputs.map(output => output.definition),
-                             },
-                         ],
-                     },
-                 };
 -            cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
++            cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
+                 // Stub webdav response, points to output json
 -                cy.intercept({method: 'PROPFIND', url: '*'}, {
 -                    statusCode: 200,
 -                    body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
 -                });
++                cy.intercept(
++                    { method: "PROPFIND", url: "*" },
++                    {
++                        statusCode: 200,
++                        body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
++                    }
++                );
              });
-         });
-         // Stub fake output collection
-         cy.intercept(
-             { method: "GET", url: `**/arvados/v1/collections/${fakeOutputUUID}*` },
-             {
-                 statusCode: 200,
-                 body: {
-                     uuid: fakeOutputUUID,
-                     portable_data_hash: fakeOutputPDH,
-                 },
-             }
-         );
  
-         // Stub fake output json
-         cy.intercept(
-             { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
-             {
-                 statusCode: 200,
-                 body: {},
-             }
-         );
-         cy.readFile("cypress/fixtures/webdav-propfind-outputs.xml").then(data => {
-             // Stub webdav response, points to output json
-             cy.intercept(
-                 { method: "PROPFIND", url: "*" },
-                 {
-                     statusCode: 200,
-                     body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID),
-                 }
 -            createContainerRequest(
 -                activeUser,
 -                'test_container_request',
 -                'arvados/jobs',
 -                ['echo', 'hello world'],
 -                false, 'Committed')
 -            .as('containerRequest');
++            createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
++                "containerRequest"
 +            );
-         });
  
-         createContainerRequest(activeUser, "test_container_request", "arvados/jobs", ["echo", "hello world"], false, "Committed").as(
-             "containerRequest"
-         );
-         cy.getAll("@containerRequest").then(function ([containerRequest]) {
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-io-card] h6")
-                 .contains("Inputs")
-                 .parents("[data-cy=process-io-card]")
-                 .within(() => {
-                     cy.wait(2000);
-                     cy.waitForDom();
-                     cy.get("tbody tr").each(item => {
-                         cy.wrap(item).contains("No value");
 -            cy.getAll('@containerRequest').then(function([containerRequest]) {
++            cy.getAll("@containerRequest").then(function ([containerRequest]) {
+                 cy.goToPath(`/processes/${containerRequest.uuid}`);
 -                cy.get('[data-cy=process-io-card] h6').contains('Inputs')
 -                    .parents('[data-cy=process-io-card]').within(() => {
++                cy.get("[data-cy=process-io-card] h6")
++                    .contains("Inputs")
++                    .parents("[data-cy=process-io-card]")
++                    .within(() => {
+                         cy.wait(2000);
+                         cy.waitForDom();
 -                        cy.get('tbody tr').each((item) => {
 -                            cy.wrap(item).contains('No value');
++                        cy.get("tbody tr").each(item => {
++                            cy.wrap(item).contains("No value");
+                         });
                      });
-                 });
-             cy.get("[data-cy=process-io-card] h6")
-                 .contains("Outputs")
-                 .parents("[data-cy=process-io-card]")
-                 .within(() => {
-                     cy.get("tbody tr").each(item => {
-                         cy.wrap(item).contains("No value");
 -                cy.get('[data-cy=process-io-card] h6').contains('Outputs')
 -                    .parents('[data-cy=process-io-card]').within(() => {
 -                        cy.get('tbody tr').each((item) => {
 -                            cy.wrap(item).contains('No value');
++                cy.get("[data-cy=process-io-card] h6")
++                    .contains("Outputs")
++                    .parents("[data-cy=process-io-card]")
++                    .within(() => {
++                        cy.get("tbody tr").each(item => {
++                            cy.wrap(item).contains("No value");
+                         });
                      });
-                 });
-         });
-     });
-     it("allows copying processes", function () {
-         const crName = "first_container_request";
-         const copiedCrName = "copied_container_request";
-         createContainerRequest(activeUser, crName, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
-             cy.loginAs(activeUser);
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.get("[data-cy=process-details]").should("contain", crName);
-             cy.get("[data-cy=process-details]").find('button[title="More options"]').click();
-             cy.get("ul[data-cy=context-menu]").contains("Copy and re-run process").click();
-         });
-         cy.get("[data-cy=form-dialog]").within(() => {
-             cy.get("input[name=name]").clear().type(copiedCrName);
-             cy.get("[data-cy=projects-tree-home-tree-picker]").click();
-             cy.get("[data-cy=form-submit-btn]").click();
-         });
-         cy.get("[data-cy=process-details]").should("contain", copiedCrName);
-         cy.get("[data-cy=process-details]").find("button").contains("Run");
-     });
-     const getFakeContainer = fakeContainerUuid => ({
-         href: `/containers/${fakeContainerUuid}`,
-         kind: "arvados#container",
-         etag: "ecfosljpnxfari9a8m7e4yv06",
-         uuid: fakeContainerUuid,
-         owner_uuid: "zzzzz-tpzed-000000000000000",
-         created_at: "2023-02-13T15:55:47.308915000Z",
-         modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
-         modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
-         modified_at: "2023-02-15T19:12:45.987086000Z",
-         command: [
-             "arvados-cwl-runner",
-             "--api=containers",
-             "--local",
-             "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
-             "/var/lib/cwl/workflow.json#main",
-             "/var/lib/cwl/cwl.input.json",
-         ],
-         container_image: "4ad7d11381df349e464694762db14e04+303",
-         cwd: "/var/spool/cwl",
-         environment: {},
-         exit_code: null,
-         finished_at: null,
-         locked_by_uuid: null,
-         log: null,
-         output: null,
-         output_path: "/var/spool/cwl",
-         progress: null,
-         runtime_constraints: {
-             API: true,
-             cuda: {
-                 device_count: 0,
-                 driver_version: "",
-                 hardware_capability: "",
-             },
-             keep_cache_disk: 2147483648,
-             keep_cache_ram: 0,
-             ram: 1342177280,
-             vcpus: 1,
-         },
-         runtime_status: {},
-         started_at: null,
-         auth_uuid: null,
-         scheduling_parameters: {
-             max_run_time: 0,
-             partitions: [],
-             preemptible: false,
-         },
-         runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
-         runtime_auth_scopes: ["all"],
-         lock_count: 2,
-         gateway_address: null,
-         interactive_session_started: false,
-         output_storage_classes: ["default"],
-         output_properties: {},
-         cost: 0.0,
-         subrequests_cost: 0.0,
-     });
-     it("shows cancel button when appropriate", function () {
-         // Ignore collection requests
-         cy.intercept(
-             { method: "GET", url: `**/arvados/v1/collections/*` },
-             {
-                 statusCode: 200,
-                 body: {},
-             }
-         );
-         // Uncommitted container
-         const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
-         createContainerRequest(activeUser, crUncommitted, "arvados/jobs", ["echo", "hello world"], false, "Uncommitted").then(function (
-             containerRequest
-         ) {
-             // Navigate to process and verify run / cancel button
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.waitForDom();
-             cy.get("[data-cy=process-details]").should("contain", crUncommitted);
-             cy.get("[data-cy=process-run-button]").should("exist");
-             cy.get("[data-cy=process-cancel-button]").should("not.exist");
-         });
-         // Queued container
-         const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
-         const fakeCrUuid = "zzzzz-dz642-000000000000001";
-         createContainerRequest(activeUser, crQueued, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
-             // Fake container uuid
-             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
-                 req.reply(res => {
-                     res.body.output_uuid = fakeCrUuid;
-                     res.body.priority = 500;
-                     res.body.state = "Committed";
-                 });
              });
-             // Fake container
-             const container = getFakeContainer(fakeCrUuid);
-             cy.intercept(
-                 { method: "GET", url: `**/arvados/v1/container/${fakeCrUuid}` },
-                 {
-                     statusCode: 200,
-                     body: { ...container, state: "Queued", priority: 500 },
-                 }
-             );
-             // Navigate to process and verify cancel button
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.waitForDom();
-             cy.get("[data-cy=process-details]").should("contain", crQueued);
-             cy.get("[data-cy=process-cancel-button]").contains("Cancel");
-         });
-         // Locked container
-         const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
-         const fakeCrLockedUuid = "zzzzz-dz642-000000000000002";
-         createContainerRequest(activeUser, crLocked, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
-             // Fake container uuid
-             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
-                 req.reply(res => {
-                     res.body.output_uuid = fakeCrLockedUuid;
-                     res.body.priority = 500;
-                     res.body.state = "Committed";
-                 });
-             });
-             // Fake container
-             const container = getFakeContainer(fakeCrLockedUuid);
-             cy.intercept(
-                 { method: "GET", url: `**/arvados/v1/container/${fakeCrLockedUuid}` },
-                 {
-                     statusCode: 200,
-                     body: { ...container, state: "Locked", priority: 500 },
-                 }
-             );
-             // Navigate to process and verify cancel button
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.waitForDom();
-             cy.get("[data-cy=process-details]").should("contain", crLocked);
-             cy.get("[data-cy=process-cancel-button]").contains("Cancel");
-         });
-         // On Hold container
-         const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
-         const fakeCrOnHoldUuid = "zzzzz-dz642-000000000000003";
-         createContainerRequest(activeUser, crOnHold, "arvados/jobs", ["echo", "hello world"], false, "Committed").then(function (containerRequest) {
-             // Fake container uuid
-             cy.intercept({ method: "GET", url: `**/arvados/v1/container_requests/${containerRequest.uuid}` }, req => {
-                 req.reply(res => {
-                     res.body.output_uuid = fakeCrOnHoldUuid;
-                     res.body.priority = 0;
-                     res.body.state = "Committed";
-                 });
-             });
-             // Fake container
-             const container = getFakeContainer(fakeCrOnHoldUuid);
-             cy.intercept(
-                 { method: "GET", url: `**/arvados/v1/container/${fakeCrOnHoldUuid}` },
-                 {
-                     statusCode: 200,
-                     body: { ...container, state: "Queued", priority: 0 },
-                 }
-             );
-             // Navigate to process and verify cancel button
-             cy.goToPath(`/processes/${containerRequest.uuid}`);
-             cy.waitForDom();
-             cy.get("[data-cy=process-details]").should("contain", crOnHold);
-             cy.get("[data-cy=process-run-button]").should("exist");
-             cy.get("[data-cy=process-cancel-button]").should("not.exist");
          });
      });
 -
  });
index f0de6f862221b5cee6bd7963fe9d00c2fc2dc1cb,fd14cc4226323b331a22c09ea35724748a07f6b7..74c4cd9109896300e0858bff17fddddfb2798bd7
@@@ -126,24 -125,27 +126,25 @@@ describe("Project tests", function () 
          cy.loginAs(activeUser);
  
          // Create project
 -        cy.get('[data-cy=side-panel-button]').click();
 -        cy.get('[data-cy=side-panel-new-project]').click();
 -        cy.get('[data-cy=form-dialog]')
 -            .should('contain', 'New Project')
 +        cy.get("[data-cy=side-panel-button]").click();
 +        cy.get("[data-cy=side-panel-new-project]").click();
 +        cy.get("[data-cy=form-dialog]")
 +            .should("contain", "New Project")
              .within(() => {
 -                cy.get('[data-cy=name-field]').within(() => {
 -                    cy.get('input').type(projName);
 +                cy.get("[data-cy=name-field]").within(() => {
 +                    cy.get("input").type(projName);
                  });
              });
 -        cy.get('[data-cy=form-submit-btn]').click();
 -        cy.get('[data-cy=form-dialog]').should('not.exist');
 +        cy.get("[data-cy=form-submit-btn]").click();
++        cy.get("[data-cy=form-dialog]").should("not.exist");
  
          const editProjectDescription = (name, type) => {
 -            cy.get('[data-cy=side-panel-tree]').contains('Home Projects').click();
 -            cy.get('[data-cy=project-panel] tbody tr').contains(name).rightclick();
 -            cy.get('[data-cy=context-menu]').contains('Edit').click();
 -            cy.get('[data-cy=form-dialog]').within(() => {
 -                cy.get('div[contenteditable=true]')
 -                    .click()
 -                    .type(type);
 -                cy.get('[data-cy=form-submit-btn]').click();
 +            cy.get("[data-cy=side-panel-tree]").contains("Home Projects").click();
 +            cy.get("[data-cy=project-panel] tbody tr").contains(name).rightclick({ force: true });
 +            cy.get("[data-cy=context-menu]").contains("Edit").click();
 +            cy.get("[data-cy=form-dialog]").within(() => {
 +                cy.get("div[contenteditable=true]").click().type(type);
 +                cy.get("[data-cy=form-submit-btn]").click();
              });
          };
  
          const projectName = `Test project (${Math.floor(999999 * Math.random())})`;
  
          cy.loginAs(activeUser);
 -        cy.get('[data-cy=side-panel-button]').click();
 -        cy.get('[data-cy=side-panel-new-project]').click();
 -        cy.get('[data-cy=form-dialog]')
 -            .should('contain', 'New Project')
 +        cy.get("[data-cy=side-panel-button]").click();
 +        cy.get("[data-cy=side-panel-new-project]").click();
 +        cy.get("[data-cy=form-dialog]")
 +            .should("contain", "New Project")
              .within(() => {
 -                cy.get('[data-cy=name-field]').within(() => {
 -                    cy.get('input').type(projectName);
 +                cy.get("[data-cy=name-field]").within(() => {
 +                    cy.get("input").type(projectName);
                  });
 -                cy.get('[data-cy=form-submit-btn]').click();
 +                cy.get("[data-cy=form-submit-btn]").click();
              });
 -        cy.get('[data-cy=form-dialog]').should("not.exist");
 -        cy.get('[data-cy=snackbar]').contains('created');
 -        cy.get('[data-cy=snackbar]').should("not.exist");
 -        cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
 +        cy.get("[data-cy=form-dialog]").should("not.exist");
++        cy.get("[data-cy=snackbar]").contains("created");
++        cy.get("[data-cy=snackbar]").should("not.exist");
 +        cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
+         cy.waitForDom();
 -        cy.get('[data-cy=project-panel]').contains(projectName).should('be.visible').rightclick();
 -        cy.get('[data-cy=context-menu]').contains('Copy to clipboard').click();
 -        cy.window().then((win) => (
 -            win.navigator.clipboard.readText().then((text) => {
 -                expect(text).to.match(/https\:\/\/127\.0\.0\.1\:[0-9]+\/projects\/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/,);
 +        cy.get("[data-cy=project-panel]").contains(projectName).should("be.visible").rightclick();
 +        cy.get("[data-cy=context-menu]").contains("Copy to clipboard").click();
 +        cy.window().then(win =>
 +            win.navigator.clipboard.readText().then(text => {
-                 expect(text).to.match(/https\:\/\/localhost\:[0-9]+\/projects\/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/);
++                expect(text).to.match(/https\:\/\/127\.0\.0\.1\:[0-9]+\/projects\/[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}/);
              })
 -        ));
 -
 +        );
      });
  
 -    it('sorts displayed items correctly', () => {
 +    it("sorts displayed items correctly", () => {
          cy.loginAs(activeUser);
  
          cy.get('[data-cy=project-panel] button[title="Select columns"]').click();
index bfcca0c658cb8cdac4f5bc3f4e26ed3eadde05e1,085298dcd0e05e1c485167c83f5b13a4291e6c27..d8aa35d3d2d4b6398282350f9d68e88ccb5a2030
@@@ -165,23 -160,23 +165,23 @@@ describe("Search tests", function () 
          });
      });
  
 -    it('shows search context menu', function() {
 +    it("shows search context menu", function () {
-         const colName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
-         const federatedColName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+         const colName = `Home Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+         const federatedColName = `Federated Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
          const federatedColUuid = "xxxxx-4zz18-000000000000000";
  
          // Intercept config to insert remote cluster
-         cy.intercept({ method: "GET", hostname: "localhost", url: "**/arvados/v1/config?nocache=*" }, req => {
 -        cy.intercept({method: 'GET', hostname: '127.0.0.1', url: '**/arvados/v1/config?nocache=*'}, (req) => {
 -            req.reply((res) => {
++        cy.intercept({ method: "GET", hostname: "127.0.0.1", url: "**/arvados/v1/config?nocache=*" }, req => {
 +            req.reply(res => {
                  res.body.RemoteClusters = {
                      "*": res.body.RemoteClusters["*"],
 -                    "xxxxx": {
 -                        "ActivateUsers": true,
 -                        "Host": "xxxxx.fakecluster.tld",
 -                        "Insecure": false,
 -                        "Proxy": true,
 -                        "Scheme": ""
 -                    }
 +                    xxxxx: {
 +                        ActivateUsers: true,
 +                        Host: "xxxxx.fakecluster.tld",
 +                        Insecure: false,
 +                        Proxy: true,
 +                        Scheme: "",
 +                    },
                  };
              });
          });
              });
  
              // Check copy to clipboard
 -            cy.get('[data-cy=search-results]').contains(colName).rightclick();
 -            cy.get('[data-cy=context-menu]').within((ctx) => {
 +            cy.get("[data-cy=search-results]").contains(colName).rightclick();
 +            cy.get("[data-cy=context-menu]").within(ctx => {
                  // Check that there are 4 items in the menu
 -                cy.get(ctx).children().should('have.length', 4);
 -                cy.contains('API Details');
 -                cy.contains('Copy to clipboard');
 -                cy.contains('Open in new tab');
 -                cy.contains('View details');
 +                cy.get(ctx).children().should("have.length", 4);
 +                cy.contains("API Details");
 +                cy.contains("Copy to clipboard");
 +                cy.contains("Open in new tab");
 +                cy.contains("View details");
  
 -                cy.contains('Copy to clipboard').click();
 +                cy.contains("Copy to clipboard").click();
+                 cy.waitForDom();
 -                cy.window().then((win) => (
 -                    win.navigator.clipboard.readText().then((text) => {
 +                cy.window().then(win =>
 +                    win.navigator.clipboard.readText().then(text => {
                          expect(text).to.match(new RegExp(`/collections/${testCollection.uuid}$`));
                      })
 -                ));
 +                );
              });
  
              // Check open in new tab
 -            cy.get('[data-cy=search-results]').contains(colName).rightclick();
 -            cy.get('[data-cy=context-menu]').within(() => {
 -                cy.contains('Open in new tab').click();
 +            cy.get("[data-cy=search-results]").contains(colName).rightclick();
 +            cy.get("[data-cy=context-menu]").within(() => {
 +                cy.contains("Open in new tab").click();
+                 cy.waitForDom();
 -                cy.get('@Open').should('have.been.calledOnceWith', `${window.location.origin}/collections/${testCollection.uuid}`)
 +                cy.get("@Open").should("have.been.calledOnceWith", `${window.location.origin}/collections/${testCollection.uuid}`);
              });
  
              // Check federated result copy to clipboard
 -            cy.get('[data-cy=search-results]').contains(federatedColName).rightclick();
 -            cy.get('[data-cy=context-menu]').within(() => {
 -                cy.contains('Copy to clipboard').click();
 +            cy.get("[data-cy=search-results]").contains(federatedColName).rightclick();
 +            cy.get("[data-cy=context-menu]").within(() => {
 +                cy.contains("Copy to clipboard").click();
+                 cy.waitForDom();
 -                cy.window().then((win) => (
 -                    win.navigator.clipboard.readText().then((text) => {
 +                cy.window().then(win =>
 +                    win.navigator.clipboard.readText().then(text => {
                          expect(text).to.equal(`https://wb2.xxxxx.fakecluster.tld/collections/${federatedColUuid}`);
                      })
 -                ));
 +                );
              });
              // Check open in new tab
 -            cy.get('[data-cy=search-results]').contains(federatedColName).rightclick();
 -            cy.get('[data-cy=context-menu]').within(() => {
 -                cy.contains('Open in new tab').click();
 +            cy.get("[data-cy=search-results]").contains(federatedColName).rightclick();
 +            cy.get("[data-cy=context-menu]").within(() => {
 +                cy.contains("Open in new tab").click();
+                 cy.waitForDom();
 -                cy.get('@Open').should('have.been.calledWith', `https://wb2.xxxxx.fakecluster.tld/collections/${federatedColUuid}`)
 +                cy.get("@Open").should("have.been.calledWith", `https://wb2.xxxxx.fakecluster.tld/collections/${federatedColUuid}`);
              });
 -
          });
      });
  });
index f6a508772f86526aab1e941420a30a27fa733df0,80d649777bfb436d6f9052f17dd74ccbfd0d63ae..92011b208ee51a7887996ca4088c60b39838aad0
@@@ -40,53 -40,52 +40,53 @@@ describe("Virtual machine login manage 
              .within(() => {
                  cy.get('button[title="Add Login Permission"]').click();
              });
 -        cy.get('[data-cy=form-dialog]')
 -            .should('contain', 'Add login permission')
 +        cy.get("[data-cy=form-dialog]")
 +            .should("contain", "Add login permission")
              .within(() => {
 -                cy.get('label')
 -                  .contains('Search for user')
 -                  .parent()
 -                  .within(() => {
 -                    cy.get('input').type('VMAdmin');
 -                  })
 +                cy.get("label")
 +                    .contains("Search for user")
 +                    .parent()
 +                    .within(() => {
 +                        cy.get("input").type("VMAdmin");
 +                    });
              });
-         cy.get("[role=tooltip]").click();
 -        cy.waitForDom().get('[role=tooltip]').click();
 -        cy.get('[data-cy=form-dialog]').as('add-login-dialog')
 -            .should('contain', 'Add login permission')
++        cy.waitForDom().get("[role=tooltip]").click();
 +        cy.get("[data-cy=form-dialog]")
 +            .as("add-login-dialog")
 +            .should("contain", "Add login permission")
              .within(() => {
 -                cy.get('label')
 -                  .contains('Add groups')
 -                  .parent()
 -                  .within(() => {
 -                    cy.get('input').type('docker ');
 -                    // Veryfy submit enabled (form has changed)
 -                    cy.get('@add-login-dialog').within(() => {
 -                        cy.get('[data-cy=form-submit-btn]').should('be.enabled');
 -                    });
 -                    cy.get('input').type('sudo');
 -                    // Veryfy submit disabled (partial input in chips)
 -                    cy.get('@add-login-dialog').within(() => {
 -                        cy.get('[data-cy=form-submit-btn]').should('be.disabled');
 +                cy.get("label")
 +                    .contains("Add groups")
 +                    .parent()
 +                    .within(() => {
 +                        cy.get("input").type("docker ");
 +                        // Veryfy submit enabled (form has changed)
 +                        cy.get("@add-login-dialog").within(() => {
 +                            cy.get("[data-cy=form-submit-btn]").should("be.enabled");
 +                        });
 +                        cy.get("input").type("sudo");
 +                        // Veryfy submit disabled (partial input in chips)
 +                        cy.get("@add-login-dialog").within(() => {
 +                            cy.get("[data-cy=form-submit-btn]").should("be.disabled");
 +                        });
 +                        cy.get("input").type("{enter}");
                      });
 -                    cy.get('input').type('{enter}');
 -                  })
              });
 -        cy.get('[data-cy=form-dialog]').within(() => {
 -            cy.get('[data-cy=form-submit-btn]').click();
 +        cy.get("[data-cy=form-dialog]").within(() => {
 +            cy.get("[data-cy=form-submit-btn]").click();
          });
  
 -        cy.get('[data-cy=vm-admin-table]')
 +        cy.get("[data-cy=vm-admin-table]")
              .contains(vmHost)
 -            .parents('tr')
 +            .parents("tr")
              .within(() => {
 -                cy.get('td').contains('admin');
 -        });
 +                cy.get("td").contains("admin");
 +            });
  
          // Add login permission to activeUser
 -        cy.get('[data-cy=vm-admin-table]')
 +        cy.get("[data-cy=vm-admin-table]")
              .contains(vmHost)
 -            .parents('tr')
 +            .parents("tr")
              .within(() => {
                  cy.get('button[title="Add Login Permission"]').click();
              });
          // Remove login permissions
          cy.loginAs(adminUser);
          cy.get('header button[title="Admin Panel"]').click();
 -        cy.get('#admin-menu').contains('Virtual Machines').click();
 +        cy.get("#admin-menu").contains("Virtual Machines").click();
  
 -        cy.get('[data-cy=vm-admin-table]')
 -            .contains('user'); // Wait for page to finish
 +        cy.get("[data-cy=vm-admin-table]").contains("user"); // Wait for page to finish
  
 -        cy.get('[data-cy=vm-admin-table]')
 +        cy.get("[data-cy=vm-admin-table]")
              .contains(vmHost)
 -            .parents('tr')
 -            .as('vmRow')
 -            .contains('user')
 -            .parents('[role=button]')
 -            .find('svg')
 -            .as('removeButton');
 -        cy.get('@removeButton').click();
 -        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
 -
 -        cy.get('@vmRow')
 -            .within(() => {
 -                cy.get('div[role=button]').should('not.contain', 'user');
 -                cy.get('div[role=button]').should('have.length', 1)
 -            });
 +            .parents("tr")
 +            .as("vmRow")
 +            .contains("user")
 +            .parents("[role=button]")
 +            .find("svg")
 +            .as("removeButton");
 +        cy.get("@removeButton").click();
 +        cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
 +
 +        cy.get("@vmRow").within(() => {
 +            cy.get("div[role=button]").should("not.contain", "user");
 +            cy.get("div[role=button]").should("have.length", 1);
 +        });
  
 -        cy.get('@vmRow')
 -            .find('div[role=button]')
 -            .contains('admin')
 -            .parents('[role=button]')
 -            .find('svg')
 -            .as('removeButton');
 -        cy.get('@removeButton').click();
 -        cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
 +        cy.get("@vmRow").find("div[role=button]").contains("admin").parents("[role=button]").find("svg").as("removeButton");
 +        cy.get("@removeButton").click();
 +        cy.get("[data-cy=confirmation-dialog-ok-btn]").click();
  
-         cy.get("[data-cy=vm-admin-table]")
 -        cy.waitForDom().get('[data-cy=vm-admin-table]')
++        cy.waitForDom()
++            .get("[data-cy=vm-admin-table]")
              .contains(vmHost)
 -            .parents('tr')
 +            .parents("tr")
              .within(() => {
 -                cy.get('div[role=button]').should('not.exist');
 +                cy.get("div[role=button]").should("not.exist");
              });
  
          // Check admin's vm page for login
index 199b5c13c570932b2ce5d5a2c0bc068da8a0577f,67ddf45d8365bc77b769d4b7a857666d23af00ab..46d77fe8b19721a5b87a63fab7900a20802cb7f9
  // -- This will overwrite an existing command --
  // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... })
  
 -const controllerURL = Cypress.env('controller_url');
 -const systemToken = Cypress.env('system_token');
+ import { extractFilesData } from "services/collection-service/collection-service-files-response";
 +const controllerURL = Cypress.env("controller_url");
 +const systemToken = Cypress.env("system_token");
  let createdResources = [];
  
- // Clean up on a 'before' hook to allow post-mortem analysis on individual tests.
- beforeEach(function () {
 -const containerLogFolderPrefix = 'log for container ';
++const containerLogFolderPrefix = "log for container ";
+ // Clean up anything that was created.  You can temporarily add
+ // 'return' to the top if you need the resources to hang around to
+ // debug a specific test.
+ afterEach(function () {
      if (createdResources.length === 0) {
          return;
      }
-     cy.log(`Cleaning ${createdResources.length} previously created resource(s)`);
+     cy.log(`Cleaning ${createdResources.length} previously created resource(s).`);
 -    createdResources.forEach(function({suffix, uuid}) {
 +    createdResources.forEach(function ({ suffix, uuid }) {
          // Don't fail when a resource isn't already there, some objects may have
          // been removed, directly or indirectly, from the test that created them.
          cy.deleteResource(systemToken, suffix, uuid, false);
@@@ -56,290 -77,374 +62,360 @@@ Cypress.Commands.add
              qs: auth ? qs : Object.assign({ api_token: token }, qs),
              auth: auth ? { bearer: `${token}` } : undefined,
              followRedirect: followRedirect,
 -            failOnStatusCode: failOnStatusCode
 +            failOnStatusCode: failOnStatusCode,
          });
 -    });
 -});
 +    }
 +);
  
 -Cypress.Commands.add(
 -    "getUser", (username, first_name = '', last_name = '', is_admin = false, is_active = true) => {
 -        // Create user if not already created
 -        return cy.doRequest('POST', '/auth/controller/callback', {
 -            auth_info: JSON.stringify({
 -                email: `${username}@example.local`,
 -                username: username,
 -                first_name: first_name,
 -                last_name: last_name,
 -                alternate_emails: []
 -            }),
 -            return_to: ',https://example.local'
 -        }, null, systemToken, true, false) // Don't follow redirects so we can catch the token
 -        .its('headers.location').as('location')
 -        // Get its token and set the account up as admin and/or active
 -        .then(function () {
 -            this.userToken = this.location.split("=")[1]
 -            assert.isString(this.userToken)
 -            return cy.doRequest('GET', '/arvados/v1/users', null, {
 -                filters: `[["username", "=", "${username}"]]`
 -            })
 -            .its('body.items.0').as('aUser')
 +Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => {
 +    // Create user if not already created
 +    return (
 +        cy
 +            .doRequest(
 +                "POST",
 +                "/auth/controller/callback",
 +                {
 +                    auth_info: JSON.stringify({
 +                        email: `${username}@example.local`,
 +                        username: username,
 +                        first_name: first_name,
 +                        last_name: last_name,
 +                        alternate_emails: [],
 +                    }),
 +                    return_to: ",https://example.local",
 +                },
 +                null,
 +                systemToken,
 +                true,
 +                false
 +            ) // Don't follow redirects so we can catch the token
 +            .its("headers.location")
 +            .as("location")
 +            // Get its token and set the account up as admin and/or active
              .then(function () {
 -                cy.doRequest('PUT', `/arvados/v1/users/${this.aUser.uuid}`, {
 -                    user: {
 -                        is_admin: is_admin,
 -                        is_active: is_active
 -                    }
 -                })
 -                .its('body').as('theUser')
 -                .then(function () {
 -                    cy.doRequest('GET', '/arvados/v1/api_clients', null, {
 -                        filters: `[["is_trusted", "=", false]]`,
 -                        order: `["created_at desc"]`
 +                this.userToken = this.location.split("=")[1];
 +                assert.isString(this.userToken);
 +                return cy
 +                    .doRequest("GET", "/arvados/v1/users", null, {
 +                        filters: `[["username", "=", "${username}"]]`,
                      })
 -                    .its('body.items').as('apiClients')
 +                    .its("body.items.0")
 +                    .as("aUser")
                      .then(function () {
 -                        if (this.apiClients.length > 0) {
 -                            cy.doRequest('PUT', `/arvados/v1/api_clients/${this.apiClients[0].uuid}`, {
 -                                api_client: {
 -                                    is_trusted: true
 -                                }
 -                            })
 -                            .its('body').as('updatedApiClient')
 -                            .then(function() {
 -                                assert(this.updatedApiClient.is_trusted);
 -                            })
 -                        }
 -                    })
 -                    .then(function () {
 -                        return { user: this.theUser, token: this.userToken };
 -                    })
 -                })
 +                        cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, {
 +                            user: {
 +                                is_admin: is_admin,
 +                                is_active: is_active,
 +                            },
 +                        })
 +                            .its("body")
 +                            .as("theUser")
 +                            .then(function () {
 +                                cy.doRequest("GET", "/arvados/v1/api_clients", null, {
 +                                    filters: `[["is_trusted", "=", false]]`,
 +                                    order: `["created_at desc"]`,
 +                                })
 +                                    .its("body.items")
 +                                    .as("apiClients")
 +                                    .then(function () {
 +                                        if (this.apiClients.length > 0) {
 +                                            cy.doRequest("PUT", `/arvados/v1/api_clients/${this.apiClients[0].uuid}`, {
 +                                                api_client: {
 +                                                    is_trusted: true,
 +                                                },
 +                                            })
 +                                                .its("body")
 +                                                .as("updatedApiClient")
 +                                                .then(function () {
 +                                                    assert(this.updatedApiClient.is_trusted);
 +                                                });
 +                                        }
 +                                    })
 +                                    .then(function () {
 +                                        return { user: this.theUser, token: this.userToken };
 +                                    });
 +                            });
 +                    });
              })
 -        })
 -    }
 -)
 +    );
 +});
  
 -Cypress.Commands.add(
 -    "createLink", (token, data) => {
 -        return cy.createResource(token, 'links', {
 -            link: JSON.stringify(data)
 -        })
 -    }
 -)
 +Cypress.Commands.add("createLink", (token, data) => {
 +    return cy.createResource(token, "links", {
 +        link: JSON.stringify(data),
 +    });
 +});
  
- Cypress.Commands.add("createGroup", (token, data) => {
-     return cy.createResource(token, "groups", {
-         group: JSON.stringify(data),
-         ensure_unique_name: true,
-     });
- });
- Cypress.Commands.add("trashGroup", (token, uuid) => {
-     return cy.deleteResource(token, "groups", uuid);
- });
- Cypress.Commands.add("createWorkflow", (token, data) => {
-     return cy.createResource(token, "workflows", {
-         workflow: JSON.stringify(data),
-         ensure_unique_name: true,
-     });
- });
- Cypress.Commands.add("getCollection", (token, uuid) => {
-     return cy.getResource(token, "collections", uuid);
- });
- Cypress.Commands.add("createCollection", (token, data) => {
-     return cy.createResource(token, "collections", {
-         collection: JSON.stringify(data),
-         ensure_unique_name: true,
-     });
- });
- Cypress.Commands.add("updateCollection", (token, uuid, data) => {
-     return cy.updateResource(token, "collections", uuid, {
-         collection: JSON.stringify(data),
-     });
- });
- Cypress.Commands.add("getContainer", (token, uuid) => {
-     return cy.getResource(token, "containers", uuid);
- });
- Cypress.Commands.add("updateContainer", (token, uuid, data) => {
-     return cy.updateResource(token, "containers", uuid, {
-         container: JSON.stringify(data),
-     });
- });
- Cypress.Commands.add("createContainerRequest", (token, data) => {
-     return cy.createResource(token, "container_requests", {
-         container_request: JSON.stringify(data),
-         ensure_unique_name: true,
-     });
- });
- Cypress.Commands.add("updateContainerRequest", (token, uuid, data) => {
-     return cy.updateResource(token, "container_requests", uuid, {
-         container_request: JSON.stringify(data),
-     });
- });
- Cypress.Commands.add("createLog", (token, data) => {
-     return cy.createResource(token, "logs", {
-         log: JSON.stringify(data),
-     });
- });
+ Cypress.Commands.add(
 -    "createGroup", (token, data) => {
 -        return cy.createResource(token, 'groups', {
 -            group: JSON.stringify(data),
 -            ensure_unique_name: true
 -        })
++    "doWebDAVRequest",
++    (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
++        return cy.doRequest("GET", "/arvados/v1/config", null, null).then(({ body: config }) => {
++            return cy.request({
++                method: method,
++                url: `${config.Services.WebDAVDownload.ExternalURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`,
++                body: data,
++                qs: auth ? qs : Object.assign({ api_token: token }, qs),
++                auth: auth ? { bearer: `${token}` } : undefined,
++                followRedirect: followRedirect,
++                failOnStatusCode: failOnStatusCode,
++            });
++        });
+     }
 -)
++);
  
- Cypress.Commands.add("logsForContainer", (token, uuid, logType, logTextArray = []) => {
-     let logs = [];
-     for (const logText of logTextArray) {
-         logs.push(
-             cy
-                 .createLog(token, {
-                     object_uuid: uuid,
-                     event_type: logType,
-                     properties: {
-                         text: logText,
-                     },
 -Cypress.Commands.add(
 -    "trashGroup", (token, uuid) => {
 -        return cy.deleteResource(token, 'groups', uuid);
 -    }
 -)
++Cypress.Commands.add("getUser", (username, first_name = "", last_name = "", is_admin = false, is_active = true) => {
++    // Create user if not already created
++    return (
++        cy
++            .doRequest(
++                "POST",
++                "/auth/controller/callback",
++                {
++                    auth_info: JSON.stringify({
++                        email: `${username}@example.local`,
++                        username: username,
++                        first_name: first_name,
++                        last_name: last_name,
++                        alternate_emails: [],
++                    }),
++                    return_to: ",https://example.local",
++                },
++                null,
++                systemToken,
++                true,
++                false
++            ) // Don't follow redirects so we can catch the token
++            .its("headers.location")
++            .as("location")
++            // Get its token and set the account up as admin and/or active
++            .then(
++                function () {
++                    this.userToken = this.location.split("=")[1];
++                    assert.isString(this.userToken);
++                    return cy
++                        .doRequest("GET", "/arvados/v1/users", null, {
++                            filters: `[["username", "=", "${username}"]]`,
++                        })
++                        .its("body.items.0")
++                        .as("aUser")
++                        .then(function () {
++                            cy.doRequest("PUT", `/arvados/v1/users/${this.aUser.uuid}`, {
++                                user: {
++                                    is_admin: is_admin,
++                                    is_active: is_active,
++                                },
++                            }).as("lastLogRecord");
++                        });
++                },
++                cy.getAll("@lastLogRecord").then(function () {
++                    return logs;
 +                })
-                 .as("lastLogRecord")
-         );
-     }
-     cy.getAll("@lastLogRecord").then(function () {
-         return logs;
-     });
++            )
++    );
 +});
  
 +Cypress.Commands.add("createVirtualMachine", (token, data) => {
 +    return cy.createResource(token, "virtual_machines", {
 +        virtual_machine: JSON.stringify(data),
 +        ensure_unique_name: true,
 +    });
 +});
  
 -Cypress.Commands.add(
 -    "createWorkflow", (token, data) => {
 -        return cy.createResource(token, 'workflows', {
 -            workflow: JSON.stringify(data),
 -            ensure_unique_name: true
 -        })
 -    }
 -)
 +Cypress.Commands.add("getResource", (token, suffix, uuid) => {
 +    return cy
 +        .doRequest("GET", `/arvados/v1/${suffix}/${uuid}`, null, {}, token)
 +        .its("body")
 +        .then(function (resource) {
 +            return resource;
 +        });
 +});
  
 -Cypress.Commands.add(
 -    "createCollection", (token, data) => {
 -        return cy.createResource(token, 'collections', {
 -            collection: JSON.stringify(data),
 -            ensure_unique_name: true
 -        })
 -    }
 -)
 +Cypress.Commands.add("createResource", (token, suffix, data) => {
 +    return cy
 +        .doRequest("POST", "/arvados/v1/" + suffix, data, null, token, true)
 +        .its("body")
 +        .then(function (resource) {
 +            createdResources.push({ suffix, uuid: resource.uuid });
 +            return resource;
 +        });
 +});
  
 -Cypress.Commands.add(
 -    "getCollection", (token, uuid) => {
 -        return cy.getResource(token, 'collections', uuid)
 -    }
 -)
 +Cypress.Commands.add("deleteResource", (token, suffix, uuid, failOnStatusCode = true) => {
 +    return cy
 +        .doRequest("DELETE", "/arvados/v1/" + suffix + "/" + uuid, null, null, token, false, true, failOnStatusCode)
 +        .its("body")
 +        .then(function (resource) {
 +            return resource;
 +        });
 +});
  
 -Cypress.Commands.add(
 -    "updateCollection", (token, uuid, data) => {
 -        return cy.updateResource(token, 'collections', uuid, {
 -            collection: JSON.stringify(data)
 -        })
 -    }
 -)
 +Cypress.Commands.add("updateResource", (token, suffix, uuid, data) => {
 +    return cy
 +        .doRequest("PATCH", "/arvados/v1/" + suffix + "/" + uuid, data, null, token, true)
 +        .its("body")
 +        .then(function (resource) {
 +            return resource;
 +        });
 +});
  
- Cypress.Commands.add("loginAs", user => {
-     cy.clearCookies();
-     cy.clearLocalStorage();
-     cy.visit(`/token/?api_token=${user.token}`);
-     cy.url({ timeout: 10000 }).should("contain", "/projects/");
-     cy.get("div#root").should("contain", "Arvados Workbench (zzzzz)");
-     cy.get("div#root").should("not.contain", "Your account is inactive");
 -Cypress.Commands.add(
 -    "collectionReplaceFiles", (token, uuid, data) => {
 -        return cy.updateResource(token, 'collections', uuid, {
 -            collection: {
 -                preserve_version: true,
 -            },
 -            replace_files: JSON.stringify(data)
 -        })
 -    }
 -)
++Cypress.Commands.add("createCollection", (token, data) => {
++    return cy.createResource(token, "collections", {
++        collection: JSON.stringify(data),
++        ensure_unique_name: true,
++    });
++});
 -Cypress.Commands.add(
 -    "getContainer", (token, uuid) => {
 -        return cy.getResource(token, 'containers', uuid)
 -    }
 -)
++Cypress.Commands.add("getCollection", (token, uuid) => {
++    return cy.getResource(token, "collections", uuid);
++});
 -Cypress.Commands.add(
 -    "updateContainer", (token, uuid, data) => {
 -        return cy.updateResource(token, 'containers', uuid, {
 -            container: JSON.stringify(data)
 -        })
 -    }
 -)
++Cypress.Commands.add("updateCollection", (token, uuid, data) => {
++    return cy.updateResource(token, "collections", uuid, {
++        collection: JSON.stringify(data),
++    });
++});
 -Cypress.Commands.add(
 -    "getContainerRequest", (token, uuid) => {
 -        return cy.getResource(token, 'container_requests', uuid)
 -    }
 -)
++Cypress.Commands.add("collectionReplaceFiles", (token, uuid, data) => {
++    return cy.updateResource(token, "collections", uuid, {
++        collection: {
++            preserve_version: true,
++        },
++        replace_files: JSON.stringify(data),
++    });
 +});
  
- Cypress.Commands.add("testEditProjectOrCollection", (container, oldName, newName, newDescription, isProject = true) => {
-     cy.get(container).contains(oldName).rightclick();
-     cy.get("[data-cy=context-menu]")
-         .contains(isProject ? "Edit project" : "Edit collection")
-         .click();
-     cy.get("[data-cy=form-dialog]").within(() => {
-         cy.get("input[name=name]").clear().type(newName);
-         cy.get(isProject ? "div[contenteditable=true]" : "input[name=description]")
-             .clear()
-             .type(newDescription);
-         cy.get("[data-cy=form-submit-btn]").click();
 -Cypress.Commands.add(
 -    'createContainerRequest', (token, data) => {
 -        return cy.createResource(token, 'container_requests', {
 -            container_request: JSON.stringify(data),
 -            ensure_unique_name: true
 -        })
 -    }
 -)
++Cypress.Commands.add("getContainer", (token, uuid) => {
++    return cy.getResource(token, "containers", uuid);
++});
 -Cypress.Commands.add(
 -    "updateContainerRequest", (token, uuid, data) => {
 -        return cy.updateResource(token, 'container_requests', uuid, {
 -            container_request: JSON.stringify(data)
 -        })
 -    }
 -)
++Cypress.Commands.add("updateContainer", (token, uuid, data) => {
++    return cy.updateResource(token, "containers", uuid, {
++        container: JSON.stringify(data),
 +    });
++});
 +
-     cy.get(container).contains(newName).rightclick();
-     cy.get("[data-cy=context-menu]")
-         .contains(isProject ? "Edit project" : "Edit collection")
-         .click();
-     cy.get("[data-cy=form-dialog]").within(() => {
-         cy.get("input[name=name]").should("have.value", newName);
++Cypress.Commands.add("getContainerRequest", (token, uuid) => {
++    return cy.getResource(token, "container_requests", uuid);
++});
 +
-         if (isProject) {
-             cy.get("span[data-text=true]").contains(newDescription);
-         } else {
-             cy.get("input[name=description]").should("have.value", newDescription);
-         }
++Cypress.Commands.add("createContainerRequest", (token, data) => {
++    return cy.createResource(token, "container_requests", {
++        container_request: JSON.stringify(data),
++        ensure_unique_name: true,
++    });
++});
 +
-         cy.get("[data-cy=form-cancel-btn]").click();
++Cypress.Commands.add("updateContainerRequest", (token, uuid, data) => {
++    return cy.updateResource(token, "container_requests", uuid, {
++        container_request: JSON.stringify(data),
 +    });
 +});
  
 -Cypress.Commands.add(
 -    "appendLog", (token, crUuid, fileName, lines = []) => (
 -        cy.getContainerRequest(token, crUuid).then((containerRequest) => {
 -            if (containerRequest.log_uuid) {
 -                cy.listContainerRequestLogs(token, crUuid).then((logFiles) => {
 -                    const filePath = `${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`;
 -                    if (logFiles.find((file) => (file.name === fileName))) {
 -                        // File exists, fetch and append
 -                        return cy.doWebDAVRequest(
 -                                "GET",
 -                                `c=${filePath}`,
 -                                null,
 -                                null,
 -                                token
 -                            )
 -                            .then(({ body: contents }) => cy.doWebDAVRequest(
 -                                "PUT",
 -                                `c=${filePath}`,
 -                                contents.split("\n").concat(lines).join("\n"),
 -                                null,
 -                                token
 -                            ));
 -                    } else {
 -                        // File not exists, put new file
 -                        cy.doWebDAVRequest(
 -                            "PUT",
 -                            `c=${filePath}`,
 -                            lines.join("\n"),
 -                            null,
 -                            token
 -                        )
 -                    }
 -                });
 -            } else {
 -                // Create log collection
 -                return cy.createCollection(token, {
+ /**
+  * Requires an admin token for log_uuid modification to succeed
+  */
 -                    manifest_text: ""
 -                }).then((collection) => {
++Cypress.Commands.add("appendLog", (token, crUuid, fileName, lines = []) =>
++    cy.getContainerRequest(token, crUuid).then(containerRequest => {
++        if (containerRequest.log_uuid) {
++            cy.listContainerRequestLogs(token, crUuid).then(logFiles => {
++                const filePath = `${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`;
++                if (logFiles.find(file => file.name === fileName)) {
++                    // File exists, fetch and append
++                    return cy
++                        .doWebDAVRequest("GET", `c=${filePath}`, null, null, token)
++                        .then(({ body: contents }) =>
++                            cy.doWebDAVRequest("PUT", `c=${filePath}`, contents.split("\n").concat(lines).join("\n"), null, token)
++                        );
++                } else {
++                    // File not exists, put new file
++                    cy.doWebDAVRequest("PUT", `c=${filePath}`, lines.join("\n"), null, token);
++                }
++            });
++        } else {
++            // Create log collection
++            return cy
++                .createCollection(token, {
+                     name: `Test log collection ${Math.floor(Math.random() * 999999)}`,
+                     owner_uuid: containerRequest.owner_uuid,
 -                    }).then(() => (
++                    manifest_text: "",
++                })
++                .then(collection => {
+                     // Update CR log_uuid to fake log collection
+                     cy.updateContainerRequest(token, containerRequest.uuid, {
+                         log_uuid: collection.uuid,
 -                        cy.collectionReplaceFiles(token, collection.uuid, {
 -                            [`/${containerLogFolderPrefix}${containerRequest.container_uuid}`]: "d41d8cd98f00b204e9800998ecf8427e+0"
 -                        }).then(() => (
 -                            // Put new log file with contents into fake log collection
 -                            cy.doWebDAVRequest(
 -                                'PUT',
 -                                `c=${collection.uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`,
 -                                lines.join('\n'),
 -                                null,
 -                                token
++                    }).then(() =>
+                         // Create empty directory for container uuid
 -                        ))
 -                    ));
++                        cy
++                            .collectionReplaceFiles(token, collection.uuid, {
++                                [`/${containerLogFolderPrefix}${containerRequest.container_uuid}`]: "d41d8cd98f00b204e9800998ecf8427e+0",
++                            })
++                            .then(() =>
++                                // Put new log file with contents into fake log collection
++                                cy.doWebDAVRequest(
++                                    "PUT",
++                                    `c=${collection.uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}/${fileName}`,
++                                    lines.join("\n"),
++                                    null,
++                                    token
++                                )
+                             )
 -            }
 -        })
 -    )
 -)
 -
 -Cypress.Commands.add(
 -    "listContainerRequestLogs", (token, crUuid) => (
 -        cy.getContainerRequest(token, crUuid).then((containerRequest) => (
 -            cy.doWebDAVRequest('PROPFIND', `c=${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}`, null, null, token)
 -                .then(({body: data}) => {
 -                    return extractFilesData(new DOMParser().parseFromString(data, "text/xml"));
 -                })
 -        ))
 -    )
++                    );
+                 });
 -Cypress.Commands.add(
 -    "createVirtualMachine", (token, data) => {
 -        return cy.createResource(token, 'virtual_machines', {
 -            virtual_machine: JSON.stringify(data),
 -            ensure_unique_name: true
 -        })
 -    }
 -)
 -
 -Cypress.Commands.add(
 -    "getResource", (token, suffix, uuid) => {
 -        return cy.doRequest('GET', `/arvados/v1/${suffix}/${uuid}`, null, {}, token)
 -            .its('body')
 -            .then(function (resource) {
 -                return resource;
 -            })
 -    }
 -)
 -
 -Cypress.Commands.add(
 -    "createResource", (token, suffix, data) => {
 -        return cy.doRequest('POST', '/arvados/v1/' + suffix, data, null, token, true)
 -            .its('body')
 -            .then(function (resource) {
 -                createdResources.push({suffix, uuid: resource.uuid});
 -                return resource;
++        }
++    })
+ );
 -    }
 -)
 -
 -Cypress.Commands.add(
 -    "deleteResource", (token, suffix, uuid, failOnStatusCode = true) => {
 -        return cy.doRequest('DELETE', '/arvados/v1/' + suffix + '/' + uuid, null, null, token, false, true, failOnStatusCode)
 -            .its('body')
 -            .then(function (resource) {
 -                return resource;
 -            })
 -    }
 -)
 -
 -Cypress.Commands.add(
 -    "updateResource", (token, suffix, uuid, data) => {
 -        return cy.doRequest('PATCH', '/arvados/v1/' + suffix + '/' + uuid, data, null, token, true)
 -            .its('body')
 -            .then(function (resource) {
 -                return resource;
 -            })
 -    }
 -)
 -
 -Cypress.Commands.add(
 -    "loginAs", (user) => {
 -        cy.clearCookies()
 -        cy.clearLocalStorage()
 -        cy.visit(`/token/?api_token=${user.token}`);
 -        cy.url({timeout: 10000}).should('contain', '/projects/');
 -        cy.get('div#root').should('contain', 'Arvados Workbench (zzzzz)');
 -        cy.get('div#root').should('not.contain', 'Your account is inactive');
 -    }
 -)
 -
 -Cypress.Commands.add(
 -    "testEditProjectOrCollection", (container, oldName, newName, newDescription, isProject = true) => {
 -        cy.get(container).contains(oldName).rightclick();
 -        cy.get('[data-cy=context-menu]').contains(isProject ? 'Edit project' : 'Edit collection').click();
 -        cy.get('[data-cy=form-dialog]').within(() => {
 -            cy.get('input[name=name]').clear().type(newName);
 -            cy.get(isProject ? 'div[contenteditable=true]' : 'input[name=description]').clear().type(newDescription);
 -            cy.get('[data-cy=form-submit-btn]').click();
 -        });
 -
 -        cy.get(container).contains(newName).rightclick();
 -        cy.get('[data-cy=context-menu]').contains(isProject ? 'Edit project' : 'Edit collection').click();
 -        cy.get('[data-cy=form-dialog]').within(() => {
 -            cy.get('input[name=name]').should('have.value', newName);
 -
 -            if (isProject) {
 -                cy.get('span[data-text=true]').contains(newDescription);
 -            } else {
 -                cy.get('input[name=description]').should('have.value', newDescription);
 -            }
++Cypress.Commands.add("listContainerRequestLogs", (token, crUuid) =>
++    cy.getContainerRequest(token, crUuid).then(containerRequest =>
++        cy
++            .doWebDAVRequest(
++                "PROPFIND",
++                `c=${containerRequest.log_uuid}/${containerLogFolderPrefix}${containerRequest.container_uuid}`,
++                null,
++                null,
++                token
++            )
++            .then(({ body: data }) => {
++                return extractFilesData(new DOMParser().parseFromString(data, "text/xml"));
+             })
 -            cy.get('[data-cy=form-cancel-btn]').click();
 -        });
 -    }
 -)
++    )
++);
 -Cypress.Commands.add(
 -    "doSearch", (searchTerm) => {
 -        cy.get('[data-cy=searchbar-input-field]').type(`{selectall}${searchTerm}{enter}`);
 -    }
 -)
++cy.get("[data-cy=form-cancel-btn]").click();
 +Cypress.Commands.add("doSearch", searchTerm => {
 +    cy.get("[data-cy=searchbar-input-field]").type(`{selectall}${searchTerm}{enter}`);
 +    cy.get("[data-cy=searchbar-parent-form]").submit();
 +});
  
 -Cypress.Commands.add(
 -    "goToPath", (path) => {
 -        return cy.window().its('appHistory').invoke('push', path);
 -    }
 -)
 +Cypress.Commands.add("goToPath", path => {
 +    return cy.window().its("appHistory").invoke("push", path);
 +});
  
 -Cypress.Commands.add('getAll', (...elements) => {
 -    const promise = cy.wrap([], { log: false })
 +Cypress.Commands.add("getAll", (...elements) => {
 +    const promise = cy.wrap([], { log: false });
  
      for (let element of elements) {
 -        promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got])))
 +        promise.then(arr => cy.get(element).then(got => cy.wrap([...arr, got])));
      }
  
 -    return promise
 -})
 +    return promise;
 +});
  
 -Cypress.Commands.add('shareWith', (srcUserToken, targetUserUUID, itemUUID, permission = 'can_write') => {
 +Cypress.Commands.add("shareWith", (srcUserToken, targetUserUUID, itemUUID, permission = "can_write") => {
      cy.createLink(srcUserToken, {
          name: permission,
 -        link_class: 'permission',
 +        link_class: "permission",
          head_uuid: itemUUID,
 -        tail_uuid: targetUserUUID
 +        tail_uuid: targetUserUUID,
      });
 -})
 +});
  
 -Cypress.Commands.add('addToFavorites', (userToken, userUUID, itemUUID) => {
 +Cypress.Commands.add("addToFavorites", (userToken, userUUID, itemUUID) => {
      cy.createLink(userToken, {
          head_uuid: itemUUID,
 -        link_class: 'star',
 -        name: '',
 +        link_class: "star",
 +        name: "",
          owner_uuid: userUUID,
          tail_uuid: userUUID,
      });
index 0fe933f5738cfaca30ac7b816c873644a0382062,c9d5b65711cfe8f965731f588934fa980e062b8d..83de48dec8fb40552e7d5e3a970b27d087ef6983
@@@ -2,14 -2,14 +2,14 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
 -import React from 'react';
 -import classNames from 'classnames';
 -import { connect } from 'react-redux';
 +import React from "react";
 +import classNames from "classnames";
 +import { connect } from "react-redux";
  import { FixedSizeList } from "react-window";
  import AutoSizer from "react-virtualized-auto-sizer";
 -import servicesProvider from 'common/service-provider';
 -import { DownloadIcon, MoreHorizontalIcon, MoreVerticalIcon } from 'components/icon/icon';
 -import { SearchInput } from 'components/search-input/search-input';
 +import servicesProvider from "common/service-provider";
- import { CustomizeTableIcon, DownloadIcon, MoreOptionsIcon } from "components/icon/icon";
++import { DownloadIcon, MoreHorizontalIcon, MoreVerticalIcon } from "components/icon/icon";
 +import { SearchInput } from "components/search-input/search-input";
  import {
      ListItemIcon,
      StyleRulesCallback,
      Checkbox,
      CircularProgress,
      Button,
 -} from '@material-ui/core';
 -import { FileTreeData } from '../file-tree/file-tree-data';
 -import { TreeItem, TreeItemStatus } from '../tree/tree';
 -import { RootState } from 'store/store';
 -import { WebDAV, WebDAVRequestConfig } from 'common/webdav';
 -import { AuthState } from 'store/auth/auth-reducer';
 -import { extractFilesData } from 'services/collection-service/collection-service-files-response';
 -import {
 -    DefaultIcon,
 -    DirectoryIcon,
 -    FileIcon,
 -    BackIcon,
 -    SidePanelRightArrowIcon
 -} from 'components/icon/icon';
 -import { setCollectionFiles } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
 -import { sortBy } from 'lodash';
 -import { formatFileSize } from 'common/formatters';
 -import { getInlineFileUrl, sanitizeToken } from 'views-components/context-menu/actions/helpers';
 -import { extractUuidKind, ResourceKind } from 'models/resource';
 +} from "@material-ui/core";
 +import { FileTreeData } from "../file-tree/file-tree-data";
 +import { TreeItem, TreeItemStatus } from "../tree/tree";
 +import { RootState } from "store/store";
 +import { WebDAV, WebDAVRequestConfig } from "common/webdav";
 +import { AuthState } from "store/auth/auth-reducer";
 +import { extractFilesData } from "services/collection-service/collection-service-files-response";
 +import { DefaultIcon, DirectoryIcon, FileIcon, BackIcon, SidePanelRightArrowIcon } from "components/icon/icon";
 +import { setCollectionFiles } from "store/collection-panel/collection-panel-files/collection-panel-files-actions";
 +import { sortBy } from "lodash";
 +import { formatFileSize } from "common/formatters";
 +import { getInlineFileUrl, sanitizeToken } from "views-components/context-menu/actions/helpers";
++import { extractUuidKind, ResourceKind } from "models/resource";
  
  export interface CollectionPanelFilesProps {
      isWritable: boolean;
@@@ -220,470 -226,373 +221,486 @@@ const styles: StyleRulesCallback<CssRul
  
  const pathPromise = {};
  
 -export const CollectionPanelFiles = withStyles(styles)(connect((state: RootState) => ({
 -    auth: state.auth,
 -    collectionPanel: state.collectionPanel,
 -    collectionPanelFiles: state.collectionPanelFiles,
 -}))((props: CollectionPanelFilesProps & WithStyles<CssRules> & { auth: AuthState }) => {
 -    const { classes, onItemMenuOpen, onUploadDataClick, isWritable, dispatch, collectionPanelFiles, collectionPanel } = props;
 -    const { apiToken, config } = props.auth;
 -
 -    const webdavClient = new WebDAV({
 -        baseURL: config.keepWebServiceUrl,
 -        headers: {
 -            Authorization: `Bearer ${apiToken}`
 -        },
 -    });
 +export const CollectionPanelFiles = withStyles(styles)(
 +    connect((state: RootState) => ({
 +        auth: state.auth,
 +        collectionPanel: state.collectionPanel,
 +        collectionPanelFiles: state.collectionPanelFiles,
 +    }))((props: CollectionPanelFilesProps & WithStyles<CssRules> & { auth: AuthState }) => {
 +        const { classes, onItemMenuOpen, onUploadDataClick, isWritable, dispatch, collectionPanelFiles, collectionPanel } = props;
 +        const { apiToken, config } = props.auth;
 +
 +        const webdavClient = new WebDAV({
 +            baseURL: config.keepWebServiceUrl,
 +            headers: {
 +                Authorization: `Bearer ${apiToken}`,
 +            },
 +        });
 +
 +        const webDAVRequestConfig: WebDAVRequestConfig = {
 +            headers: {
 +                Depth: "1",
 +            },
 +        };
  
 -    const webDAVRequestConfig: WebDAVRequestConfig = {
 -        headers: {
 -            Depth: '1',
 -        },
 -    };
 -
 -    const parentRef = React.useRef(null);
 -    const [path, setPath] = React.useState<string[]>([]);
 -    const [pathData, setPathData] = React.useState({});
 -    const [isLoading, setIsLoading] = React.useState(false);
 -    const [leftSearch, setLeftSearch] = React.useState('');
 -    const [rightSearch, setRightSearch] = React.useState('');
 -
 -    const leftKey = (path.length > 1 ? path.slice(0, path.length - 1) : path).join('/');
 -    const rightKey = path.join('/');
 -
 -    const leftData = pathData[leftKey] || [];
 -    const rightData = pathData[rightKey];
 -
 -    React.useEffect(() => {
 -        if (props.currentItemUuid && extractUuidKind(props.currentItemUuid) === ResourceKind.COLLECTION) {
 -            setPathData({});
 -            setPath([props.currentItemUuid]);
 -        }
 -    }, [props.currentItemUuid]);
 -
 -    const fetchData = (keys, ignoreCache = false) => {
 -        const keyArray = Array.isArray(keys) ? keys : [keys];
 -
 -        Promise.all(keyArray.filter(key => !!key)
 -            .map((key) => {
 -                const dataExists = !!pathData[key];
 -                const runningRequest = pathPromise[key];
 -
 -                if (ignoreCache || (!dataExists && !runningRequest)) {
 -                    if (!isLoading) {
 -                        setIsLoading(true);
 -                    }
 +        const parentRef = React.useRef(null);
 +        const [path, setPath] = React.useState<string[]>([]);
 +        const [pathData, setPathData] = React.useState({});
 +        const [isLoading, setIsLoading] = React.useState(false);
 +        const [leftSearch, setLeftSearch] = React.useState("");
 +        const [rightSearch, setRightSearch] = React.useState("");
  
 -                    pathPromise[key] = true;
 +        const leftKey = (path.length > 1 ? path.slice(0, path.length - 1) : path).join("/");
 +        const rightKey = path.join("/");
  
 -                    return webdavClient.propfind(`c=${key}`, webDAVRequestConfig);
 -                }
 +        const leftData = pathData[leftKey] || [];
 +        const rightData = pathData[rightKey];
  
 -                return Promise.resolve(null);
 -            })
 -            .filter((promise) => !!promise)
 -        )
 -            .then((requests) => {
 -                const newState = requests.map((request, index) => {
 -                    if (request && request.responseXML != null) {
 -                        const key = keyArray[index];
 -                        const result: any = extractFilesData(request.responseXML);
 -                        const sortedResult = sortBy(result, (n) => n.name).sort((n1, n2) => {
 -                            if (n1.type === 'directory' && n2.type !== 'directory') {
 -                                return -1;
 +        React.useEffect(() => {
-             if (props.currentItemUuid) {
++            if (props.currentItemUuid && extractUuidKind(props.currentItemUuid) === ResourceKind.COLLECTION) {
 +                setPathData({});
 +                setPath([props.currentItemUuid]);
 +            }
 +        }, [props.currentItemUuid]);
 +
 +        const fetchData = (keys, ignoreCache = false) => {
 +            const keyArray = Array.isArray(keys) ? keys : [keys];
 +
 +            Promise.all(
 +                keyArray
 +                    .filter(key => !!key)
 +                    .map(key => {
 +                        const dataExists = !!pathData[key];
 +                        const runningRequest = pathPromise[key];
 +
 +                        if (ignoreCache || (!dataExists && !runningRequest)) {
 +                            if (!isLoading) {
 +                                setIsLoading(true);
                              }
 -                            if (n1.type !== 'directory' && n2.type === 'directory') {
 -                                return 1;
 +
 +                            pathPromise[key] = true;
 +
 +                            return webdavClient.propfind(`c=${key}`, webDAVRequestConfig);
 +                        }
 +
 +                        return Promise.resolve(null);
 +                    })
 +                    .filter(promise => !!promise)
 +            )
 +                .then(requests => {
 +                    const newState = requests
 +                        .map((request, index) => {
 +                            if (request && request.responseXML != null) {
 +                                const key = keyArray[index];
 +                                const result: any = extractFilesData(request.responseXML);
 +                                const sortedResult = sortBy(result, n => n.name).sort((n1, n2) => {
 +                                    if (n1.type === "directory" && n2.type !== "directory") {
 +                                        return -1;
 +                                    }
 +                                    if (n1.type !== "directory" && n2.type === "directory") {
 +                                        return 1;
 +                                    }
 +                                    return 0;
 +                                });
 +
 +                                return { [key]: sortedResult };
                              }
 -                            return 0;
 -                        });
 +                            return {};
 +                        })
 +                        .reduce((prev, next) => {
 +                            return { ...next, ...prev };
 +                        }, {});
 +                    setPathData(state => ({ ...state, ...newState }));
 +                })
 +                .finally(() => {
 +                    setIsLoading(false);
 +                    keyArray.forEach(key => delete pathPromise[key]);
 +                });
 +        };
  
 -                        return { [key]: sortedResult };
 -                    }
 -                    return {};
 -                }).reduce((prev, next) => {
 -                    return { ...next, ...prev };
 -                }, {});
 -                setPathData((state) => ({ ...state, ...newState }));
 -            })
 -            .finally(() => {
 -                setIsLoading(false);
 -                keyArray.forEach(key => delete pathPromise[key]);
 -            });
 -    };
 -
 -    React.useEffect(() => {
 -        if (rightKey) {
 -            fetchData(rightKey);
 -            setLeftSearch('');
 -            setRightSearch('');
 -        }
 -    }, [rightKey, rightData]); // eslint-disable-line react-hooks/exhaustive-deps
 -
 -    const currentPDH = (collectionPanel.item || {}).portableDataHash;
 -    React.useEffect(() => {
 -        if (currentPDH) {
 -            fetchData([leftKey, rightKey], true);
 -        }
 -    }, [currentPDH]); // eslint-disable-line react-hooks/exhaustive-deps
 -
 -    React.useEffect(() => {
 -        if (rightData) {
 -            const filtered = rightData.filter(({ name }) => name.indexOf(rightSearch) > -1);
 -            setCollectionFiles(filtered, false)(dispatch);
 -        }
 -    }, [rightData, dispatch, rightSearch]);
 -
 -    const handleRightClick = React.useCallback(
 -        (event) => {
 -            event.preventDefault();
 -            let elem = event.target;
 -
 -            while (elem && elem.dataset && !elem.dataset.item) {
 -                elem = elem.parentNode;
 +        React.useEffect(() => {
 +            if (rightKey) {
 +                fetchData(rightKey);
 +                setLeftSearch("");
 +                setRightSearch("");
              }
-         }, [rightKey]); // eslint-disable-line react-hooks/exhaustive-deps
++        }, [rightKey, rightData]); // eslint-disable-line react-hooks/exhaustive-deps
  
 -            if (!elem || !elem.dataset) {
 -                return;
 +        const currentPDH = (collectionPanel.item || {}).portableDataHash;
 +        React.useEffect(() => {
 +            if (currentPDH) {
 +                fetchData([leftKey, rightKey], true);
              }
 +        }, [currentPDH]); // eslint-disable-line react-hooks/exhaustive-deps
  
 -            const { id } = elem.dataset;
 +        React.useEffect(() => {
 +            if (rightData) {
 +                const filtered = rightData.filter(({ name }) => name.indexOf(rightSearch) > -1);
 +                setCollectionFiles(filtered, false)(dispatch);
 +            }
 +        }, [rightData, dispatch, rightSearch]);
  
 -            const item: any = {
 -                id,
 -                data: rightData.find((elem) => elem.id === id),
 -            };
 +        const handleRightClick = React.useCallback(
 +            event => {
 +                event.preventDefault();
 +                let elem = event.target;
  
 -            if (id) {
 -                onItemMenuOpen(event, item, isWritable);
 -            }
 -        },
 -        [onItemMenuOpen, isWritable, rightData]);
 +                while (elem && elem.dataset && !elem.dataset.item) {
 +                    elem = elem.parentNode;
 +                }
  
 -    React.useEffect(() => {
 -        let node = null;
 +                if (!elem || !elem.dataset) {
 +                    return;
 +                }
  
 -        if (parentRef?.current) {
 -            node = parentRef.current;
 -            (node as any).addEventListener('contextmenu', handleRightClick);
 -        }
 +                const { id } = elem.dataset;
  
 -        return () => {
 -            if (node) {
 -                (node as any).removeEventListener('contextmenu', handleRightClick);
 -            }
 -        };
 -    }, [parentRef, handleRightClick]);
 +                const item: any = {
 +                    id,
 +                    data: rightData.find(elem => elem.id === id),
 +                };
  
 -    const handleClick = React.useCallback(
 -        (event: any) => {
 -            let isCheckbox = false;
 -            let isMoreButton = false;
 -            let elem = event.target;
 +                if (id) {
 +                    onItemMenuOpen(event, item, isWritable);
 +                }
 +            },
 +            [onItemMenuOpen, isWritable, rightData]
 +        );
  
 -            if (elem.type === 'checkbox') {
 -                isCheckbox = true;
 -            }
 -            // The "More options" button click event could be triggered on its
 -            // internal graphic element.
 -            else if ((elem.dataset && elem.dataset.id === 'moreOptions') || (elem.parentNode && elem.parentNode.dataset && elem.parentNode.dataset.id === 'moreOptions')) {
 -                isMoreButton = true;
 -            }
 +        React.useEffect(() => {
 +            let node = null;
  
 -            while (elem && elem.dataset && !elem.dataset.item) {
 -                elem = elem.parentNode;
 +            if (parentRef?.current) {
 +                node = parentRef.current;
 +                (node as any).addEventListener("contextmenu", handleRightClick);
              }
  
 -            if (elem && elem.dataset && !isCheckbox && !isMoreButton) {
 -                const { parentPath, subfolderPath, breadcrumbPath, type } = elem.dataset;
 -
 -                if (breadcrumbPath) {
 -                    const index = path.indexOf(breadcrumbPath);
 -                    setPath((state) => ([...state.slice(0, index + 1)]));
 +            return () => {
 +                if (node) {
 +                    (node as any).removeEventListener("contextmenu", handleRightClick);
                  }
 +            };
 +        }, [parentRef, handleRightClick]);
  
 -                if (parentPath && type === 'directory') {
 -                    if (path.length > 1) {
 -                        path.pop()
 -                    }
 +        const handleClick = React.useCallback(
 +            (event: any) => {
 +                let isCheckbox = false;
 +                let isMoreButton = false;
 +                let elem = event.target;
  
 -                    setPath((state) => ([...state, parentPath]));
 +                if (elem.type === "checkbox") {
 +                    isCheckbox = true;
                  }
 -
 -                if (subfolderPath && type === 'directory') {
 -                    setPath((state) => ([...state, subfolderPath]));
 +                // The "More options" button click event could be triggered on its
 +                // internal graphic element.
 +                else if (
 +                    (elem.dataset && elem.dataset.id === "moreOptions") ||
 +                    (elem.parentNode && elem.parentNode.dataset && elem.parentNode.dataset.id === "moreOptions")
 +                ) {
 +                    isMoreButton = true;
                  }
  
 -                if (elem.dataset.id && type === 'file') {
 -                    const item = rightData.find(({ id }) => id === elem.dataset.id) || leftData.find(({ id }) => id === elem.dataset.id);
 -                    const enhancedItem = servicesProvider.getServices().collectionService.extendFileURL(item);
 -                    const fileUrl = sanitizeToken(getInlineFileUrl(enhancedItem.url, config.keepWebServiceUrl, config.keepWebInlineServiceUrl), true);
 -                    window.open(fileUrl, '_blank');
 +                while (elem && elem.dataset && !elem.dataset.item) {
 +                    elem = elem.parentNode;
                  }
 -            }
  
 -            if (isCheckbox) {
 -                const { id } = elem.dataset;
 -                const item = collectionPanelFiles[id];
 -                props.onSelectionToggle(event, item);
 -            }
 -            if (isMoreButton) {
 -                const { id } = elem.dataset;
 -                const item: any = {
 -                    id,
 -                    data: rightData.find((elem) => elem.id === id),
 -                };
 -                onItemMenuOpen(event, item, isWritable);
 -            }
 -        },
 -        [path, setPath, collectionPanelFiles] // eslint-disable-line react-hooks/exhaustive-deps
 -    );
 -
 -    const getItemIcon = React.useCallback(
 -        (type: string, activeClass: string | null) => {
 -            let Icon = DefaultIcon;
 -
 -            switch (type) {
 -                case 'directory':
 -                    Icon = DirectoryIcon;
 -                    break;
 -                case 'file':
 -                    Icon = FileIcon;
 -                    break;
 -            }
 +                if (elem && elem.dataset && !isCheckbox && !isMoreButton) {
 +                    const { parentPath, subfolderPath, breadcrumbPath, type } = elem.dataset;
  
 -            return (
 -                <ListItemIcon className={classNames(classes.listItemIcon, activeClass)}>
 -                    <Icon />
 -                </ListItemIcon>
 -            )
 -        },
 -        [classes]
 -    );
 +                    if (breadcrumbPath) {
 +                        const index = path.indexOf(breadcrumbPath);
 +                        setPath(state => [...state.slice(0, index + 1)]);
 +                    }
  
 -    const getActiveClass = React.useCallback(
 -        (name) => {
 -            return path[path.length - 1] === name ? classes.rowActive : null;
 -        },
 -        [path, classes]
 -    );
 +                    if (parentPath && type === "directory") {
 +                        if (path.length > 1) {
 +                            path.pop();
 +                        }
  
 -    const onOptionsMenuOpen = React.useCallback(
 -        (ev, isWritable) => {
 -            props.onOptionsMenuOpen(ev, isWritable);
 -        },
 -        [props.onOptionsMenuOpen] // eslint-disable-line react-hooks/exhaustive-deps
 -    );
 -
 -    return <div data-cy="collection-files-panel" onClick={handleClick} ref={parentRef}>
 -        <div className={classes.pathPanel}>
 -            <div className={classes.pathPanelPathWrapper}>
 -                {path.map((p: string, index: number) =>
 -                    <span key={`${index}-${p}`} data-item="true"
 -                        className={classes.pathPanelItem} data-breadcrumb-path={p}>
 -                        <span className={classes.rowActive}>{index === 0 ? 'Home' : p}</span> <b>/</b>&nbsp;
 -                    </span>)
 +                        setPath(state => [...state, parentPath]);
 +                    }
 +
 +                    if (subfolderPath && type === "directory") {
 +                        setPath(state => [...state, subfolderPath]);
 +                    }
 +
 +                    if (elem.dataset.id && type === "file") {
 +                        const item = rightData.find(({ id }) => id === elem.dataset.id) || leftData.find(({ id }) => id === elem.dataset.id);
 +                        const enhancedItem = servicesProvider.getServices().collectionService.extendFileURL(item);
 +                        const fileUrl = sanitizeToken(
 +                            getInlineFileUrl(enhancedItem.url, config.keepWebServiceUrl, config.keepWebInlineServiceUrl),
 +                            true
 +                        );
 +                        window.open(fileUrl, "_blank");
 +                    }
                  }
 -            </div>
 -            <Tooltip className={classes.pathPanelMenu} title="More options" disableFocusListener>
 -                <IconButton data-cy='collection-files-panel-options-btn'
 -                    onClick={(ev) => {
 -                        onOptionsMenuOpen(ev, isWritable);
 -                    }}>
 -                    <MoreVerticalIcon />
 -                </IconButton>
 -            </Tooltip>
 -        </div>
 -        <div className={classes.wrapper}>
 -            <div className={classNames(classes.leftPanel, path.length > 1 ? classes.leftPanelVisible : classes.leftPanelHidden)} data-cy="collection-files-left-panel">
 -                <Tooltip title="Go back" className={path.length > 1 ? classes.backButton : classes.backButtonHidden}>
 -                    <IconButton onClick={() => setPath((state) => ([...state.slice(0, state.length - 1)]))}>
 -                        <BackIcon />
 -                    </IconButton>
 -                </Tooltip>
 -                <div className={path.length > 1 ? classes.searchWrapper : classes.searchWrapperHidden}>
 -                    <SearchInput selfClearProp={leftKey} label="Search" value={leftSearch} onSearch={setLeftSearch} />
 -                </div>
 -                <div className={classes.dataWrapper}>{leftData
 -                    ? <AutoSizer defaultWidth={0}>{({ height, width }) => {
 -                        const filtered = leftData.filter(({ name }) => name.indexOf(leftSearch) > -1);
 -                        return !!filtered.length
 -                            ? <FixedSizeList height={height} itemCount={filtered.length}
 -                                itemSize={35} width={width}>{({ index, style }) => {
 -                                    const { id, type, name } = filtered[index];
 -                                    return <div data-id={id} style={style} data-item="true"
 -                                        data-type={type} data-parent-path={name}
 -                                        className={classNames(classes.row, getActiveClass(name))}
 -                                        key={id}>
 -                                        {getItemIcon(type, getActiveClass(name))}
 -                                        <div className={classes.rowName}>
 -                                            {name}
 -                                        </div>
 -                                        {getActiveClass(name)
 -                                            ? <SidePanelRightArrowIcon
 -                                                style={{ display: 'inline', marginTop: '5px', marginLeft: '5px' }} />
 -                                            : null
 -                                        }
 -                                    </div>;
 -                                }}</FixedSizeList>
 -                            : <div className={classes.rowEmpty}>No directories available</div>
 -                    }}
 -                    </AutoSizer>
 -                    : <div data-cy="collection-loader" className={classes.row}><CircularProgress className={classes.loader} size={30} /></div>}
 -                </div>
 -            </div>
 -            <div className={classes.rightPanel} data-cy="collection-files-right-panel">
 -                <div className={classes.searchWrapper}>
 -                    <SearchInput selfClearProp={rightKey} label="Search" value={rightSearch} onSearch={setRightSearch} />
 +
 +                if (isCheckbox) {
 +                    const { id } = elem.dataset;
 +                    const item = collectionPanelFiles[id];
 +                    props.onSelectionToggle(event, item);
 +                }
 +                if (isMoreButton) {
 +                    const { id } = elem.dataset;
 +                    const item: any = {
 +                        id,
 +                        data: rightData.find(elem => elem.id === id),
 +                    };
 +                    onItemMenuOpen(event, item, isWritable);
 +                }
 +            },
 +            [path, setPath, collectionPanelFiles] // eslint-disable-line react-hooks/exhaustive-deps
 +        );
 +
 +        const getItemIcon = React.useCallback(
 +            (type: string, activeClass: string | null) => {
 +                let Icon = DefaultIcon;
 +
 +                switch (type) {
 +                    case "directory":
 +                        Icon = DirectoryIcon;
 +                        break;
 +                    case "file":
 +                        Icon = FileIcon;
 +                        break;
 +                }
 +
 +                return (
 +                    <ListItemIcon className={classNames(classes.listItemIcon, activeClass)}>
 +                        <Icon />
 +                    </ListItemIcon>
 +                );
 +            },
 +            [classes]
 +        );
 +
 +        const getActiveClass = React.useCallback(
 +            name => {
 +                return path[path.length - 1] === name ? classes.rowActive : null;
 +            },
 +            [path, classes]
 +        );
 +
 +        const onOptionsMenuOpen = React.useCallback(
 +            (ev, isWritable) => {
 +                props.onOptionsMenuOpen(ev, isWritable);
 +            },
 +            [props.onOptionsMenuOpen] // eslint-disable-line react-hooks/exhaustive-deps
 +        );
 +
 +        return (
 +            <div
 +                data-cy="collection-files-panel"
 +                onClick={handleClick}
-                 ref={parentRef}>
++                ref={parentRef}
++            >
 +                <div className={classes.pathPanel}>
 +                    <div className={classes.pathPanelPathWrapper}>
 +                        {path.map((p: string, index: number) => (
 +                            <span
 +                                key={`${index}-${p}`}
 +                                data-item="true"
 +                                className={classes.pathPanelItem}
-                                 data-breadcrumb-path={p}>
++                                data-breadcrumb-path={p}
++                            >
 +                                <span className={classes.rowActive}>{index === 0 ? "Home" : p}</span> <b>/</b>&nbsp;
 +                            </span>
 +                        ))}
 +                    </div>
 +                    <Tooltip
 +                        className={classes.pathPanelMenu}
 +                        title="More options"
-                         disableFocusListener>
++                        disableFocusListener
++                    >
 +                        <IconButton
 +                            data-cy="collection-files-panel-options-btn"
 +                            onClick={ev => {
 +                                onOptionsMenuOpen(ev, isWritable);
-                             }}>
-                             <CustomizeTableIcon />
++                            }}
++                        >
++                            <MoreVerticalIcon />
 +                        </IconButton>
 +                    </Tooltip>
                  </div>
 -                {isWritable &&
 -                    <Button className={classes.uploadButton} data-cy='upload-button'
 -                        onClick={() => {
 -                            onUploadDataClick(rightKey === leftKey ? undefined : rightKey);
 -                        }}
 -                        variant='contained' color='primary' size='small'>
 -                        <DownloadIcon className={classes.uploadIcon} />
 -                        Upload data
 -                    </Button>}
 -                <div className={classes.dataWrapper}>{rightData && !isLoading
 -                    ? <AutoSizer defaultHeight={500}>{({ height, width }) => {
 -                        const filtered = rightData.filter(({ name }) => name.indexOf(rightSearch) > -1);
 -                        return !!filtered.length
 -                            ? <FixedSizeList height={height} itemCount={filtered.length}
 -                                itemSize={35} width={width}>{({ index, style }) => {
 -                                    const { id, type, name, size } = filtered[index];
 -
 -                                    return <div style={style} data-id={id} data-item="true"
 -                                        data-type={type} data-subfolder-path={name}
 -                                        className={classes.row} key={id}>
 -                                        <Checkbox color="primary"
 -                                            className={classes.rowSelection}
 -                                            checked={collectionPanelFiles[id] ? collectionPanelFiles[id].value.selected : false}
 -                                        />&nbsp;
 -                                        {getItemIcon(type, null)}
 -                                        <div className={classes.rowName}>
 -                                            {name}
 -                                        </div>
 -                                        <span className={classes.rowName} style={{
 -                                            marginLeft: 'auto', marginRight: '1rem'
 -                                        }}>
 -                                            {formatFileSize(size)}
 -                                        </span>
 -                                        <Tooltip title="More options" disableFocusListener>
 -                                            <IconButton data-id='moreOptions'
 -                                                data-cy='file-item-options-btn'
 -                                                className={classes.moreOptionsButton}>
 -                                                <MoreHorizontalIcon
 -                                                    data-id='moreOptions'
 -                                                    className={classes.moreOptions} />
 -                                            </IconButton>
 -                                        </Tooltip>
 -                                    </div>
 -                                }}</FixedSizeList>
 -                            : <div className={classes.rowEmpty}>This collection is empty</div>
 -                    }}</AutoSizer>
 -                    : <div className={classes.row}>
 -                        <CircularProgress className={classes.loader} size={30} />
 -                    </div>}
 +                <div className={classes.wrapper}>
 +                    <div
 +                        className={classNames(classes.leftPanel, path.length > 1 ? classes.leftPanelVisible : classes.leftPanelHidden)}
-                         data-cy="collection-files-left-panel">
++                        data-cy="collection-files-left-panel"
++                    >
 +                        <Tooltip
 +                            title="Go back"
-                             className={path.length > 1 ? classes.backButton : classes.backButtonHidden}>
++                            className={path.length > 1 ? classes.backButton : classes.backButtonHidden}
++                        >
 +                            <IconButton onClick={() => setPath(state => [...state.slice(0, state.length - 1)])}>
 +                                <BackIcon />
 +                            </IconButton>
 +                        </Tooltip>
 +                        <div className={path.length > 1 ? classes.searchWrapper : classes.searchWrapperHidden}>
 +                            <SearchInput
 +                                selfClearProp={leftKey}
 +                                label="Search"
 +                                value={leftSearch}
 +                                onSearch={setLeftSearch}
 +                            />
 +                        </div>
 +                        <div className={classes.dataWrapper}>
 +                            {leftData ? (
 +                                <AutoSizer defaultWidth={0}>
 +                                    {({ height, width }) => {
 +                                        const filtered = leftData.filter(({ name }) => name.indexOf(leftSearch) > -1);
 +                                        return !!filtered.length ? (
 +                                            <FixedSizeList
 +                                                height={height}
 +                                                itemCount={filtered.length}
 +                                                itemSize={35}
-                                                 width={width}>
++                                                width={width}
++                                            >
 +                                                {({ index, style }) => {
 +                                                    const { id, type, name } = filtered[index];
 +                                                    return (
 +                                                        <div
 +                                                            data-id={id}
 +                                                            style={style}
 +                                                            data-item="true"
 +                                                            data-type={type}
 +                                                            data-parent-path={name}
 +                                                            className={classNames(classes.row, getActiveClass(name))}
-                                                             key={id}>
++                                                            key={id}
++                                                        >
 +                                                            {getItemIcon(type, getActiveClass(name))}
 +                                                            <div className={classes.rowName}>{name}</div>
 +                                                            {getActiveClass(name) ? (
 +                                                                <SidePanelRightArrowIcon
 +                                                                    style={{ display: "inline", marginTop: "5px", marginLeft: "5px" }}
 +                                                                />
 +                                                            ) : null}
 +                                                        </div>
 +                                                    );
 +                                                }}
 +                                            </FixedSizeList>
 +                                        ) : (
 +                                            <div className={classes.rowEmpty}>No directories available</div>
 +                                        );
 +                                    }}
 +                                </AutoSizer>
 +                            ) : (
 +                                <div
 +                                    data-cy="collection-loader"
-                                     className={classes.row}>
++                                    className={classes.row}
++                                >
 +                                    <CircularProgress
 +                                        className={classes.loader}
 +                                        size={30}
 +                                    />
 +                                </div>
 +                            )}
 +                        </div>
 +                    </div>
 +                    <div
 +                        className={classes.rightPanel}
-                         data-cy="collection-files-right-panel">
++                        data-cy="collection-files-right-panel"
++                    >
 +                        <div className={classes.searchWrapper}>
 +                            <SearchInput
 +                                selfClearProp={rightKey}
 +                                label="Search"
 +                                value={rightSearch}
 +                                onSearch={setRightSearch}
 +                            />
 +                        </div>
 +                        {isWritable && (
 +                            <Button
 +                                className={classes.uploadButton}
 +                                data-cy="upload-button"
 +                                onClick={() => {
 +                                    onUploadDataClick(rightKey === leftKey ? undefined : rightKey);
 +                                }}
 +                                variant="contained"
 +                                color="primary"
-                                 size="small">
++                                size="small"
++                            >
 +                                <DownloadIcon className={classes.uploadIcon} />
 +                                Upload data
 +                            </Button>
 +                        )}
 +                        <div className={classes.dataWrapper}>
 +                            {rightData && !isLoading ? (
 +                                <AutoSizer defaultHeight={500}>
 +                                    {({ height, width }) => {
 +                                        const filtered = rightData.filter(({ name }) => name.indexOf(rightSearch) > -1);
 +                                        return !!filtered.length ? (
 +                                            <FixedSizeList
 +                                                height={height}
 +                                                itemCount={filtered.length}
 +                                                itemSize={35}
-                                                 width={width}>
++                                                width={width}
++                                            >
 +                                                {({ index, style }) => {
 +                                                    const { id, type, name, size } = filtered[index];
 +
 +                                                    return (
 +                                                        <div
 +                                                            style={style}
 +                                                            data-id={id}
 +                                                            data-item="true"
 +                                                            data-type={type}
 +                                                            data-subfolder-path={name}
 +                                                            className={classes.row}
-                                                             key={id}>
++                                                            key={id}
++                                                        >
 +                                                            <Checkbox
 +                                                                color="primary"
 +                                                                className={classes.rowSelection}
 +                                                                checked={collectionPanelFiles[id] ? collectionPanelFiles[id].value.selected : false}
 +                                                            />
 +                                                            &nbsp;
 +                                                            {getItemIcon(type, null)}
 +                                                            <div className={classes.rowName}>{name}</div>
 +                                                            <span
 +                                                                className={classes.rowName}
 +                                                                style={{
 +                                                                    marginLeft: "auto",
 +                                                                    marginRight: "1rem",
-                                                                 }}>
++                                                                }}
++                                                            >
 +                                                                {formatFileSize(size)}
 +                                                            </span>
 +                                                            <Tooltip
 +                                                                title="More options"
-                                                                 disableFocusListener>
++                                                                disableFocusListener
++                                                            >
 +                                                                <IconButton
 +                                                                    data-id="moreOptions"
 +                                                                    data-cy="file-item-options-btn"
-                                                                     className={classes.moreOptionsButton}>
-                                                                     <MoreOptionsIcon
++                                                                    className={classes.moreOptionsButton}
++                                                                >
++                                                                    <MoreHorizontalIcon
 +                                                                        data-id="moreOptions"
 +                                                                        className={classes.moreOptions}
 +                                                                    />
 +                                                                </IconButton>
 +                                                            </Tooltip>
 +                                                        </div>
 +                                                    );
 +                                                }}
 +                                            </FixedSizeList>
 +                                        ) : (
 +                                            <div className={classes.rowEmpty}>This collection is empty</div>
 +                                        );
 +                                    }}
 +                                </AutoSizer>
 +                            ) : (
 +                                <div className={classes.row}>
 +                                    <CircularProgress
 +                                        className={classes.loader}
 +                                        size={30}
 +                                    />
 +                                </div>
 +                            )}
 +                        </div>
 +                    </div>
                  </div>
              </div>
 -        </div>
 -    </div>
 -}));
 +        );
 +    })
 +);
index 91d7dc77418d0b026e23711172ab58e7311be60e,dc097c390ef110fa2ff262d300e48d5fcaf62657..ad5762dfeb1bac4bda716b02ff60bdd646c6dbf0
@@@ -7,17 -7,21 +7,17 @@@ import { Grid, Paper, Toolbar, StyleRul
  import { ColumnSelector } from "components/column-selector/column-selector";
  import { DataTable, DataColumns, DataTableFetchMode } from "components/data-table/data-table";
  import { DataColumn } from "components/data-table/data-column";
 -import { SearchInput } from 'components/search-input/search-input';
 +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 {
 -    CloseIcon,
 -    IconType,
 -    MaximizeIcon,
 -    UnMaximizeIcon,
 -    MoreVerticalIcon
 -} from 'components/icon/icon';
 -import { PaperProps } from '@material-ui/core/Paper';
 -import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
++import { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
++import { TCheckedList } from "components/data-table/data-table";
 +import { createTree } from "models/tree";
 +import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
- import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreOptionsIcon } from "components/icon/icon";
++import { CloseIcon, IconType, MaximizeIcon, UnMaximizeIcon, MoreVerticalIcon } from "components/icon/icon";
 +import { PaperProps } from "@material-ui/core/Paper";
 +import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
- import { MultiselectToolbar } from "components/multiselect-toolbar/MultiselectToolbar";
- import { TCheckedList } from "components/data-table/data-table";
  
 -type CssRules = 'searchBox' | 'headerMenu' | "toolbar" | "footer" | "root" | 'moreOptionsButton' | 'title' | 'dataTable' | 'container';
 +type CssRules = "searchBox" | "headerMenu" | "toolbar" | "footer" | "root" | "moreOptionsButton" | "title" | "dataTable" | "container";
  
  const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
      searchBox: {
@@@ -155,182 -153,90 +155,194 @@@ export const DataExplorer = withStyles(
  
          render() {
              const {
 -                columns, onContextMenu, onFiltersChange, onSortToggle, extractKey,
 -                rowsPerPage, rowsPerPageOptions, onColumnToggle, searchLabel, searchValue, onSearch,
 -                items, itemsAvailable, onRowClick, onRowDoubleClick, classes,
 -                defaultViewIcon, defaultViewMessages, hideColumnSelector, actions, paperProps, hideSearchInput,
 -                paperKey, fetchMode, currentItemUuid, title,
 -                doHidePanel, doMaximizePanel, doUnMaximizePanel, panelName, panelMaximized, elementPath
 +                columns,
 +                onContextMenu,
 +                onFiltersChange,
 +                onSortToggle,
 +                extractKey,
 +                rowsPerPage,
 +                rowsPerPageOptions,
 +                onColumnToggle,
 +                searchLabel,
 +                searchValue,
 +                onSearch,
 +                items,
 +                itemsAvailable,
 +                onRowClick,
 +                onRowDoubleClick,
 +                classes,
 +                defaultViewIcon,
 +                defaultViewMessages,
 +                hideColumnSelector,
 +                actions,
 +                paperProps,
 +                hideSearchInput,
 +                paperKey,
 +                fetchMode,
 +                currentItemUuid,
 +                title,
 +                doHidePanel,
 +                doMaximizePanel,
 +                doUnMaximizePanel,
 +                panelName,
 +                panelMaximized,
 +                elementPath,
 +                toggleMSToolbar,
 +                setCheckedListOnStore,
 +                checkedList,
              } = this.props;
 -            return <Paper className={classes.root} {...paperProps} key={paperKey} data-cy={this.props["data-cy"]}>
 -                <Grid container direction="column" wrap="nowrap" className={classes.container}>
 -                    <div>
 -                        {title && <Grid item xs className={classes.title}>{title}</Grid>}
 -                        {
 -                            (!hideColumnSelector || !hideSearchInput || !!actions) &&
 -                            <Grid className={classes.headerMenu} item xs>
 -                                <Toolbar className={classes.toolbar}>
 -                                    {!hideSearchInput && <div className={classes.searchBox}>
 -                                        {!hideSearchInput && <SearchInput
 -                                            label={searchLabel}
 -                                            value={searchValue}
 -                                            selfClearProp={''}
 -                                            onSearch={onSearch} />}
 -                                    </div>}
 -                                    {actions}
 -                                    {!hideColumnSelector && <ColumnSelector
 -                                        columns={columns}
 -                                        onColumnToggle={onColumnToggle} />}
 -                                    { doUnMaximizePanel && panelMaximized &&
 -                                    <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
 -                                        <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
 -                                    </Tooltip> }
 -                                    { doMaximizePanel && !panelMaximized &&
 -                                        <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
 -                                            <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
 -                                        </Tooltip> }
 -                                    { doHidePanel &&
 -                                        <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
 -                                            <IconButton disabled={panelMaximized} onClick={doHidePanel}><CloseIcon /></IconButton>
 -                                        </Tooltip> }
 -                                </Toolbar>
 -                            </Grid>
 -                        }
 -                    </div>
 -                <Grid item xs="auto" className={classes.dataTable}><DataTable
 -                    columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
 -                    items={items}
 -                    onRowClick={(_, item: T) => onRowClick(item)}
 -                    onContextMenu={onContextMenu}
 -                    onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
 -                    onFiltersChange={onFiltersChange}
 -                    onSortToggle={onSortToggle}
 -                    extractKey={extractKey}
 -                    working={this.state.showLoading}
 -                    defaultViewIcon={defaultViewIcon}
 -                    defaultViewMessages={defaultViewMessages}
 -                    currentItemUuid={currentItemUuid}
 -                    currentRoute={paperKey} /></Grid>
 -                <Grid item xs><Toolbar className={classes.footer}>
 -                    {
 -                        elementPath &&
 -                        <Grid container>
 -                            <span data-cy="element-path">
 -                                {elementPath}
 -                            </span>
 +            return (
 +                <Paper
 +                    className={classes.root}
 +                    {...paperProps}
 +                    key={paperKey}
-                     data-cy={this.props["data-cy"]}>
++                    data-cy={this.props["data-cy"]}
++                >
 +                    <Grid
 +                        container
 +                        direction="column"
 +                        wrap="nowrap"
-                         className={classes.container}>
++                        className={classes.container}
++                    >
 +                        <div>
 +                            {title && (
 +                                <Grid
 +                                    item
 +                                    xs
-                                     className={classes.title}>
++                                    className={classes.title}
++                                >
 +                                    {title}
 +                                </Grid>
 +                            )}
 +                            {(!hideColumnSelector || !hideSearchInput || !!actions) && (
 +                                <Grid
 +                                    className={classes.headerMenu}
 +                                    item
-                                     xs>
++                                    xs
++                                >
 +                                    <Toolbar className={classes.toolbar}>
 +                                        {!hideSearchInput && (
 +                                            <div className={classes.searchBox}>
 +                                                {!hideSearchInput && (
 +                                                    <SearchInput
 +                                                        label={searchLabel}
 +                                                        value={searchValue}
 +                                                        selfClearProp={""}
 +                                                        onSearch={onSearch}
 +                                                    />
 +                                                )}
 +                                            </div>
 +                                        )}
 +                                        {actions}
 +                                        {!hideColumnSelector && (
 +                                            <ColumnSelector
 +                                                columns={columns}
 +                                                onColumnToggle={onColumnToggle}
 +                                            />
 +                                        )}
 +                                        {doUnMaximizePanel && panelMaximized && (
 +                                            <Tooltip
 +                                                title={`Unmaximize ${panelName || "panel"}`}
-                                                 disableFocusListener>
++                                                disableFocusListener
++                                            >
 +                                                <IconButton onClick={doUnMaximizePanel}>
 +                                                    <UnMaximizeIcon />
 +                                                </IconButton>
 +                                            </Tooltip>
 +                                        )}
 +                                        {doMaximizePanel && !panelMaximized && (
 +                                            <Tooltip
 +                                                title={`Maximize ${panelName || "panel"}`}
-                                                 disableFocusListener>
++                                                disableFocusListener
++                                            >
 +                                                <IconButton onClick={doMaximizePanel}>
 +                                                    <MaximizeIcon />
 +                                                </IconButton>
 +                                            </Tooltip>
 +                                        )}
 +                                        {doHidePanel && (
 +                                            <Tooltip
 +                                                title={`Close ${panelName || "panel"}`}
-                                                 disableFocusListener>
++                                                disableFocusListener
++                                            >
 +                                                <IconButton
 +                                                    disabled={panelMaximized}
-                                                     onClick={doHidePanel}>
++                                                    onClick={doHidePanel}
++                                                >
 +                                                    <CloseIcon />
 +                                                </IconButton>
 +                                            </Tooltip>
 +                                        )}
 +                                    </Toolbar>
 +                                    <MultiselectToolbar />
 +                                </Grid>
 +                            )}
 +                        </div>
 +                        <Grid
 +                            item
 +                            xs="auto"
-                             className={classes.dataTable}>
++                            className={classes.dataTable}
++                        >
 +                            <DataTable
 +                                columns={this.props.contextMenuColumn ? [...columns, this.contextMenuColumn] : columns}
 +                                items={items}
 +                                onRowClick={(_, item: T) => onRowClick(item)}
 +                                onContextMenu={onContextMenu}
 +                                onRowDoubleClick={(_, item: T) => onRowDoubleClick(item)}
 +                                onFiltersChange={onFiltersChange}
 +                                onSortToggle={onSortToggle}
 +                                extractKey={extractKey}
 +                                working={this.state.showLoading}
 +                                defaultViewIcon={defaultViewIcon}
 +                                defaultViewMessages={defaultViewMessages}
 +                                currentItemUuid={currentItemUuid}
 +                                currentRoute={paperKey}
 +                                toggleMSToolbar={toggleMSToolbar}
 +                                setCheckedListOnStore={setCheckedListOnStore}
 +                                checkedList={checkedList}
 +                            />
 +                        </Grid>
 +                        <Grid
 +                            item
-                             xs>
++                            xs
++                        >
 +                            <Toolbar className={classes.footer}>
 +                                {elementPath && (
 +                                    <Grid container>
 +                                        <span data-cy="element-path">{elementPath}</span>
 +                                    </Grid>
 +                                )}
 +                                <Grid
 +                                    container={!elementPath}
-                                     justify="flex-end">
++                                    justify="flex-end"
++                                >
 +                                    {fetchMode === DataTableFetchMode.PAGINATED ? (
 +                                        <TablePagination
 +                                            count={itemsAvailable}
 +                                            rowsPerPage={rowsPerPage}
 +                                            rowsPerPageOptions={rowsPerPageOptions}
 +                                            page={this.props.page}
 +                                            onChangePage={this.changePage}
 +                                            onChangeRowsPerPage={this.changeRowsPerPage}
 +                                            // Disable next button on empty lists since that's not default behavior
 +                                            nextIconButtonProps={itemsAvailable > 0 ? {} : { disabled: true }}
 +                                            component="div"
 +                                        />
 +                                    ) : (
 +                                        <Button
 +                                            variant="text"
 +                                            size="medium"
-                                             onClick={this.loadMore}>
++                                            onClick={this.loadMore}
++                                        >
 +                                            Load more
 +                                        </Button>
 +                                    )}
 +                                </Grid>
 +                            </Toolbar>
                          </Grid>
 -                    }
 -                    <Grid container={!elementPath} justify="flex-end">
 -                        {fetchMode === DataTableFetchMode.PAGINATED ? <TablePagination
 -                            count={itemsAvailable}
 -                            rowsPerPage={rowsPerPage}
 -                            rowsPerPageOptions={rowsPerPageOptions}
 -                            page={this.props.page}
 -                            onChangePage={this.changePage}
 -                            onChangeRowsPerPage={this.changeRowsPerPage}
 -                            // Disable next button on empty lists since that's not default behavior
 -                            nextIconButtonProps={(itemsAvailable > 0) ? {} : {disabled: true}}
 -                            component="div" /> : <Button
 -                                variant="text"
 -                                size="medium"
 -                                onClick={this.loadMore}
 -                            >Load more</Button>}
                      </Grid>
 -                </Toolbar></Grid>
 -                </Grid>
 -            </Paper>;
 +                </Paper>
 +            );
          }
  
          changePage = (event: React.MouseEvent<HTMLButtonElement>, page: number) => {
  
          loadMore = () => {
              this.props.onLoadMore(this.props.page + 1);
 -        }
 +        };
  
 -        renderContextMenuTrigger = (item: T) =>
 -            <Grid container justify="center">
 -                <Tooltip title="More options" disableFocusListener>
 -                    <IconButton className={this.props.classes.moreOptionsButton} onClick={event => this.props.onContextMenu(event, item)}>
 +        renderContextMenuTrigger = (item: T) => (
 +            <Grid
 +                container
-                 justify="center">
++                justify="center"
++            >
 +                <Tooltip
 +                    title="More options"
-                     disableFocusListener>
++                    disableFocusListener
++                >
 +                    <IconButton
 +                        className={this.props.classes.moreOptionsButton}
-                         onClick={event => this.props.onContextMenu(event, item)}>
-                         <MoreOptionsIcon />
++                        onClick={event => this.props.onContextMenu(event, item)}
++                    >
+                         <MoreVerticalIcon />
                      </IconButton>
                  </Tooltip>
              </Grid>
diff --cc src/index.tsx
index cddfb464315ba3304c42218bb59f16c262611e16,7cc18783d13f70d02fcef54cdf745e3d9164d257..d2af0952867dee9b45debda3b57d7971f1b7781d
@@@ -16,76 -16,57 +16,81 @@@ import { ApiToken } from "views-compone
  import { AddSession } from "views-components/add-session/add-session";
  import { initAuth, logout } from "store/auth/auth-action";
  import { createServices } from "services/services";
 -import { MuiThemeProvider } from '@material-ui/core/styles';
 -import { CustomTheme } from 'common/custom-theme';
 -import { fetchConfig } from 'common/config';
 -import servicesProvider from 'common/service-provider';
 -import { addMenuActionSet, ContextMenuKind } from 'views-components/context-menu/context-menu';
 +import { MuiThemeProvider } from "@material-ui/core/styles";
 +import { CustomTheme } from "common/custom-theme";
 +import { fetchConfig } from "common/config";
 +import servicesProvider from "common/service-provider";
 +import { addMenuActionSet, ContextMenuKind } from "views-components/context-menu/context-menu";
  import { rootProjectActionSet } from "views-components/context-menu/action-sets/root-project-action-set";
 -import { filterGroupActionSet, frozenActionSet, projectActionSet, readOnlyProjectActionSet } from "views-components/context-menu/action-sets/project-action-set";
 -import { resourceActionSet } from 'views-components/context-menu/action-sets/resource-action-set';
 +import {
 +    filterGroupActionSet,
 +    frozenActionSet,
 +    projectActionSet,
 +    readOnlyProjectActionSet,
 +} from "views-components/context-menu/action-sets/project-action-set";
 +import { resourceActionSet } from "views-components/context-menu/action-sets/resource-action-set";
  import { favoriteActionSet } from "views-components/context-menu/action-sets/favorite-action-set";
- import { collectionFilesActionSet, readOnlyCollectionFilesActionSet } from "views-components/context-menu/action-sets/collection-files-action-set";
 -import { collectionFilesActionSet, collectionFilesMultipleActionSet, readOnlyCollectionFilesActionSet, readOnlyCollectionFilesMultipleActionSet } from 'views-components/context-menu/action-sets/collection-files-action-set';
 -import { collectionDirectoryItemActionSet, collectionFileItemActionSet, readOnlyCollectionDirectoryItemActionSet, readOnlyCollectionFileItemActionSet } from 'views-components/context-menu/action-sets/collection-files-item-action-set';
 -import { collectionFilesNotSelectedActionSet } from 'views-components/context-menu/action-sets/collection-files-not-selected-action-set';
 -import { collectionActionSet, collectionAdminActionSet, oldCollectionVersionActionSet, readOnlyCollectionActionSet } from 'views-components/context-menu/action-sets/collection-action-set';
 -import { loadWorkbench } from 'store/workbench/workbench-actions';
 -import { Routes } from 'routes/routes';
++import {
++    collectionFilesActionSet,
++    collectionFilesMultipleActionSet,
++    readOnlyCollectionFilesActionSet,
++    readOnlyCollectionFilesMultipleActionSet,
++} from "views-components/context-menu/action-sets/collection-files-action-set";
 +import {
 +    collectionDirectoryItemActionSet,
 +    collectionFileItemActionSet,
 +    readOnlyCollectionDirectoryItemActionSet,
 +    readOnlyCollectionFileItemActionSet,
 +} from "views-components/context-menu/action-sets/collection-files-item-action-set";
 +import { collectionFilesNotSelectedActionSet } from "views-components/context-menu/action-sets/collection-files-not-selected-action-set";
 +import {
 +    collectionActionSet,
 +    collectionAdminActionSet,
 +    oldCollectionVersionActionSet,
 +    readOnlyCollectionActionSet,
 +} from "views-components/context-menu/action-sets/collection-action-set";
 +import { loadWorkbench } from "store/workbench/workbench-actions";
 +import { Routes } from "routes/routes";
  import { trashActionSet } from "views-components/context-menu/action-sets/trash-action-set";
 -import { ServiceRepository } from 'services/services';
 -import { initWebSocket } from 'websocket/websocket';
 -import { Config } from 'common/config';
 -import { addRouteChangeHandlers } from './routes/route-change-handlers';
 -import { setTokenDialogApiHost } from 'store/token-dialog/token-dialog-actions';
 +import { ServiceRepository } from "services/services";
 +import { initWebSocket } from "websocket/websocket";
 +import { Config } from "common/config";
 +import { addRouteChangeHandlers } from "./routes/route-change-handlers";
 +import { setTokenDialogApiHost } from "store/token-dialog/token-dialog-actions";
  import {
      processResourceActionSet,
      processResourceAdminActionSet,
 -    readOnlyProcessResourceActionSet
 -} from 'views-components/context-menu/action-sets/process-resource-action-set';
 -import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
 -import { trashedCollectionActionSet } from 'views-components/context-menu/action-sets/trashed-collection-action-set';
 -import { setBuildInfo } from 'store/app-info/app-info-actions';
 -import { getBuildInfo } from 'common/app-info';
 -import { DragDropContextProvider } from 'react-dnd';
 -import HTML5Backend from 'react-dnd-html5-backend';
 -import { initAdvancedFormProjectsTree } from 'store/search-bar/search-bar-actions';
 -import { repositoryActionSet } from 'views-components/context-menu/action-sets/repository-action-set';
 -import { sshKeyActionSet } from 'views-components/context-menu/action-sets/ssh-key-action-set';
 -import { keepServiceActionSet } from 'views-components/context-menu/action-sets/keep-service-action-set';
 -import { loadVocabulary } from 'store/vocabulary/vocabulary-actions';
 -import { virtualMachineActionSet } from 'views-components/context-menu/action-sets/virtual-machine-action-set';
 -import { userActionSet } from 'views-components/context-menu/action-sets/user-action-set';
 -import { apiClientAuthorizationActionSet } from 'views-components/context-menu/action-sets/api-client-authorization-action-set';
 -import { groupActionSet } from 'views-components/context-menu/action-sets/group-action-set';
 -import { groupMemberActionSet } from 'views-components/context-menu/action-sets/group-member-action-set';
 -import { linkActionSet } from 'views-components/context-menu/action-sets/link-action-set';
 -import { loadFileViewersConfig } from 'store/file-viewers/file-viewers-actions';
 -import { filterGroupAdminActionSet, frozenAdminActionSet, projectAdminActionSet } from 'views-components/context-menu/action-sets/project-admin-action-set';
 -import { permissionEditActionSet } from 'views-components/context-menu/action-sets/permission-edit-action-set';
 -import { workflowActionSet, readOnlyWorkflowActionSet } from 'views-components/context-menu/action-sets/workflow-action-set';
 +    readOnlyProcessResourceActionSet,
 +} from "views-components/context-menu/action-sets/process-resource-action-set";
 +import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
 +import { trashedCollectionActionSet } from "views-components/context-menu/action-sets/trashed-collection-action-set";
 +import { setBuildInfo } from "store/app-info/app-info-actions";
 +import { getBuildInfo } from "common/app-info";
 +import { DragDropContextProvider } from "react-dnd";
 +import HTML5Backend from "react-dnd-html5-backend";
 +import { initAdvancedFormProjectsTree } from "store/search-bar/search-bar-actions";
 +import { repositoryActionSet } from "views-components/context-menu/action-sets/repository-action-set";
 +import { sshKeyActionSet } from "views-components/context-menu/action-sets/ssh-key-action-set";
 +import { keepServiceActionSet } from "views-components/context-menu/action-sets/keep-service-action-set";
 +import { loadVocabulary } from "store/vocabulary/vocabulary-actions";
 +import { virtualMachineActionSet } from "views-components/context-menu/action-sets/virtual-machine-action-set";
 +import { userActionSet } from "views-components/context-menu/action-sets/user-action-set";
 +import { apiClientAuthorizationActionSet } from "views-components/context-menu/action-sets/api-client-authorization-action-set";
 +import { groupActionSet } from "views-components/context-menu/action-sets/group-action-set";
 +import { groupMemberActionSet } from "views-components/context-menu/action-sets/group-member-action-set";
 +import { linkActionSet } from "views-components/context-menu/action-sets/link-action-set";
 +import { loadFileViewersConfig } from "store/file-viewers/file-viewers-actions";
 +import {
 +    filterGroupAdminActionSet,
 +    frozenAdminActionSet,
 +    projectAdminActionSet,
 +} from "views-components/context-menu/action-sets/project-admin-action-set";
 +import { permissionEditActionSet } from "views-components/context-menu/action-sets/permission-edit-action-set";
- import { workflowActionSet } from "views-components/context-menu/action-sets/workflow-action-set";
++import { workflowActionSet, readOnlyWorkflowActionSet } from "views-components/context-menu/action-sets/workflow-action-set";
  import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 -import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
 -import { storeRedirects } from './common/redirect-to';
 -import { searchResultsActionSet } from 'views-components/context-menu/action-sets/search-results-action-set';
 +import { openNotFoundDialog } from "./store/not-found-panel/not-found-panel-action";
 +import { storeRedirects } from "./common/redirect-to";
 +import { searchResultsActionSet } from "views-components/context-menu/action-sets/search-results-action-set";
  
  console.log(`Starting arvados [${getBuildInfo()}]`);
  
index 4bd989e07c160144e71c74ebc7f8968f6e3039e8,7e28c37bee64b04060fe1fa5c11698aed403eb1e..de8f258708dc1e94d278a9324a8c68e3f2a9aa6a
@@@ -12,22 -12,27 +12,28 @@@ import { TrashableResourceService } fro
  import { ApiActions } from "services/api/api-actions";
  import { Session } from "models/session";
  import { CommonService } from "services/common-service/common-service";
+ import { snakeCase } from "lodash";
+ import { CommonResourceServiceError } from "services/common-service/common-resource-service";
  
  export type UploadProgress = (fileId: number, loaded: number, total: number, currentTime: number) => void;
 -type CollectionPartialUpdateOrCreate =  Partial<CollectionResource> & Pick<CollectionResource, "uuid"> |
 -                                        Partial<CollectionResource> & Pick<CollectionResource, "ownerUuid">;
++type CollectionPartialUpdateOrCreate =
++    | (Partial<CollectionResource> & Pick<CollectionResource, "uuid">)
++    | (Partial<CollectionResource> & Pick<CollectionResource, "ownerUuid">);
  
 -export const emptyCollectionPdh = 'd41d8cd98f00b204e9800998ecf8427e+0';
 -export const SOURCE_DESTINATION_EQUAL_ERROR_MESSAGE = 'Source and destination cannot be the same';
 +export const emptyCollectionPdh = "d41d8cd98f00b204e9800998ecf8427e+0";
++export const SOURCE_DESTINATION_EQUAL_ERROR_MESSAGE = "Source and destination cannot be the same";
  
  export class CollectionService extends TrashableResourceService<CollectionResource> {
-     constructor(serverApi: AxiosInstance, private webdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
+     constructor(serverApi: AxiosInstance, private keepWebdavClient: WebDAV, private authService: AuthService, actions: ApiActions) {
          super(serverApi, "collections", actions, [
 -            'fileCount',
 -            'fileSizeTotal',
 -            'replicationConfirmed',
 -            'replicationConfirmedAt',
 -            'storageClassesConfirmed',
 -            'storageClassesConfirmedAt',
 -            'unsignedManifestText',
 -            'version',
 +            "fileCount",
 +            "fileSizeTotal",
 +            "replicationConfirmed",
 +            "replicationConfirmedAt",
 +            "storageClassesConfirmed",
 +            "storageClassesConfirmedAt",
 +            "unsignedManifestText",
 +            "version",
          ]);
      }
  
          const payload = {
              collection: {
                  preserve_version: true,
+                 ...CommonService.mapKeys(snakeCase)(data),
+                 // Don't send uuid in payload when creating
+                 uuid: undefined,
              },
 -            replace_files: fileMap
 +            replace_files: fileMap,
          };
-         return CommonService.defaultResponse(
-             this.serverApi.put<CollectionResource>(`/${this.resourceType}/${collectionUuid}`, payload),
-             this.actions,
-             true, // mapKeys
-             showErrors
-         );
+         if (data.uuid) {
+             return CommonService.defaultResponse(
 -                this.serverApi
 -                    .put<CollectionResource>(`/${this.resourceType}/${data.uuid}`, payload),
++                this.serverApi.put<CollectionResource>(`/${this.resourceType}/${data.uuid}`, payload),
+                 this.actions,
+                 true, // mapKeys
+                 showErrors
+             );
+         } else {
+             return CommonService.defaultResponse(
 -                this.serverApi
 -                    .post<CollectionResource>(`/${this.resourceType}`, payload),
++                this.serverApi.post<CollectionResource>(`/${this.resourceType}`, payload),
+                 this.actions,
+                 true, // mapKeys
+                 showErrors
+             );
+         }
      }
  
 -    async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress, targetLocation: string = '') {
 -        if (collectionUuid === "" || files.length === 0) { return; }
 +    async uploadFiles(collectionUuid: string, files: File[], onProgress?: UploadProgress, targetLocation: string = "") {
 +        if (collectionUuid === "" || files.length === 0) {
 +            return;
 +        }
          // files have to be uploaded sequentially
          for (let idx = 0; idx < files.length; idx++) {
              await this.uploadFile(collectionUuid, files[idx], idx, onProgress, targetLocation);
      }
  
      async renameFile(collectionUuid: string, collectionPdh: string, oldPath: string, newPath: string) {
-         return this.replaceFiles(collectionUuid, {
 -        return this.replaceFiles({uuid: collectionUuid}, {
--            [this.combineFilePath([newPath])]: `${collectionPdh}${this.combineFilePath([oldPath])}`,
-             [this.combineFilePath([oldPath])]: "",
 -            [this.combineFilePath([oldPath])]: '',
--        });
++        return this.replaceFiles(
++            { uuid: collectionUuid },
++            {
++                [this.combineFilePath([newPath])]: `${collectionPdh}${this.combineFilePath([oldPath])}`,
++                [this.combineFilePath([oldPath])]: "",
++            }
++        );
      }
  
      extendFileURL = (file: CollectionDirectory | CollectionFile) => {
-         const baseUrl = this.webdavClient.getBaseUrl().endsWith("/") ? this.webdavClient.getBaseUrl().slice(0, -1) : this.webdavClient.getBaseUrl();
 -        const baseUrl = this.keepWebdavClient.getBaseUrl().endsWith('/')
++        const baseUrl = this.keepWebdavClient.getBaseUrl().endsWith("/")
+             ? this.keepWebdavClient.getBaseUrl().slice(0, -1)
+             : this.keepWebdavClient.getBaseUrl();
          const apiToken = this.authService.getApiToken();
 -        const encodedApiToken = apiToken ? encodeURI(apiToken) : '';
 +        const encodedApiToken = apiToken ? encodeURI(apiToken) : "";
          const userApiToken = `/t=${encodedApiToken}/`;
 -        const splittedPrevFileUrl = file.url.split('/');
 -        const url = `${baseUrl}/${splittedPrevFileUrl[1]}${userApiToken}${splittedPrevFileUrl.slice(2).join('/')}`;
 +        const splittedPrevFileUrl = file.url.split("/");
 +        const url = `${baseUrl}/${splittedPrevFileUrl[1]}${userApiToken}${splittedPrevFileUrl.slice(2).join("/")}`;
          return {
              ...file,
 -            url
 +            url,
          };
 -    }
 +    };
  
      async getFileContents(file: CollectionFile) {
-         return (await this.webdavClient.get(`c=${file.id}`)).response;
+         return (await this.keepWebdavClient.get(`c=${file.id}`)).response;
      }
  
 -    private async uploadFile(collectionUuid: string, file: File, fileId: number, onProgress: UploadProgress = () => { return; }, targetLocation: string = '') {
 -        const fileURL = `c=${targetLocation !== '' ? targetLocation : collectionUuid}/${file.name}`.replace('//', '/');
 +    private async uploadFile(
 +        collectionUuid: string,
 +        file: File,
 +        fileId: number,
 +        onProgress: UploadProgress = () => {
 +            return;
 +        },
 +        targetLocation: string = ""
 +    ) {
 +        const fileURL = `c=${targetLocation !== "" ? targetLocation : collectionUuid}/${file.name}`.replace("//", "/");
          const requestConfig = {
              headers: {
 -                'Content-Type': 'text/octet-stream'
 +                "Content-Type": "text/octet-stream",
              },
              onUploadProgress: (e: ProgressEvent) => {
                  onProgress(fileId, e.loaded, e.total, Date.now());
          const fileMap = optimizedFiles.reduce((obj, filePath) => {
              return {
                  ...obj,
 -                [this.combineFilePath([filePath])]: ''
 -            }
 -        }, {})
 +                [this.combineFilePath([filePath])]: "",
 +            };
 +        }, {});
  
-         return this.replaceFiles(collectionUuid, fileMap, showErrors);
 -        return this.replaceFiles({uuid: collectionUuid}, fileMap, showErrors);
++        return this.replaceFiles({ uuid: collectionUuid }, fileMap, showErrors);
      }
  
-     copyFiles(sourcePdh: string, files: string[], destinationCollectionUuid: string, destinationPath: string, showErrors?: boolean) {
 -    copyFiles(sourcePdh: string, files: string[], destinationCollection: CollectionPartialUpdateOrCreate, destinationPath: string, showErrors?: boolean) {
++    copyFiles(
++        sourcePdh: string,
++        files: string[],
++        destinationCollection: CollectionPartialUpdateOrCreate,
++        destinationPath: string,
++        showErrors?: boolean
++    ) {
          const fileMap = files.reduce((obj, sourceFile) => {
-             const sourceFileName = sourceFile.split("/").filter(Boolean).slice(-1).join("");
 -            const fileBasename = sourceFile.split('/').filter(Boolean).slice(-1).join("");
++            const fileBasename = sourceFile.split("/").filter(Boolean).slice(-1).join("");
              return {
                  ...obj,
-                 [this.combineFilePath([destinationPath, sourceFileName])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`,
 -                [this.combineFilePath([destinationPath, fileBasename])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`
++                [this.combineFilePath([destinationPath, fileBasename])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`,
              };
          }, {});
  
-         return this.replaceFiles(destinationCollectionUuid, fileMap, showErrors);
+         return this.replaceFiles(destinationCollection, fileMap, showErrors);
      }
  
 -    moveFiles(sourceUuid: string, sourcePdh: string, files: string[], destinationCollection: CollectionPartialUpdateOrCreate, destinationPath: string, showErrors?: boolean) {
 +    moveFiles(
 +        sourceUuid: string,
 +        sourcePdh: string,
 +        files: string[],
-         destinationCollectionUuid: string,
++        destinationCollection: CollectionPartialUpdateOrCreate,
 +        destinationPath: string,
 +        showErrors?: boolean
 +    ) {
-         if (sourceUuid === destinationCollectionUuid) {
+         if (sourceUuid === destinationCollection.uuid) {
+             let errors: CommonResourceServiceError[] = [];
              const fileMap = files.reduce((obj, sourceFile) => {
-                 const sourceFileName = sourceFile.split("/").filter(Boolean).slice(-1).join("");
-                 return {
-                     ...obj,
-                     [this.combineFilePath([destinationPath, sourceFileName])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`,
-                     [this.combineFilePath([sourceFile])]: "",
-                 };
 -                const fileBasename = sourceFile.split('/').filter(Boolean).slice(-1).join("");
++                const fileBasename = sourceFile.split("/").filter(Boolean).slice(-1).join("");
+                 const fileDestinationPath = this.combineFilePath([destinationPath, fileBasename]);
+                 const fileSourcePath = this.combineFilePath([sourceFile]);
+                 const fileSourceUri = `${sourcePdh}${fileSourcePath}`;
 -
+                 if (fileDestinationPath !== fileSourcePath) {
+                     return {
+                         ...obj,
+                         [fileDestinationPath]: fileSourceUri,
 -                        [fileSourcePath]: '',
++                        [fileSourcePath]: "",
+                     };
+                 } else {
+                     errors.push(CommonResourceServiceError.SOURCE_DESTINATION_CANNOT_BE_SAME);
+                     return obj;
+                 }
              }, {});
  
-             return this.replaceFiles(sourceUuid, fileMap, showErrors);
+             if (errors.length === 0) {
 -                return this.replaceFiles({uuid: sourceUuid}, fileMap, showErrors)
++                return this.replaceFiles({ uuid: sourceUuid }, fileMap, showErrors);
+             } else {
 -                return Promise.reject({errors});
++                return Promise.reject({ errors });
+             }
          } else {
-             return this.copyFiles(sourcePdh, files, destinationCollectionUuid, destinationPath, showErrors).then(() => {
 -            return this.copyFiles(sourcePdh, files, destinationCollection, destinationPath, showErrors)
 -                .then(() => {
 -                    return this.deleteFiles(sourceUuid, files, showErrors);
 -                });
++            return this.copyFiles(sourcePdh, files, destinationCollection, destinationPath, showErrors).then(() => {
 +                return this.deleteFiles(sourceUuid, files, showErrors);
 +            });
          }
      }
  
      createDirectory(collectionUuid: string, path: string, showErrors?: boolean) {
 -        const fileMap = {[this.combineFilePath([path])]: emptyCollectionPdh};
 +        const fileMap = { [this.combineFilePath([path])]: emptyCollectionPdh };
  
-         return this.replaceFiles(collectionUuid, fileMap, showErrors);
 -        return this.replaceFiles({uuid: collectionUuid}, fileMap, showErrors);
++        return this.replaceFiles({ uuid: collectionUuid }, fileMap, showErrors);
      }
 -
  }
index 6fc5d56f51626ccd4e38f99d86b4d61d038fcb48,4abfb372f915152f62761cd4440df6605df0914a..bd93649ee88d7c1bafa222a922069c54f44a3cf2
@@@ -2,31 -2,32 +2,32 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
 -import { unionize, ofType, UnionOf } from 'common/unionize';
 +import { unionize, ofType, UnionOf } from "common/unionize";
  import { ContextMenuPosition } from "./context-menu-reducer";
 -import { ContextMenuKind } from 'views-components/context-menu/context-menu';
 -import { Dispatch } from 'redux';
 -import { RootState } from 'store/store';
 -import { getResource, getResourceWithEditableStatus } from '../resources/resources';
 -import { UserResource } from 'models/user';
 -import { isSidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
 -import { extractUuidKind, ResourceKind, EditableResource, Resource } from 'models/resource';
 -import { Process } from 'store/processes/process';
 -import { RepositoryResource } from 'models/repositories';
 -import { SshKeyResource } from 'models/ssh-key';
 -import { VirtualMachinesResource } from 'models/virtual-machines';
 -import { KeepServiceResource } from 'models/keep-services';
 -import { ProcessResource } from 'models/process';
 -import { CollectionResource } from 'models/collection';
 -import { GroupClass, GroupResource } from 'models/group';
 -import { GroupContentsResource } from 'services/groups-service/groups-service';
 -import { LinkResource } from 'models/link';
 -import { resourceIsFrozen } from 'common/frozen-resources';
 -import { ProjectResource } from 'models/project';
 -import { filterCollectionFilesBySelection } from 'store/collection-panel/collection-panel-files/collection-panel-files-state';
 +import { ContextMenuKind } from "views-components/context-menu/context-menu";
 +import { Dispatch } from "redux";
 +import { RootState } from "store/store";
 +import { getResource, getResourceWithEditableStatus } from "../resources/resources";
 +import { UserResource } from "models/user";
 +import { isSidePanelTreeCategory } from "store/side-panel-tree/side-panel-tree-actions";
 +import { extractUuidKind, ResourceKind, EditableResource, Resource } from "models/resource";
 +import { Process } from "store/processes/process";
 +import { RepositoryResource } from "models/repositories";
 +import { SshKeyResource } from "models/ssh-key";
 +import { VirtualMachinesResource } from "models/virtual-machines";
 +import { KeepServiceResource } from "models/keep-services";
 +import { ProcessResource } from "models/process";
 +import { CollectionResource } from "models/collection";
 +import { GroupClass, GroupResource } from "models/group";
 +import { GroupContentsResource } from "services/groups-service/groups-service";
 +import { LinkResource } from "models/link";
 +import { resourceIsFrozen } from "common/frozen-resources";
 +import { ProjectResource } from "models/project";
++import { filterCollectionFilesBySelection } from "store/collection-panel/collection-panel-files/collection-panel-files-state";
  
  export const contextMenuActions = unionize({
 -    OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition, resource: ContextMenuResource }>(),
 -    CLOSE_CONTEXT_MENU: ofType<{}>()
 +    OPEN_CONTEXT_MENU: ofType<{ position: ContextMenuPosition; resource: ContextMenuResource }>(),
 +    CLOSE_CONTEXT_MENU: ofType<{}>(),
  });
  
  export type ContextMenuAction = UnionOf<typeof contextMenuActions>;
index 6d6a48b754fec9900e5ed09a2d7d9258253ba678,b6ff4b71acd55e25ee854421440203014971e8b6..25cb8561b1576d5b87073c3224987849d20851f6
@@@ -24,12 -24,9 +24,13 @@@ import { CommandOutputParameter } from 
  import { ContainerResource } from "models/container";
  import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
  import { FilterBuilder } from "services/api/filter-builder";
 +import { selectedToArray } from "components/multiselect-toolbar/MultiselectToolbar";
 +import { Resource, ResourceKind } from "models/resource";
 +import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+ import { CommonResourceServiceError, getCommonResourceServiceError } from "services/common-service/common-resource-service";
  
 -export const loadProcess = (containerRequestUuid: string) =>
 +export const loadProcess =
 +    (containerRequestUuid: string) =>
      async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process | undefined> => {
          let containerRequest: ContainerRequestResource | undefined = undefined;
          try {
@@@ -116,53 -112,57 +117,54 @@@ const containerFieldsNoMounts = 
      "scheduling_parameters",
      "started_at",
      "state",
+     "subrequests_cost",
      "uuid",
 -]
 +];
  
 -export const cancelRunningWorkflow = (uuid: string) =>
 -    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 -        try {
 -            const process = await services.containerRequestService.update(uuid, { priority: 0 });
 -            dispatch<any>(updateResources([process]));
 -            if (process.containerUuid) {
 -                const container = await services.containerService.get(process.containerUuid, false);
 -                dispatch<any>(updateResources([container]));
 -            }
 -            return process;
 -        } catch (e) {
 -            throw new Error('Could not cancel the process.');
 +export const cancelRunningWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    try {
 +        const process = await services.containerRequestService.update(uuid, { priority: 0 });
 +        dispatch<any>(updateResources([process]));
 +        if (process.containerUuid) {
 +            const container = await services.containerService.get(process.containerUuid, false);
 +            dispatch<any>(updateResources([container]));
          }
 -    };
 +        return process;
 +    } catch (e) {
 +        throw new Error("Could not cancel the process.");
 +    }
 +};
  
 -export const resumeOnHoldWorkflow = (uuid: string) =>
 -    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 -        try {
 -            const process = await services.containerRequestService.update(uuid, { priority: 500 });
 -            dispatch<any>(updateResources([process]));
 -            if (process.containerUuid) {
 -                const container = await services.containerService.get(process.containerUuid, false);
 -                dispatch<any>(updateResources([container]));
 -            }
 -            return process;
 -        } catch (e) {
 -            throw new Error('Could not resume the process.');
 +export const resumeOnHoldWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    try {
 +        const process = await services.containerRequestService.update(uuid, { priority: 500 });
 +        dispatch<any>(updateResources([process]));
 +        if (process.containerUuid) {
 +            const container = await services.containerService.get(process.containerUuid, false);
 +            dispatch<any>(updateResources([container]));
          }
 -    };
 +        return process;
 +    } catch (e) {
 +        throw new Error("Could not resume the process.");
 +    }
 +};
  
 -export const startWorkflow = (uuid: string) =>
 -    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 -        try {
 -            const process = await services.containerRequestService.update(uuid, { state: ContainerRequestState.COMMITTED });
 -            if (process) {
 -                dispatch<any>(updateResources([process]));
 -                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process started', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
 -            } else {
 -                dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
 -            }
 -        } catch (e) {
 +export const startWorkflow = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    try {
 +        const process = await services.containerRequestService.update(uuid, { state: ContainerRequestState.COMMITTED });
 +        if (process) {
 +            dispatch<any>(updateResources([process]));
 +            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Process started", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
 +        } else {
              dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
          }
 -    };
 +    } catch (e) {
 +        dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
 +    }
 +};
  
 -export const reRunProcess = (processUuid: string, workflowUuid: string) =>
 -    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +export const reRunProcess =
 +    (processUuid: string, workflowUuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
          const process = getResource<any>(processUuid)(getState().resources);
          const workflows = getState().runProcessPanel.searchWorkflows;
          const workflow = workflows.find(workflow => workflow.uuid === workflowUuid);
@@@ -272,61 -262,48 +274,70 @@@ export const getInputCollectionMounts 
  };
  
  export const getOutputParameters = (data: any): CommandOutputParameter[] => {
 -    if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) { return []; }
 +    if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
 +        return [];
 +    }
      const outputs = getWorkflowOutputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
 -    return outputs ? outputs.map(
 -        (it: any) => (
 -            {
 -                type: it.type,
 -                id: it.id,
 -                label: it.label,
 -                doc: it.doc
 -            }
 -        )
 -    ) : [];
 +    return outputs
 +        ? outputs.map((it: any) => ({
 +              type: it.type,
 +              id: it.id,
 +              label: it.label,
 +              doc: it.doc,
 +          }))
 +        : [];
  };
  
 -export const openRemoveProcessDialog = (uuid: string) =>
 -    (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 -        dispatch(dialogActions.OPEN_DIALOG({
 -            id: REMOVE_PROCESS_DIALOG,
 -            data: {
 -                title: 'Remove process permanently',
 -                text: 'Are you sure you want to remove this process?',
 -                confirmButtonLabel: 'Remove',
 -                uuid
 -            }
 -        }));
 +export const openRemoveProcessDialog =
 +    (resource: ContextMenuResource, numOfProcesses: Number) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +        const confirmationText =
 +            numOfProcesses === 1
 +                ? "Are you sure you want to remove this process?"
 +                : `Are you sure you want to remove these ${numOfProcesses} processes?`;
 +        const titleText = numOfProcesses === 1 ? "Remove process permanently" : "Remove processes permanently";
 +
 +        dispatch(
 +            dialogActions.OPEN_DIALOG({
 +                id: REMOVE_PROCESS_DIALOG,
 +                data: {
 +                    title: titleText,
 +                    text: confirmationText,
 +                    confirmButtonLabel: "Remove",
 +                    uuid: resource.uuid,
 +                    resource,
 +                },
 +            })
 +        );
      };
  
 -export const REMOVE_PROCESS_DIALOG = 'removeProcessDialog';
 +export const REMOVE_PROCESS_DIALOG = "removeProcessDialog";
  
 -export const removeProcessPermanently = (uuid: string) =>
 -    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +export const removeProcessPermanently = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
 +    const resource = getState().dialog.removeProcessDialog.data.resource;
 +    const checkedList = getState().multiselect.checkedList;
 +
 +    const uuidsToRemove: string[] = resource.isSingle ? [resource.uuid] : selectedToArray(checkedList);
 +
 +    //if no items in checkedlist, default to normal context menu behavior
 +    if (!uuidsToRemove.length) uuidsToRemove.push(uuid);
 +
 +    const processesToRemove = uuidsToRemove
 +        .map(uuid => getResource(uuid)(getState().resources) as Resource)
 +        .filter(resource => resource.kind === ResourceKind.PROCESS);
 +
 +    for (const process of processesToRemove) {
-         dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removing ...", kind: SnackbarKind.INFO }));
-         await services.containerRequestService.delete(process.uuid);
-         dispatch(projectPanelActions.REQUEST_ITEMS());
-         dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removed.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+         try {
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
++            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removing ...", kind: SnackbarKind.INFO }));
+             await services.containerRequestService.delete(uuid, false);
+             dispatch(projectPanelActions.REQUEST_ITEMS());
 -            dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removed.', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
++            dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removed.", hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+         } catch (e) {
+             const error = getCommonResourceServiceError(e);
+             if (error === CommonResourceServiceError.PERMISSION_ERROR_FORBIDDEN) {
+                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Access denied`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+             } else {
+                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: `Deletion failed`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
+             }
+         }
 -    };
 +    }
 +};
index f4b82cb9144aa668a9c16e5b4198956a4d9020c3,913207c35769ddb6cb39608be5f6b988d0bb7499..e0af6047f4cef1742544df36be355b36b448f68e
@@@ -2,90 -2,82 +2,91 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
- import { routerMiddleware, routerReducer } from 'react-router-redux';
- import thunkMiddleware from 'redux-thunk';
- import { History } from 'history';
- import { handleRedirects } from '../common/redirect-to';
- import { authReducer } from './auth/auth-reducer';
- import { authMiddleware } from './auth/auth-middleware';
- import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
- import { detailsPanelReducer } from './details-panel/details-panel-reducer';
- import { contextMenuReducer } from './context-menu/context-menu-reducer';
- import { reducer as formReducer } from 'redux-form';
- import { favoritesReducer } from './favorites/favorites-reducer';
- import { snackbarReducer } from './snackbar/snackbar-reducer';
- import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
- import { dataExplorerMiddleware } from './data-explorer/data-explorer-middleware';
- import { FAVORITE_PANEL_ID } from './favorite-panel/favorite-panel-action';
- import { PROJECT_PANEL_ID } from './project-panel/project-panel-action';
- import { ProjectPanelMiddlewareService } from './project-panel/project-panel-middleware-service';
- import { FavoritePanelMiddlewareService } from './favorite-panel/favorite-panel-middleware-service';
- import { AllProcessesPanelMiddlewareService } from './all-processes-panel/all-processes-panel-middleware-service';
- import { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
- import { dialogReducer } from './dialog/dialog-reducer';
- import { ServiceRepository } from 'services/services';
- import { treePickerReducer, treePickerSearchReducer } from './tree-picker/tree-picker-reducer';
- import { treePickerSearchMiddleware } from './tree-picker/tree-picker-middleware';
- import { resourcesReducer } from 'store/resources/resources-reducer';
- import { propertiesReducer } from './properties/properties-reducer';
- import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
- import { TrashPanelMiddlewareService } from 'store/trash-panel/trash-panel-middleware-service';
- import { TRASH_PANEL_ID } from 'store/trash-panel/trash-panel-action';
- import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel-reducer';
- import { processPanelReducer } from 'store/process-panel/process-panel-reducer';
- import { SHARED_WITH_ME_PANEL_ID } from 'store/shared-with-me-panel/shared-with-me-panel-actions';
- import { SharedWithMeMiddlewareService } from './shared-with-me-panel/shared-with-me-middleware-service';
- import { progressIndicatorReducer } from './progress-indicator/progress-indicator-reducer';
- import { runProcessPanelReducer } from 'store/run-process-panel/run-process-panel-reducer';
- import { WorkflowMiddlewareService } from './workflow-panel/workflow-middleware-service';
- import { WORKFLOW_PANEL_ID } from './workflow-panel/workflow-panel-actions';
- import { appInfoReducer } from 'store/app-info/app-info-reducer';
- import { searchBarReducer } from './search-bar/search-bar-reducer';
- import { SEARCH_RESULTS_PANEL_ID } from 'store/search-results-panel/search-results-panel-actions';
- import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
- import { virtualMachinesReducer } from 'store/virtual-machines/virtual-machines-reducer';
- import { repositoriesReducer } from 'store/repositories/repositories-reducer';
- import { keepServicesReducer } from 'store/keep-services/keep-services-reducer';
- import { UserMiddlewareService } from 'store/users/user-panel-middleware-service';
- import { USERS_PANEL_ID } from 'store/users/users-actions';
- import { UserProfileGroupsMiddlewareService } from 'store/user-profile/user-profile-groups-middleware-service';
- import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions';
- import { GroupsPanelMiddlewareService } from 'store/groups-panel/groups-panel-middleware-service';
- import { GROUPS_PANEL_ID } from 'store/groups-panel/groups-panel-actions';
- import { GroupDetailsPanelMembersMiddlewareService } from 'store/group-details-panel/group-details-panel-members-middleware-service';
- import { GroupDetailsPanelPermissionsMiddlewareService } from 'store/group-details-panel/group-details-panel-permissions-middleware-service';
- import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID } from 'store/group-details-panel/group-details-panel-actions';
- import { LINK_PANEL_ID } from 'store/link-panel/link-panel-actions';
- import { LinkMiddlewareService } from 'store/link-panel/link-panel-middleware-service';
- import { API_CLIENT_AUTHORIZATION_PANEL_ID } from 'store/api-client-authorizations/api-client-authorizations-actions';
- import { ApiClientAuthorizationMiddlewareService } from 'store/api-client-authorizations/api-client-authorizations-middleware-service';
- import { PublicFavoritesMiddlewareService } from 'store/public-favorites-panel/public-favorites-middleware-service';
- import { PUBLIC_FAVORITE_PANEL_ID } from 'store/public-favorites-panel/public-favorites-action';
- import { publicFavoritesReducer } from 'store/public-favorites/public-favorites-reducer';
- import { linkAccountPanelReducer } from './link-account-panel/link-account-panel-reducer';
- import { CollectionsWithSameContentAddressMiddlewareService } from 'store/collections-content-address-panel/collections-content-address-middleware-service';
- import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from 'store/collections-content-address-panel/collections-content-address-panel-actions';
- import { ownerNameReducer } from 'store/owner-name/owner-name-reducer';
- import { SubprocessMiddlewareService } from 'store/subprocess-panel/subprocess-panel-middleware-service';
- import { SUBPROCESS_PANEL_ID } from 'store/subprocess-panel/subprocess-panel-actions';
- import { ALL_PROCESSES_PANEL_ID } from './all-processes-panel/all-processes-panel-action';
- import { Config } from 'common/config';
- import { pluginConfig } from 'plugins';
- import { MiddlewareListReducer } from 'common/plugintypes';
- import { tooltipsMiddleware } from './tooltips/tooltips-middleware';
- import { sidePanelReducer } from './side-panel/side-panel-reducer';
- import { bannerReducer } from './banner/banner-reducer';
- import { multiselectReducer } from './multiselect/multiselect-reducer';
 -import { createStore, applyMiddleware, Middleware, combineReducers, Store, Action, Dispatch } from 'redux';
++import { createStore, applyMiddleware, compose, Middleware, combineReducers, Store, Action, Dispatch } from "redux";
+ import { routerMiddleware, routerReducer } from "react-router-redux";
 -import thunkMiddleware from 'redux-thunk';
++import thunkMiddleware from "redux-thunk";
+ import { History } from "history";
 -import { handleRedirects } from '../common/redirect-to';
++import { handleRedirects } from "../common/redirect-to";
+ import { authReducer } from "./auth/auth-reducer";
+ import { authMiddleware } from "./auth/auth-middleware";
 -import { dataExplorerReducer } from './data-explorer/data-explorer-reducer';
 -import { detailsPanelReducer } from './details-panel/details-panel-reducer';
 -import { contextMenuReducer } from './context-menu/context-menu-reducer';
 -import { reducer as formReducer } from 'redux-form';
 -import { favoritesReducer } from './favorites/favorites-reducer';
 -import { snackbarReducer } from './snackbar/snackbar-reducer';
 -import { collectionPanelFilesReducer } from './collection-panel/collection-panel-files/collection-panel-files-reducer';
++import { dataExplorerReducer } from "./data-explorer/data-explorer-reducer";
++import { detailsPanelReducer } from "./details-panel/details-panel-reducer";
++import { contextMenuReducer } from "./context-menu/context-menu-reducer";
++import { reducer as formReducer } from "redux-form";
++import { favoritesReducer } from "./favorites/favorites-reducer";
++import { snackbarReducer } from "./snackbar/snackbar-reducer";
++import { collectionPanelFilesReducer } from "./collection-panel/collection-panel-files/collection-panel-files-reducer";
+ import { dataExplorerMiddleware } from "./data-explorer/data-explorer-middleware";
+ import { FAVORITE_PANEL_ID } from "./favorite-panel/favorite-panel-action";
+ import { PROJECT_PANEL_ID } from "./project-panel/project-panel-action";
+ import { ProjectPanelMiddlewareService } from "./project-panel/project-panel-middleware-service";
+ import { FavoritePanelMiddlewareService } from "./favorite-panel/favorite-panel-middleware-service";
+ import { AllProcessesPanelMiddlewareService } from "./all-processes-panel/all-processes-panel-middleware-service";
 -import { collectionPanelReducer } from './collection-panel/collection-panel-reducer';
 -import { dialogReducer } from './dialog/dialog-reducer';
++import { collectionPanelReducer } from "./collection-panel/collection-panel-reducer";
++import { dialogReducer } from "./dialog/dialog-reducer";
+ import { ServiceRepository } from "services/services";
 -import { treePickerReducer, treePickerSearchReducer } from './tree-picker/tree-picker-reducer';
 -import { treePickerSearchMiddleware } from './tree-picker/tree-picker-middleware';
 -import { resourcesReducer } from 'store/resources/resources-reducer';
 -import { propertiesReducer } from './properties/properties-reducer';
 -import { fileUploaderReducer } from './file-uploader/file-uploader-reducer';
++import { treePickerReducer, treePickerSearchReducer } from "./tree-picker/tree-picker-reducer";
++import { treePickerSearchMiddleware } from "./tree-picker/tree-picker-middleware";
++import { resourcesReducer } from "store/resources/resources-reducer";
++import { propertiesReducer } from "./properties/properties-reducer";
++import { fileUploaderReducer } from "./file-uploader/file-uploader-reducer";
+ import { TrashPanelMiddlewareService } from "store/trash-panel/trash-panel-middleware-service";
+ import { TRASH_PANEL_ID } from "store/trash-panel/trash-panel-action";
 -import { processLogsPanelReducer } from './process-logs-panel/process-logs-panel-reducer';
 -import { processPanelReducer } from 'store/process-panel/process-panel-reducer';
 -import { SHARED_WITH_ME_PANEL_ID } from 'store/shared-with-me-panel/shared-with-me-panel-actions';
 -import { SharedWithMeMiddlewareService } from './shared-with-me-panel/shared-with-me-middleware-service';
 -import { progressIndicatorReducer } from './progress-indicator/progress-indicator-reducer';
 -import { runProcessPanelReducer } from 'store/run-process-panel/run-process-panel-reducer';
 -import { WorkflowMiddlewareService } from './workflow-panel/workflow-middleware-service';
 -import { WORKFLOW_PANEL_ID } from './workflow-panel/workflow-panel-actions';
 -import { appInfoReducer } from 'store/app-info/app-info-reducer';
 -import { searchBarReducer } from './search-bar/search-bar-reducer';
 -import { SEARCH_RESULTS_PANEL_ID } from 'store/search-results-panel/search-results-panel-actions';
 -import { SearchResultsMiddlewareService } from './search-results-panel/search-results-middleware-service';
++import { processLogsPanelReducer } from "./process-logs-panel/process-logs-panel-reducer";
++import { processPanelReducer } from "store/process-panel/process-panel-reducer";
++import { SHARED_WITH_ME_PANEL_ID } from "store/shared-with-me-panel/shared-with-me-panel-actions";
++import { SharedWithMeMiddlewareService } from "./shared-with-me-panel/shared-with-me-middleware-service";
++import { progressIndicatorReducer } from "./progress-indicator/progress-indicator-reducer";
++import { runProcessPanelReducer } from "store/run-process-panel/run-process-panel-reducer";
++import { WorkflowMiddlewareService } from "./workflow-panel/workflow-middleware-service";
++import { WORKFLOW_PANEL_ID } from "./workflow-panel/workflow-panel-actions";
++import { appInfoReducer } from "store/app-info/app-info-reducer";
++import { searchBarReducer } from "./search-bar/search-bar-reducer";
++import { SEARCH_RESULTS_PANEL_ID } from "store/search-results-panel/search-results-panel-actions";
++import { SearchResultsMiddlewareService } from "./search-results-panel/search-results-middleware-service";
+ import { virtualMachinesReducer } from "store/virtual-machines/virtual-machines-reducer";
 -import { repositoriesReducer } from 'store/repositories/repositories-reducer';
 -import { keepServicesReducer } from 'store/keep-services/keep-services-reducer';
 -import { UserMiddlewareService } from 'store/users/user-panel-middleware-service';
 -import { USERS_PANEL_ID } from 'store/users/users-actions';
 -import { UserProfileGroupsMiddlewareService } from 'store/user-profile/user-profile-groups-middleware-service';
 -import { USER_PROFILE_PANEL_ID } from 'store/user-profile/user-profile-actions'
 -import { GroupsPanelMiddlewareService } from 'store/groups-panel/groups-panel-middleware-service';
 -import { GROUPS_PANEL_ID } from 'store/groups-panel/groups-panel-actions';
 -import { GroupDetailsPanelMembersMiddlewareService } from 'store/group-details-panel/group-details-panel-members-middleware-service';
 -import { GroupDetailsPanelPermissionsMiddlewareService } from 'store/group-details-panel/group-details-panel-permissions-middleware-service';
 -import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID } from 'store/group-details-panel/group-details-panel-actions';
 -import { LINK_PANEL_ID } from 'store/link-panel/link-panel-actions';
 -import { LinkMiddlewareService } from 'store/link-panel/link-panel-middleware-service';
 -import { API_CLIENT_AUTHORIZATION_PANEL_ID } from 'store/api-client-authorizations/api-client-authorizations-actions';
 -import { ApiClientAuthorizationMiddlewareService } from 'store/api-client-authorizations/api-client-authorizations-middleware-service';
 -import { PublicFavoritesMiddlewareService } from 'store/public-favorites-panel/public-favorites-middleware-service';
 -import { PUBLIC_FAVORITE_PANEL_ID } from 'store/public-favorites-panel/public-favorites-action';
 -import { publicFavoritesReducer } from 'store/public-favorites/public-favorites-reducer';
 -import { linkAccountPanelReducer } from './link-account-panel/link-account-panel-reducer';
 -import { CollectionsWithSameContentAddressMiddlewareService } from 'store/collections-content-address-panel/collections-content-address-middleware-service';
 -import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from 'store/collections-content-address-panel/collections-content-address-panel-actions';
 -import { ownerNameReducer } from 'store/owner-name/owner-name-reducer';
 -import { SubprocessMiddlewareService } from 'store/subprocess-panel/subprocess-panel-middleware-service';
 -import { SUBPROCESS_PANEL_ID } from 'store/subprocess-panel/subprocess-panel-actions';
 -import { ALL_PROCESSES_PANEL_ID } from './all-processes-panel/all-processes-panel-action';
 -import { Config } from 'common/config';
 -import { pluginConfig } from 'plugins';
 -import { MiddlewareListReducer } from 'common/plugintypes';
 -import { tooltipsMiddleware } from './tooltips/tooltips-middleware';
 -import { sidePanelReducer } from './side-panel/side-panel-reducer'
 -import { bannerReducer } from './banner/banner-reducer';
 -import { composeWithDevTools } from 'redux-devtools-extension';
++import { repositoriesReducer } from "store/repositories/repositories-reducer";
++import { keepServicesReducer } from "store/keep-services/keep-services-reducer";
++import { UserMiddlewareService } from "store/users/user-panel-middleware-service";
++import { USERS_PANEL_ID } from "store/users/users-actions";
++import { UserProfileGroupsMiddlewareService } from "store/user-profile/user-profile-groups-middleware-service";
++import { USER_PROFILE_PANEL_ID } from "store/user-profile/user-profile-actions";
++import { GroupsPanelMiddlewareService } from "store/groups-panel/groups-panel-middleware-service";
++import { GROUPS_PANEL_ID } from "store/groups-panel/groups-panel-actions";
++import { GroupDetailsPanelMembersMiddlewareService } from "store/group-details-panel/group-details-panel-members-middleware-service";
++import { GroupDetailsPanelPermissionsMiddlewareService } from "store/group-details-panel/group-details-panel-permissions-middleware-service";
++import { GROUP_DETAILS_MEMBERS_PANEL_ID, GROUP_DETAILS_PERMISSIONS_PANEL_ID } from "store/group-details-panel/group-details-panel-actions";
++import { LINK_PANEL_ID } from "store/link-panel/link-panel-actions";
++import { LinkMiddlewareService } from "store/link-panel/link-panel-middleware-service";
++import { API_CLIENT_AUTHORIZATION_PANEL_ID } from "store/api-client-authorizations/api-client-authorizations-actions";
++import { ApiClientAuthorizationMiddlewareService } from "store/api-client-authorizations/api-client-authorizations-middleware-service";
++import { PublicFavoritesMiddlewareService } from "store/public-favorites-panel/public-favorites-middleware-service";
++import { PUBLIC_FAVORITE_PANEL_ID } from "store/public-favorites-panel/public-favorites-action";
++import { publicFavoritesReducer } from "store/public-favorites/public-favorites-reducer";
++import { linkAccountPanelReducer } from "./link-account-panel/link-account-panel-reducer";
++import { CollectionsWithSameContentAddressMiddlewareService } from "store/collections-content-address-panel/collections-content-address-middleware-service";
++import { COLLECTIONS_CONTENT_ADDRESS_PANEL_ID } from "store/collections-content-address-panel/collections-content-address-panel-actions";
++import { ownerNameReducer } from "store/owner-name/owner-name-reducer";
++import { SubprocessMiddlewareService } from "store/subprocess-panel/subprocess-panel-middleware-service";
++import { SUBPROCESS_PANEL_ID } from "store/subprocess-panel/subprocess-panel-actions";
++import { ALL_PROCESSES_PANEL_ID } from "./all-processes-panel/all-processes-panel-action";
++import { Config } from "common/config";
++import { pluginConfig } from "plugins";
++import { MiddlewareListReducer } from "common/plugintypes";
++import { tooltipsMiddleware } from "./tooltips/tooltips-middleware";
++import { sidePanelReducer } from "./side-panel/side-panel-reducer";
++import { bannerReducer } from "./banner/banner-reducer";
++import { multiselectReducer } from "./multiselect/multiselect-reducer";
 +
 +declare global {
 +    interface Window {
 +        __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
 +    }
 +}
 +
- const composeEnhancers = (process.env.NODE_ENV === 'development' && window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
++const composeEnhancers = (process.env.NODE_ENV === "development" && window && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose;
++import { composeWithDevTools } from "redux-devtools-extension";
  
  export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
  
@@@ -94,26 -86,57 +95,32 @@@ export type RootStore = Store<RootState
  export function configureStore(history: History, services: ServiceRepository, config: Config): RootStore {
      const rootReducer = createRootReducer(services);
  
 -    const projectPanelMiddleware = dataExplorerMiddleware(
 -        new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID)
 -    );
 -    const favoritePanelMiddleware = dataExplorerMiddleware(
 -        new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID)
 -    );
 -    const allProcessessPanelMiddleware = dataExplorerMiddleware(
 -        new AllProcessesPanelMiddlewareService(services, ALL_PROCESSES_PANEL_ID)
 -    );
 -    const trashPanelMiddleware = dataExplorerMiddleware(
 -        new TrashPanelMiddlewareService(services, TRASH_PANEL_ID)
 -    );
 -    const searchResultsPanelMiddleware = dataExplorerMiddleware(
 -        new SearchResultsMiddlewareService(services, SEARCH_RESULTS_PANEL_ID)
 -    );
 -    const sharedWithMePanelMiddleware = dataExplorerMiddleware(
 -        new SharedWithMeMiddlewareService(services, SHARED_WITH_ME_PANEL_ID)
 -    );
 -    const workflowPanelMiddleware = dataExplorerMiddleware(
 -        new WorkflowMiddlewareService(services, WORKFLOW_PANEL_ID)
 -    );
 -    const userPanelMiddleware = dataExplorerMiddleware(
 -        new UserMiddlewareService(services, USERS_PANEL_ID)
 -    );
 -    const userProfileGroupsMiddleware = dataExplorerMiddleware(
 -        new UserProfileGroupsMiddlewareService(services, USER_PROFILE_PANEL_ID)
 -    );
 -    const groupsPanelMiddleware = dataExplorerMiddleware(
 -        new GroupsPanelMiddlewareService(services, GROUPS_PANEL_ID)
 -    );
 +    const projectPanelMiddleware = dataExplorerMiddleware(new ProjectPanelMiddlewareService(services, PROJECT_PANEL_ID));
 +    const favoritePanelMiddleware = dataExplorerMiddleware(new FavoritePanelMiddlewareService(services, FAVORITE_PANEL_ID));
 +    const allProcessessPanelMiddleware = dataExplorerMiddleware(new AllProcessesPanelMiddlewareService(services, ALL_PROCESSES_PANEL_ID));
 +    const trashPanelMiddleware = dataExplorerMiddleware(new TrashPanelMiddlewareService(services, TRASH_PANEL_ID));
 +    const searchResultsPanelMiddleware = dataExplorerMiddleware(new SearchResultsMiddlewareService(services, SEARCH_RESULTS_PANEL_ID));
 +    const sharedWithMePanelMiddleware = dataExplorerMiddleware(new SharedWithMeMiddlewareService(services, SHARED_WITH_ME_PANEL_ID));
 +    const workflowPanelMiddleware = dataExplorerMiddleware(new WorkflowMiddlewareService(services, WORKFLOW_PANEL_ID));
 +    const userPanelMiddleware = dataExplorerMiddleware(new UserMiddlewareService(services, USERS_PANEL_ID));
 +    const userProfileGroupsMiddleware = dataExplorerMiddleware(new UserProfileGroupsMiddlewareService(services, USER_PROFILE_PANEL_ID));
 +    const groupsPanelMiddleware = dataExplorerMiddleware(new GroupsPanelMiddlewareService(services, GROUPS_PANEL_ID));
-     const groupDetailsPanelMembersMiddleware = dataExplorerMiddleware(new GroupDetailsPanelMembersMiddlewareService(services, GROUP_DETAILS_MEMBERS_PANEL_ID));
+     const groupDetailsPanelMembersMiddleware = dataExplorerMiddleware(
+         new GroupDetailsPanelMembersMiddlewareService(services, GROUP_DETAILS_MEMBERS_PANEL_ID)
+     );
      const groupDetailsPanelPermissionsMiddleware = dataExplorerMiddleware(
          new GroupDetailsPanelPermissionsMiddlewareService(services, GROUP_DETAILS_PERMISSIONS_PANEL_ID)
      );
 -    const linkPanelMiddleware = dataExplorerMiddleware(
 -        new LinkMiddlewareService(services, LINK_PANEL_ID)
 -    );
 +    const linkPanelMiddleware = dataExplorerMiddleware(new LinkMiddlewareService(services, LINK_PANEL_ID));
-     const apiClientAuthorizationMiddlewareService = dataExplorerMiddleware(new ApiClientAuthorizationMiddlewareService(services, API_CLIENT_AUTHORIZATION_PANEL_ID));
+     const apiClientAuthorizationMiddlewareService = dataExplorerMiddleware(
+         new ApiClientAuthorizationMiddlewareService(services, API_CLIENT_AUTHORIZATION_PANEL_ID)
+     );
 -    const publicFavoritesMiddleware = dataExplorerMiddleware(
 -        new PublicFavoritesMiddlewareService(services, PUBLIC_FAVORITE_PANEL_ID)
 -    );
 +    const publicFavoritesMiddleware = dataExplorerMiddleware(new PublicFavoritesMiddlewareService(services, PUBLIC_FAVORITE_PANEL_ID));
-     const collectionsContentAddress = dataExplorerMiddleware(new CollectionsWithSameContentAddressMiddlewareService(services, COLLECTIONS_CONTENT_ADDRESS_PANEL_ID));
+     const collectionsContentAddress = dataExplorerMiddleware(
+         new CollectionsWithSameContentAddressMiddlewareService(services, COLLECTIONS_CONTENT_ADDRESS_PANEL_ID)
+     );
 -    const subprocessMiddleware = dataExplorerMiddleware(
 -        new SubprocessMiddlewareService(services, SUBPROCESS_PANEL_ID)
 -    );
 +    const subprocessMiddleware = dataExplorerMiddleware(new SubprocessMiddlewareService(services, SUBPROCESS_PANEL_ID));
 +
      const redirectToMiddleware = (store: any) => (next: any) => (action: any) => {
          const state = store.getState();
  
  
      middlewares = pluginConfig.middlewares.reduce(reduceMiddlewaresFn, middlewares);
  
-     const enhancer = composeEnhancers(applyMiddleware(redirectToMiddleware, ...middlewares));
 -    const enhancer = composeWithDevTools({/* options */ })(applyMiddleware(redirectToMiddleware, ...middlewares));
++    const enhancer = composeWithDevTools({
++        /* options */
++    })(applyMiddleware(redirectToMiddleware, ...middlewares));
      return createStore(rootReducer, enhancer);
  }
  
index 4e27226dc01cfd1dcdcd3c68dd4a992879a128d4,a3c3a0969b2185224b340e7eabe2c9211a629568..c6f1d1b1e6c5a6a2141b69673c0e42b7600cdd91
@@@ -2,18 -2,20 +2,13 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
 -import { Dispatch } from 'redux';
 -import { RootState } from 'store/store';
 -import { getUserUuid } from 'common/getuser';
 -import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 -import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 -import {
 -    favoritePanelActions,
 -    loadFavoritePanel,
 -} from 'store/favorite-panel/favorite-panel-action';
 -import {
 -    getProjectPanelCurrentUuid,
 -    projectPanelActions,
 -    setIsProjectPanelTrashed,
 -} from 'store/project-panel/project-panel-action';
 +import { Dispatch } from "redux";
 +import { RootState } from "store/store";
 +import { getUserUuid } from "common/getuser";
 +import { loadDetailsPanel } from "store/details-panel/details-panel-action";
 +import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 +import { favoritePanelActions, loadFavoritePanel } from "store/favorite-panel/favorite-panel-action";
- import {
-     getProjectPanelCurrentUuid,
-     openProjectPanel,
-     projectPanelActions,
-     setIsProjectPanelTrashed,
- } from "store/project-panel/project-panel-action";
++import { getProjectPanelCurrentUuid, projectPanelActions, setIsProjectPanelTrashed } from "store/project-panel/project-panel-action";
  import {
      activateSidePanelTreeItem,
      initSidePanelTree,
@@@ -784,22 -861,32 +779,21 @@@ export const loadGroupDetailsPanel = (g
          dispatch(groupDetailsPanelActions.loadGroupDetailsPanel(groupUuid));
      });
  
 -const finishLoadingProject =
 -    (project: GroupContentsResource | string) =>
 -        async (dispatch: Dispatch<any>) => {
 -            const uuid = typeof project === 'string' ? project : project.uuid;
 -            dispatch(loadDetailsPanel(uuid));
 -            if (typeof project !== 'string') {
 -                dispatch(updateResources([project]));
 -            }
 -        };
 -
 -const loadGroupContentsResource = async (params: {
 -    uuid: string;
 -    userUuid: string;
 -    services: ServiceRepository;
 -}) => {
 -    const filters = new FilterBuilder()
 -        .addEqual('uuid', params.uuid)
 -        .getFilters();
 -    const { items } = await params.services.groupsService.contents(
 -        params.userUuid,
 -        {
 -            filters,
 -            recursive: true,
 -            includeTrash: true,
 -        }
 -    );
 +const finishLoadingProject = (project: GroupContentsResource | string) => async (dispatch: Dispatch<any>) => {
 +    const uuid = typeof project === "string" ? project : project.uuid;
-     dispatch(openProjectPanel(uuid));
 +    dispatch(loadDetailsPanel(uuid));
 +    if (typeof project !== "string") {
 +        dispatch(updateResources([project]));
 +    }
 +};
 +
 +const loadGroupContentsResource = async (params: { uuid: string; userUuid: string; services: ServiceRepository }) => {
 +    const filters = new FilterBuilder().addEqual("uuid", params.uuid).getFilters();
 +    const { items } = await params.services.groupsService.contents(params.userUuid, {
 +        filters,
 +        recursive: true,
 +        includeTrash: true,
 +    });
      const resource = items.shift();
      let handler: GroupContentsHandler;
      if (resource) {
index 03674665ded13609382a580705284e4d4c69e415,d0758d700a578d98caf1b437ddc8dcab829fb2d4..fb158a826d58904dd055d8b5b1e4b22aa3f2e469
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import { ContextMenuActionSet } from '../context-menu-action-set';
- import { RemoveIcon, RenameIcon } from 'components/icon/icon';
- import { DownloadCollectionFileAction } from '../actions/download-collection-file-action';
- import { openFileRemoveDialog, openRenameFileDialog } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
- import { CollectionFileViewerAction } from 'views-components/context-menu/actions/collection-file-viewer-action';
- import { CollectionCopyToClipboardAction } from '../actions/collection-copy-to-clipboard-action';
+ import { ContextMenuActionSet } from "../context-menu-action-set";
+ import { FileCopyIcon, FileMoveIcon, RemoveIcon, RenameIcon } from "components/icon/icon";
+ import { DownloadCollectionFileAction } from "../actions/download-collection-file-action";
 -import { openFileRemoveDialog, openRenameFileDialog } from 'store/collection-panel/collection-panel-files/collection-panel-files-actions';
 -import { CollectionFileViewerAction } from 'views-components/context-menu/actions/collection-file-viewer-action';
++import { openFileRemoveDialog, openRenameFileDialog } from "store/collection-panel/collection-panel-files/collection-panel-files-actions";
++import { CollectionFileViewerAction } from "views-components/context-menu/actions/collection-file-viewer-action";
+ import { CollectionCopyToClipboardAction } from "../actions/collection-copy-to-clipboard-action";
 -import { openCollectionPartialMoveToExistingCollectionDialog, openCollectionPartialMoveToNewCollectionDialog } from "store/collections/collection-partial-move-actions";
 -import { openCollectionPartialCopyToExistingCollectionDialog, openCollectionPartialCopyToNewCollectionDialog } from "store/collections/collection-partial-copy-actions";
++import {
++    openCollectionPartialMoveToExistingCollectionDialog,
++    openCollectionPartialMoveToNewCollectionDialog,
++} from "store/collections/collection-partial-move-actions";
++import {
++    openCollectionPartialCopyToExistingCollectionDialog,
++    openCollectionPartialCopyToNewCollectionDialog,
++} from "store/collections/collection-partial-copy-actions";
  
 -export const readOnlyCollectionDirectoryItemActionSet: ContextMenuActionSet = [[
 -    {
 -        name: "Copy item into new collection",
 -        icon: FileCopyIcon,
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openCollectionPartialCopyToNewCollectionDialog(resource));
 -        }
 -    },
 -    {
 -        name: "Copy item into existing collection",
 -        icon: FileCopyIcon,
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openCollectionPartialCopyToExistingCollectionDialog(resource));
 -        }
 -    },
 -    {
 -        component: CollectionFileViewerAction,
 -        execute: () => { return; },
 -    },
 -    {
 -        component: CollectionCopyToClipboardAction,
 -        execute: () => { return; },
 -    }
 -]];
 +export const readOnlyCollectionDirectoryItemActionSet: ContextMenuActionSet = [
 +    [
++        {
++            name: "Copy item into new collection",
++            icon: FileCopyIcon,
++            execute: (dispatch, resources) => {
++                dispatch<any>(openCollectionPartialCopyToNewCollectionDialog(resources[0]));
++            },
++        },
++        {
++            name: "Copy item into existing collection",
++            icon: FileCopyIcon,
++            execute: (dispatch, resources) => {
++                dispatch<any>(openCollectionPartialCopyToExistingCollectionDialog(resources[0]));
++            },
++        },
 +        {
 +            component: CollectionFileViewerAction,
 +            execute: () => {
 +                return;
 +            },
 +        },
 +        {
 +            component: CollectionCopyToClipboardAction,
 +            execute: () => {
 +                return;
 +            },
 +        },
 +    ],
 +];
  
 -export const readOnlyCollectionFileItemActionSet: ContextMenuActionSet = [[
 -    {
 -        component: DownloadCollectionFileAction,
 -        execute: () => { return; }
 -    },
 -    ...readOnlyCollectionDirectoryItemActionSet.reduce((prev, next) => prev.concat(next), []),
 -]];
 +export const readOnlyCollectionFileItemActionSet: ContextMenuActionSet = [
 +    [
 +        {
 +            component: DownloadCollectionFileAction,
 +            execute: () => {
 +                return;
 +            },
 +        },
 +        ...readOnlyCollectionDirectoryItemActionSet.reduce((prev, next) => prev.concat(next), []),
 +    ],
 +];
  
 -const writableActionSet: ContextMenuActionSet = [[
 -    {
 -        name: "Move item into new collection",
 -        icon: FileMoveIcon,
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openCollectionPartialMoveToNewCollectionDialog(resource));
 -        }
 -    },
 -    {
 -        name: "Move item into existing collection",
 -        icon: FileMoveIcon,
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openCollectionPartialMoveToExistingCollectionDialog(resource));
 -        }
 -    },
 -    {
 -        name: "Rename",
 -        icon: RenameIcon,
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openRenameFileDialog({
 -                name: resource.name,
 -                id: resource.uuid,
 -                path: resource.uuid.split('/').slice(1).join('/') }));
 -        }
 -    },
 -    {
 -        name: "Remove",
 -        icon: RemoveIcon,
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openFileRemoveDialog(resource.uuid));
 -        }
 -    }
 -]];
 +const writableActionSet: ContextMenuActionSet = [
 +    [
 +        {
-             name: 'Rename',
++            name: "Move item into new collection",
++            icon: FileMoveIcon,
++            execute: (dispatch, resources) => {
++                dispatch<any>(openCollectionPartialMoveToNewCollectionDialog(resources[0]));
++            },
++        },
++        {
++            name: "Move item into existing collection",
++            icon: FileMoveIcon,
++            execute: (dispatch, resources) => {
++                dispatch<any>(openCollectionPartialMoveToExistingCollectionDialog(resources[0]));
++            },
++        },
++        {
++            name: "Rename",
 +            icon: RenameIcon,
 +            execute: (dispatch, resources) => {
-                 resources.forEach((resource) =>
-                     dispatch<any>(
-                         openRenameFileDialog({
-                             name: resource.name,
-                             id: resource.uuid,
-                             path: resource.uuid.split('/').slice(1).join('/'),
-                         })
-                     )
++                dispatch<any>(
++                    openRenameFileDialog({
++                        name: resources[0].name,
++                        id: resources[0].uuid,
++                        path: resources[0].uuid.split("/").slice(1).join("/"),
++                    })
 +                );
 +            },
 +        },
 +        {
-             name: 'Remove',
++            name: "Remove",
 +            icon: RemoveIcon,
 +            execute: (dispatch, resources) => {
-                 resources.forEach((resource) => dispatch<any>(openFileRemoveDialog(resource.uuid)));
++                dispatch<any>(openFileRemoveDialog(resources[0].uuid));
 +            },
 +        },
 +    ],
 +];
  
  export const collectionDirectoryItemActionSet: ContextMenuActionSet = readOnlyCollectionDirectoryItemActionSet.concat(writableActionSet);
  
index cfcb2c2467641a96bb0edd45b5c039effb79755c,1baf04228ce225fcc3b3c85d45d5259dd72ddab7..4a1460bfc94f81552283595f7d31ffd08d97517a
@@@ -2,49 -2,65 +2,62 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
- import { openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
- import { DetailsIcon, AdvancedIcon, OpenIcon, Link, StartIcon } from 'components/icon/icon';
- import { copyToClipboardAction, openInNewTabAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
- import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
- import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
+ import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+ import { openRunProcess, deleteWorkflow } from "store/workflow-panel/workflow-panel-actions";
 -import {
 -    DetailsIcon,
 -    AdvancedIcon,
 -    OpenIcon,
 -    Link,
 -    StartIcon,
 -    TrashIcon
 -} from "components/icon/icon";
++import { DetailsIcon, AdvancedIcon, OpenIcon, Link, StartIcon, TrashIcon } from "components/icon/icon";
+ import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
 -import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
++import { toggleDetailsPanel } from "store/details-panel/details-panel-action";
+ import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
  
- export const workflowActionSet: ContextMenuActionSet = [
 -export const readOnlyWorkflowActionSet: ContextMenuActionSet = [[
 -    {
 -        icon: OpenIcon,
 -        name: "Open in new tab",
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openInNewTabAction(resource));
 -        }
 -    },
 -    {
 -        icon: Link,
 -        name: "Copy to clipboard",
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(copyToClipboardAction(resource));
 -        }
 -    },
 -    {
 -        icon: DetailsIcon,
 -        name: "View details",
 -        execute: dispatch => {
 -            dispatch<any>(toggleDetailsPanel());
 -        }
 -    },
 -    {
 -        icon: AdvancedIcon,
 -        name: "API Details",
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openAdvancedTabDialog(resource.uuid));
 -        }
 -    },
 -    {
 -        icon: StartIcon,
 -        name: "Run Workflow",
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(openRunProcess(resource.uuid, resource.ownerUuid, resource.name));
 -        }
 -    }
 -]];
++export const readOnlyWorkflowActionSet: ContextMenuActionSet = [
 +    [
 +        {
 +            icon: OpenIcon,
-             name: 'Open in new tab',
++            name: "Open in new tab",
 +            execute: (dispatch, resources) => {
-                 resources.forEach((resource) => dispatch<any>(openInNewTabAction(resource)));
++                dispatch<any>(openInNewTabAction(resources[0]));
 +            },
 +        },
 +        {
 +            icon: Link,
-             name: 'Copy to clipboard',
++            name: "Copy to clipboard",
 +            execute: (dispatch, resources) => {
 +                dispatch<any>(copyToClipboardAction(resources));
 +            },
 +        },
 +        {
 +            icon: DetailsIcon,
-             name: 'View details',
-             execute: (dispatch) => {
++            name: "View details",
++            execute: dispatch => {
 +                dispatch<any>(toggleDetailsPanel());
 +            },
 +        },
 +        {
 +            icon: AdvancedIcon,
-             name: 'API Details',
++            name: "API Details",
 +            execute: (dispatch, resources) => {
-                 resources.forEach((resource) => dispatch<any>(openAdvancedTabDialog(resource.uuid)));
++                dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
 +            },
 +        },
 +        {
 +            icon: StartIcon,
-             name: 'Run Workflow',
++            name: "Run Workflow",
++            execute: (dispatch, resources) => {
++                dispatch<any>(openRunProcess(resources[0].uuid, resources[0].ownerUuid, resources[0].name));
++            },
++        },
++    ],
++];
 -export const workflowActionSet: ContextMenuActionSet = [[
 -    ...readOnlyWorkflowActionSet[0],
 -    {
 -        icon: TrashIcon,
 -        name: "Delete Workflow",
 -        execute: (dispatch, resource) => {
 -            dispatch<any>(deleteWorkflow(resource.uuid, resource.ownerUuid));
 -        }
 -    },
 -]];
++export const workflowActionSet: ContextMenuActionSet = [
++    [
++        ...readOnlyWorkflowActionSet[0],
++        {
++            icon: TrashIcon,
++            name: "Delete Workflow",
 +            execute: (dispatch, resources) => {
-                 resources.forEach((resource) => dispatch<any>(openRunProcess(resource.uuid, resource.ownerUuid, resource.name)));
++                dispatch<any>(deleteWorkflow(resources[0].uuid, resources[0].ownerUuid));
 +            },
 +        },
 +    ],
 +];
index e7bfff9e090cca915225accd50ef11d8ce830877,1b4610eff6e5fdfe1359569edf0dcd210234335a..8cb3394a23a8752a9b9c8bd5888267083217b7bc
@@@ -94,16 -95,15 +97,15 @@@ export enum ContextMenuKind 
      COLLECTION_DIRECTORY_ITEM = "CollectionDirectoryItem",
      READONLY_COLLECTION_FILE_ITEM = "ReadOnlyCollectionFileItem",
      READONLY_COLLECTION_DIRECTORY_ITEM = "ReadOnlyCollectionDirectoryItem",
-     COLLECTION_FILES_NOT_SELECTED = "CollectionFilesNotSelected",
 -    COLLECTION = 'Collection',
 -    COLLECTION_ADMIN = 'CollectionAdmin',
 -    READONLY_COLLECTION = 'ReadOnlyCollection',
 -    OLD_VERSION_COLLECTION = 'OldVersionCollection',
 -    TRASHED_COLLECTION = 'TrashedCollection',
 +    COLLECTION = "Collection",
 +    COLLECTION_ADMIN = "CollectionAdmin",
 +    READONLY_COLLECTION = "ReadOnlyCollection",
 +    OLD_VERSION_COLLECTION = "OldVersionCollection",
 +    TRASHED_COLLECTION = "TrashedCollection",
      PROCESS = "Process",
 -    PROCESS_ADMIN = 'ProcessAdmin',
 -    PROCESS_RESOURCE = 'ProcessResource',
 -    READONLY_PROCESS_RESOURCE = 'ReadOnlyProcessResource',
 +    PROCESS_ADMIN = "ProcessAdmin",
 +    PROCESS_RESOURCE = "ProcessResource",
 +    READONLY_PROCESS_RESOURCE = "ReadOnlyProcessResource",
      PROCESS_LOGS = "ProcessLogs",
      REPOSITORY = "Repository",
      SSH_KEY = "SshKey",
      PERMISSION_EDIT = "PermissionEdit",
      LINK = "Link",
      WORKFLOW = "Workflow",
 -    SEARCH_RESULTS = "SearchResults"
+     READONLY_WORKFLOW = "ReadOnlyWorkflow",
 +    SEARCH_RESULTS = "SearchResults",
  }
index b6623bb664394908ce3d82bf17d30c6891c2f909,dca06084f63bfeef74aedf896a411b7ac624f764..5accd0522fd0d23af4a0fa6acc756c1b58a47eb4
@@@ -2,10 -2,10 +2,17 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
++<<<<<<< HEAD
 +import React from 'react';
 +import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from '@material-ui/core';
 +import { FavoriteStar, PublicFavoriteStar } from '../favorite-star/favorite-star';
 +import { Resource, ResourceKind, TrashableResource } from 'models/resource';
++=======
+ import React from "react";
+ import { Grid, Typography, withStyles, Tooltip, IconButton, Checkbox, Chip } from "@material-ui/core";
+ import { FavoriteStar, PublicFavoriteStar } from "../favorite-star/favorite-star";
+ import { Resource, ResourceKind, TrashableResource } from "models/resource";
++>>>>>>> main
  import {
      FreezeIcon,
      ProjectIcon,
      ActiveIcon,
      SetupIcon,
      InactiveIcon,
++<<<<<<< HEAD
 +} from 'components/icon/icon';
 +import { formatDate, formatFileSize, formatTime } from 'common/formatters';
 +import { resourceLabel } from 'common/labels';
 +import { connect, DispatchProp } from 'react-redux';
 +import { RootState } from 'store/store';
 +import { getResource, filterResources } from 'store/resources/resources';
 +import { GroupContentsResource } from 'services/groups-service/groups-service';
 +import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from 'store/processes/process';
 +import { ArvadosTheme } from 'common/custom-theme';
 +import { compose, Dispatch } from 'redux';
 +import { WorkflowResource } from 'models/workflow';
 +import { ResourceStatus as WorkflowStatus } from 'views/workflow-panel/workflow-panel-view';
 +import { getUuidPrefix, openRunProcess } from 'store/workflow-panel/workflow-panel-actions';
 +import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
 +import { getUserFullname, getUserDisplayName, User, UserResource } from 'models/user';
 +import { toggleIsAdmin } from 'store/users/users-actions';
 +import { LinkClass, LinkResource } from 'models/link';
 +import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from 'store/navigation/navigation-action';
 +import { withResourceData } from 'views-components/data-explorer/with-resources';
 +import { CollectionResource } from 'models/collection';
 +import { IllegalNamingWarning } from 'components/warning/warning';
 +import { loadResource } from 'store/resources/resources-actions';
 +import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from 'models/group';
 +import { openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
 +import { setMemberIsHidden } from 'store/group-details-panel/group-details-panel-actions';
 +import { formatPermissionLevel } from 'views-components/sharing-dialog/permission-select';
 +import { PermissionLevel } from 'models/permission';
 +import { openPermissionEditContextMenu } from 'store/context-menu/context-menu-actions';
 +import { getUserUuid } from 'common/getuser';
 +import { VirtualMachinesResource } from 'models/virtual-machines';
 +import { CopyToClipboardSnackbar } from 'components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar';
 +import { ProjectResource } from 'models/project';
 +import { ProcessResource } from 'models/process';
 +
 +const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
 +    const navFunc = 'groupClass' in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
 +    return (
 +        <Grid container alignItems='center' wrap='nowrap' spacing={16}>
 +            <Grid item>{renderIcon(item)}</Grid>
 +            <Grid item>
 +                <Typography color='primary' style={{ width: 'auto', cursor: 'pointer' }} onClick={() => dispatch<any>(navFunc(item.uuid))}>
++=======
+ } from "components/icon/icon";
+ import { formatDate, formatFileSize, formatTime } from "common/formatters";
+ import { resourceLabel } from "common/labels";
+ import { connect, DispatchProp } from "react-redux";
+ import { RootState } from "store/store";
+ import { getResource, filterResources } from "store/resources/resources";
+ import { GroupContentsResource } from "services/groups-service/groups-service";
+ import { getProcess, Process, getProcessStatus, getProcessStatusStyles, getProcessRuntime } from "store/processes/process";
+ import { ArvadosTheme } from "common/custom-theme";
+ import { compose, Dispatch } from "redux";
+ import { WorkflowResource } from "models/workflow";
+ import { ResourceStatus as WorkflowStatus } from "views/workflow-panel/workflow-panel-view";
+ import { getUuidPrefix, openRunProcess } from "store/workflow-panel/workflow-panel-actions";
+ import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
+ import { getUserFullname, getUserDisplayName, User, UserResource } from "models/user";
+ import { toggleIsAdmin } from "store/users/users-actions";
+ import { LinkClass, LinkResource } from "models/link";
+ import { navigateTo, navigateToGroupDetails, navigateToUserProfile } from "store/navigation/navigation-action";
+ import { withResourceData } from "views-components/data-explorer/with-resources";
+ import { CollectionResource } from "models/collection";
+ import { IllegalNamingWarning } from "components/warning/warning";
+ import { loadResource } from "store/resources/resources-actions";
+ import { BuiltinGroups, getBuiltinGroupUuid, GroupClass, GroupResource, isBuiltinGroup } from "models/group";
+ import { openRemoveGroupMemberDialog } from "store/group-details-panel/group-details-panel-actions";
+ import { setMemberIsHidden } from "store/group-details-panel/group-details-panel-actions";
+ import { formatPermissionLevel } from "views-components/sharing-dialog/permission-select";
+ import { PermissionLevel } from "models/permission";
+ import { openPermissionEditContextMenu } from "store/context-menu/context-menu-actions";
+ import { VirtualMachinesResource } from "models/virtual-machines";
+ import { CopyToClipboardSnackbar } from "components/copy-to-clipboard-snackbar/copy-to-clipboard-snackbar";
+ import { ProjectResource } from "models/project";
+ import { ProcessResource } from "models/process";
+ const renderName = (dispatch: Dispatch, item: GroupContentsResource) => {
+     const navFunc = "groupClass" in item && item.groupClass === GroupClass.ROLE ? navigateToGroupDetails : navigateTo;
+     return (
+         <Grid
+             container
+             alignItems="center"
+             wrap="nowrap"
+             spacing={16}
+         >
+             <Grid item>{renderIcon(item)}</Grid>
+             <Grid item>
+                 <Typography
+                     color="primary"
+                     style={{ width: "auto", cursor: "pointer" }}
+                     onClick={() => dispatch<any>(navFunc(item.uuid))}
+                 >
++>>>>>>> main
                      {item.kind === ResourceKind.PROJECT || item.kind === ResourceKind.COLLECTION ? <IllegalNamingWarning name={item.name} /> : null}
                      {item.name}
                  </Typography>
              </Grid>
              <Grid item>
++<<<<<<< HEAD
 +                <Typography variant='caption'>
++=======
+                 <Typography variant="caption">
++>>>>>>> main
                      <FavoriteStar resourceUuid={item.uuid} />
                      <PublicFavoriteStar resourceUuid={item.uuid} />
                      {item.kind === ResourceKind.PROJECT && <FrozenProject item={item} />}
@@@ -87,8 -95,12 +150,17 @@@ const FrozenProject = (props: { item: P
  
      if (props.item.frozenByUuid) {
          return (
++<<<<<<< HEAD
 +            <Tooltip onOpen={getFullName} enterDelay={500} title={<span>Project was frozen by {fullUsername}</span>}>
 +                <FreezeIcon style={{ fontSize: 'inherit' }} />
++=======
+             <Tooltip
+                 onOpen={getFullName}
+                 enterDelay={500}
+                 title={<span>Project was frozen by {fullUsername}</span>}
+             >
+                 <FreezeIcon style={{ fontSize: "inherit" }} />
++>>>>>>> main
              </Tooltip>
          );
      } else {
@@@ -124,17 -136,28 +196,39 @@@ const renderIcon = (item: GroupContents
  
  const renderDate = (date?: string) => {
      return (
++<<<<<<< HEAD
 +        <Typography noWrap style={{ minWidth: '100px' }}>
++=======
+         <Typography
+             noWrap
+             style={{ minWidth: "100px" }}
+         >
++>>>>>>> main
              {formatDate(date)}
          </Typography>
      );
  };
  
  const renderWorkflowName = (item: WorkflowResource) => (
++<<<<<<< HEAD
 +    <Grid container alignItems='center' wrap='nowrap' spacing={16}>
 +        <Grid item>{renderIcon(item)}</Grid>
 +        <Grid item>
 +            <Typography color='primary' style={{ width: '100px' }}>
++=======
+     <Grid
+         container
+         alignItems="center"
+         wrap="nowrap"
+         spacing={16}
+     >
+         <Grid item>{renderIcon(item)}</Grid>
+         <Grid item>
+             <Typography
+                 color="primary"
+                 style={{ width: "100px" }}
+             >
++>>>>>>> main
                  {item.name}
              </Typography>
          </Grid>
@@@ -155,7 -178,7 +249,11 @@@ const resourceShare = (dispatch: Dispat
      return (
          <div>
              {!isPublic && uuid && (
++<<<<<<< HEAD
 +                <Tooltip title='Share'>
++=======
+                 <Tooltip title="Share">
++>>>>>>> main
                      <IconButton onClick={() => dispatch<any>(openSharingDialog(uuid))}>
                          <ShareIcon />
                      </IconButton>
@@@ -169,8 -192,8 +267,13 @@@ export const ResourceShare = connect((s
      const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
      const uuidPrefix = getUuidPrefix(state);
      return {
++<<<<<<< HEAD
 +        uuid: resource ? resource.uuid : '',
 +        ownerUuid: resource ? resource.ownerUuid : '',
++=======
+         uuid: resource ? resource.uuid : "",
+         ownerUuid: resource ? resource.ownerUuid : "",
++>>>>>>> main
          uuidPrefix,
      };
  })((props: { ownerUuid?: string; uuidPrefix: string; uuid?: string } & DispatchProp<any>) =>
@@@ -184,20 -207,25 +287,39 @@@ const renderFirstName = (item: { firstN
  
  export const ResourceFirstName = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<UserResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return resource || { firstName: '' };
++=======
+     return resource || { firstName: "" };
++>>>>>>> main
  })(renderFirstName);
  
  const renderLastName = (item: { lastName: string }) => <Typography noWrap>{item.lastName}</Typography>;
  
  export const ResourceLastName = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<UserResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return resource || { lastName: '' };
 +})(renderLastName);
 +
 +const renderFullName = (dispatch: Dispatch, item: { uuid: string; firstName: string; lastName: string }, link?: boolean) => {
 +    const displayName = (item.firstName + ' ' + item.lastName).trim() || item.uuid;
 +    return link ? (
 +        <Typography noWrap color='primary' style={{ cursor: 'pointer' }} onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}>
++=======
+     return resource || { lastName: "" };
+ })(renderLastName);
+ const renderFullName = (dispatch: Dispatch, item: { uuid: string; firstName: string; lastName: string }, link?: boolean) => {
+     const displayName = (item.firstName + " " + item.lastName).trim() || item.uuid;
+     return link ? (
+         <Typography
+             noWrap
+             color="primary"
+             style={{ cursor: "pointer" }}
+             onClick={() => dispatch<any>(navigateToUserProfile(item.uuid))}
+         >
++>>>>>>> main
              {displayName}
          </Typography>
      ) : (
  
  export const UserResourceFullName = connect((state: RootState, props: { uuid: string; link?: boolean }) => {
      const resource = getResource<UserResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { item: resource || { uuid: '', firstName: '', lastName: '' }, link: props.link };
++=======
+     return { item: resource || { uuid: "", firstName: "", lastName: "" }, link: props.link };
++>>>>>>> main
  })((props: { item: { uuid: string; firstName: string; lastName: string }; link?: boolean } & DispatchProp<any>) =>
      renderFullName(props.dispatch, props.item, props.link)
  );
  
  const renderUuid = (item: { uuid: string }) => (
++<<<<<<< HEAD
 +    <Typography data-cy='uuid' noWrap>
 +        {item.uuid}
 +        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
++=======
+     <Typography
+         data-cy="uuid"
+         noWrap
+     >
+         {item.uuid}
+         {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
++>>>>>>> main
      </Typography>
  );
  
  const renderUuidCopyIcon = (item: { uuid: string }) => (
++<<<<<<< HEAD
 +    <Typography data-cy='uuid' noWrap>
 +        {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || '-'}
 +    </Typography>
 +);
 +
 +export const ResourceUuid = connect((state: RootState, props: { uuid: string }) => getResource<UserResource>(props.uuid)(state.resources) || { uuid: '' })(renderUuid);
++=======
+     <Typography
+         data-cy="uuid"
+         noWrap
+     >
+         {(item.uuid && <CopyToClipboardSnackbar value={item.uuid} />) || "-"}
+     </Typography>
+ );
+ export const ResourceUuid = connect(
+     (state: RootState, props: { uuid: string }) => getResource<UserResource>(props.uuid)(state.resources) || { uuid: "" }
+ )(renderUuid);
++>>>>>>> main
  
  const renderEmail = (item: { email: string }) => <Typography noWrap>{item.email}</Typography>;
  
  export const ResourceEmail = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<UserResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return resource || { email: '' };
 +})(renderEmail);
 +
 +enum UserAccountStatus {
 +    ACTIVE = 'Active',
 +    INACTIVE = 'Inactive',
 +    SETUP = 'Setup',
 +    UNKNOWN = '',
 +}
 +
 +const renderAccountStatus = (props: { status: UserAccountStatus }) => (
 +    <Grid container alignItems='center' wrap='nowrap' spacing={8} data-cy='account-status'>
++=======
+     return resource || { email: "" };
+ })(renderEmail);
+ enum UserAccountStatus {
+     ACTIVE = "Active",
+     INACTIVE = "Inactive",
+     SETUP = "Setup",
+     UNKNOWN = "",
+ }
+ const renderAccountStatus = (props: { status: UserAccountStatus }) => (
+     <Grid
+         container
+         alignItems="center"
+         wrap="nowrap"
+         spacing={8}
+         data-cy="account-status"
+     >
++>>>>>>> main
          <Grid item>
              {(() => {
                  switch (props.status) {
                      case UserAccountStatus.ACTIVE:
++<<<<<<< HEAD
 +                        return <ActiveIcon style={{ color: '#4caf50', verticalAlign: 'middle' }} />;
 +                    case UserAccountStatus.SETUP:
 +                        return <SetupIcon style={{ color: '#2196f3', verticalAlign: 'middle' }} />;
 +                    case UserAccountStatus.INACTIVE:
 +                        return <InactiveIcon style={{ color: '#9e9e9e', verticalAlign: 'middle' }} />;
++=======
+                         return <ActiveIcon style={{ color: "#4caf50", verticalAlign: "middle" }} />;
+                     case UserAccountStatus.SETUP:
+                         return <SetupIcon style={{ color: "#2196f3", verticalAlign: "middle" }} />;
+                     case UserAccountStatus.INACTIVE:
+                         return <InactiveIcon style={{ color: "#9e9e9e", verticalAlign: "middle" }} />;
++>>>>>>> main
                      default:
                          return <></>;
                  }
@@@ -303,11 -345,11 +481,19 @@@ const renderIsHidden = (props: 
      if (props.memberLinkUuid) {
          return (
              <Checkbox
++<<<<<<< HEAD
 +                data-cy='user-visible-checkbox'
 +                color='primary'
 +                checked={props.visible}
 +                disabled={!props.canManage}
 +                onClick={(e) => {
++=======
+                 data-cy="user-visible-checkbox"
+                 color="primary"
+                 checked={props.visible}
+                 disabled={!props.canManage}
+                 onClick={e => {
++>>>>>>> main
                      e.stopPropagation();
                      props.setMemberIsHidden(props.memberLinkUuid, props.permissionLinkUuid, !props.visible);
                  }}
@@@ -339,16 -381,16 +525,20 @@@ export const ResourceLinkTailIsVisible 
  
          return member?.kind === ResourceKind.USER
              ? { memberLinkUuid: link?.uuid, permissionLinkUuid, visible: isVisible, canManage: !isBuiltin }
++<<<<<<< HEAD
 +            : { memberLinkUuid: '', permissionLinkUuid: '', visible: false, canManage: false };
++=======
+             : { memberLinkUuid: "", permissionLinkUuid: "", visible: false, canManage: false };
++>>>>>>> main
      },
      { setMemberIsHidden }
  )(renderIsHidden);
  
  const renderIsAdmin = (props: { uuid: string; isAdmin: boolean; toggleIsAdmin: (uuid: string) => void }) => (
      <Checkbox
 -        color="primary"
 +        color='primary'
          checked={props.isAdmin}
-         onClick={(e) => {
+         onClick={e => {
              e.stopPropagation();
              props.toggleIsAdmin(props.uuid);
          }}
@@@ -367,7 -409,7 +557,11 @@@ const renderUsername = (item: { usernam
  
  export const ResourceUsername = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<UserResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return resource || { username: '', uuid: props.uuid };
++=======
+     return resource || { username: "", uuid: props.uuid };
++>>>>>>> main
  })(renderUsername);
  
  // Virtual machine resource
@@@ -376,16 -418,16 +570,26 @@@ const renderHostname = (item: { hostnam
  
  export const VirtualMachineHostname = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<VirtualMachinesResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return resource || { hostname: '' };
++=======
+     return resource || { hostname: "" };
++>>>>>>> main
  })(renderHostname);
  
  const renderVirtualMachineLogin = (login: { user: string }) => <Typography noWrap>{login.user}</Typography>;
  
  export const VirtualMachineLogin = connect((state: RootState, props: { linkUuid: string }) => {
      const permission = getResource<LinkResource>(props.linkUuid)(state.resources);
++<<<<<<< HEAD
 +    const user = getResource<UserResource>(permission?.tailUuid || '')(state.resources);
 +
 +    return { user: user?.username || permission?.tailUuid || '' };
++=======
+     const user = getResource<UserResource>(permission?.tailUuid || "")(state.resources);
+     return { user: user?.username || permission?.tailUuid || "" };
++>>>>>>> main
  })(renderVirtualMachineLogin);
  
  // Common methods
@@@ -393,42 -435,43 +597,59 @@@ const renderCommonData = (data: string
  
  const renderCommonDate = (date: string) => <Typography noWrap>{formatDate(date)}</Typography>;
  
- export const CommonUuid = withResourceData('uuid', renderCommonData);
+ export const CommonUuid = withResourceData("uuid", renderCommonData);
  
  // Api Client Authorizations
- export const TokenApiClientId = withResourceData('apiClientId', renderCommonData);
+ export const TokenApiClientId = withResourceData("apiClientId", renderCommonData);
  
- export const TokenApiToken = withResourceData('apiToken', renderCommonData);
+ export const TokenApiToken = withResourceData("apiToken", renderCommonData);
  
- export const TokenCreatedByIpAddress = withResourceData('createdByIpAddress', renderCommonDate);
+ export const TokenCreatedByIpAddress = withResourceData("createdByIpAddress", renderCommonDate);
  
- export const TokenDefaultOwnerUuid = withResourceData('defaultOwnerUuid', renderCommonData);
+ export const TokenDefaultOwnerUuid = withResourceData("defaultOwnerUuid", renderCommonData);
  
- export const TokenExpiresAt = withResourceData('expiresAt', renderCommonDate);
+ export const TokenExpiresAt = withResourceData("expiresAt", renderCommonDate);
  
- export const TokenLastUsedAt = withResourceData('lastUsedAt', renderCommonDate);
+ export const TokenLastUsedAt = withResourceData("lastUsedAt", renderCommonDate);
  
- export const TokenLastUsedByIpAddress = withResourceData('lastUsedByIpAddress', renderCommonData);
+ export const TokenLastUsedByIpAddress = withResourceData("lastUsedByIpAddress", renderCommonData);
  
- export const TokenScopes = withResourceData('scopes', renderCommonData);
+ export const TokenScopes = withResourceData("scopes", renderCommonData);
  
- export const TokenUserId = withResourceData('userId', renderCommonData);
+ export const TokenUserId = withResourceData("userId", renderCommonData);
  
  const clusterColors = [
++<<<<<<< HEAD
 +    ['#f44336', '#fff'],
 +    ['#2196f3', '#fff'],
 +    ['#009688', '#fff'],
 +    ['#cddc39', '#fff'],
 +    ['#ff9800', '#fff'],
++=======
+     ["#f44336", "#fff"],
+     ["#2196f3", "#fff"],
+     ["#009688", "#fff"],
+     ["#cddc39", "#fff"],
+     ["#ff9800", "#fff"],
++>>>>>>> main
  ];
  
  export const ResourceCluster = (props: { uuid: string }) => {
      const CLUSTER_ID_LENGTH = 5;
++<<<<<<< HEAD
 +    const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf('-') : 5;
 +    const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : '';
 +    const ci =
 +        pos >= CLUSTER_ID_LENGTH
 +            ? ((props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1) + props.uuid.charCodeAt(2)) * props.uuid.charCodeAt(3) + props.uuid.charCodeAt(4)) %
++=======
+     const pos = props.uuid.length > CLUSTER_ID_LENGTH ? props.uuid.indexOf("-") : 5;
+     const clusterId = pos >= CLUSTER_ID_LENGTH ? props.uuid.substring(0, pos) : "";
+     const ci =
+         pos >= CLUSTER_ID_LENGTH
+             ? ((props.uuid.charCodeAt(0) * props.uuid.charCodeAt(1) + props.uuid.charCodeAt(2)) * props.uuid.charCodeAt(3) +
+                   props.uuid.charCodeAt(4)) %
++>>>>>>> main
                clusterColors.length
              : 0;
      return (
              style={{
                  backgroundColor: clusterColors[ci][0],
                  color: clusterColors[ci][1],
++<<<<<<< HEAD
 +                padding: '2px 7px',
++=======
+                 padding: "2px 7px",
++>>>>>>> main
                  borderRadius: 3,
              }}
          >
  };
  
  // Links Resources
++<<<<<<< HEAD
 +const renderLinkName = (item: { name: string }) => <Typography noWrap>{item.name || '-'}</Typography>;
 +
 +export const ResourceLinkName = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<LinkResource>(props.uuid)(state.resources);
 +    return resource || { name: '' };
++=======
+ const renderLinkName = (item: { name: string }) => <Typography noWrap>{item.name || "-"}</Typography>;
+ export const ResourceLinkName = connect((state: RootState, props: { uuid: string }) => {
+     const resource = getResource<LinkResource>(props.uuid)(state.resources);
+     return resource || { name: "" };
++>>>>>>> main
  })(renderLinkName);
  
  const renderLinkClass = (item: { linkClass: string }) => <Typography noWrap>{item.linkClass}</Typography>;
  
  export const ResourceLinkClass = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return resource || { linkClass: '' };
 +})(renderLinkClass);
 +
 +const getResourceDisplayName = (resource: Resource): string => {
 +    if ((resource as UserResource).kind === ResourceKind.USER && typeof (resource as UserResource).firstName !== 'undefined') {
++=======
+     return resource || { linkClass: "" };
+ })(renderLinkClass);
+ const getResourceDisplayName = (resource: Resource): string => {
+     if ((resource as UserResource).kind === ResourceKind.USER && typeof (resource as UserResource).firstName !== "undefined") {
++>>>>>>> main
          // We can be sure the resource is UserResource
          return getUserDisplayName(resource as UserResource);
      } else {
@@@ -473,60 -516,77 +714,120 @@@ const renderResourceLink = (dispatch: D
      var displayName = getResourceDisplayName(item);
  
      return (
++<<<<<<< HEAD
 +        <Typography noWrap color='primary' style={{ cursor: 'pointer' }} onClick={() => dispatch<any>(navigateTo(item.uuid))}>
 +            {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || '' : '')}: {displayName || item.uuid}
++=======
+         <Typography
+             noWrap
+             color="primary"
+             style={{ cursor: "pointer" }}
+             onClick={() => {
+                 console.log(item);
+                 item.kind === ResourceKind.GROUP && (item as GroupResource).groupClass === "role"
+                     ? dispatch<any>(navigateToGroupDetails(item.uuid))
+                     : dispatch<any>(navigateTo(item.uuid));
+             }}
+         >
+             {resourceLabel(item.kind, item && item.kind === ResourceKind.GROUP ? (item as GroupResource).groupClass || "" : "")}:{" "}
+             {displayName || item.uuid}
++>>>>>>> main
          </Typography>
      );
  };
  
  export const ResourceLinkTail = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const tailResource = getResource<Resource>(resource?.tailUuid || '')(state.resources);
 +
 +    return {
 +        item: tailResource || { uuid: resource?.tailUuid || '', kind: resource?.tailKind || ResourceKind.NONE },
++=======
+     const tailResource = getResource<Resource>(resource?.tailUuid || "")(state.resources);
+     return {
+         item: tailResource || { uuid: resource?.tailUuid || "", kind: resource?.tailKind || ResourceKind.NONE },
++>>>>>>> main
      };
  })((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
  
  export const ResourceLinkHead = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const headResource = getResource<Resource>(resource?.headUuid || '')(state.resources);
 +
 +    return {
 +        item: headResource || { uuid: resource?.headUuid || '', kind: resource?.headKind || ResourceKind.NONE },
++=======
+     const headResource = getResource<Resource>(resource?.headUuid || "")(state.resources);
+     return {
+         item: headResource || { uuid: resource?.headUuid || "", kind: resource?.headKind || ResourceKind.NONE },
++>>>>>>> main
      };
  })((props: { item: Resource } & DispatchProp<any>) => renderResourceLink(props.dispatch, props.item));
  
  export const ResourceLinkUuid = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return resource || { uuid: '' };
++=======
+     return resource || { uuid: "" };
++>>>>>>> main
  })(renderUuid);
  
  export const ResourceLinkHeadUuid = connect((state: RootState, props: { uuid: string }) => {
      const link = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const headResource = getResource<Resource>(link?.headUuid || '')(state.resources);
 +
 +    return headResource || { uuid: '' };
++=======
+     const headResource = getResource<Resource>(link?.headUuid || "")(state.resources);
+     return headResource || { uuid: "" };
++>>>>>>> main
  })(renderUuid);
  
  export const ResourceLinkTailUuid = connect((state: RootState, props: { uuid: string }) => {
      const link = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const tailResource = getResource<Resource>(link?.tailUuid || '')(state.resources);
 +
 +    return tailResource || { uuid: '' };
++=======
+     const tailResource = getResource<Resource>(link?.tailUuid || "")(state.resources);
+     return tailResource || { uuid: "" };
++>>>>>>> main
  })(renderUuid);
  
  const renderLinkDelete = (dispatch: Dispatch, item: LinkResource, canManage: boolean) => {
      if (item.uuid) {
          return canManage ? (
              <Typography noWrap>
++<<<<<<< HEAD
 +                <IconButton data-cy='resource-delete-button' onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}>
++=======
+                 <IconButton
+                     data-cy="resource-delete-button"
+                     onClick={() => dispatch<any>(openRemoveGroupMemberDialog(item.uuid))}
+                 >
++>>>>>>> main
                      <RemoveIcon />
                  </IconButton>
              </Typography>
          ) : (
              <Typography noWrap>
++<<<<<<< HEAD
 +                <IconButton disabled data-cy='resource-delete-button'>
++=======
+                 <IconButton
+                     disabled
+                     data-cy="resource-delete-button"
+                 >
++>>>>>>> main
                      <RemoveIcon />
                  </IconButton>
              </Typography>
  
  export const ResourceLinkDelete = connect((state: RootState, props: { uuid: string }) => {
      const link = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
 +
 +    return {
 +        item: link || { uuid: '', kind: ResourceKind.NONE },
++=======
+     const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
+     return {
+         item: link || { uuid: "", kind: ResourceKind.NONE },
++>>>>>>> main
          canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
      };
  })((props: { item: LinkResource; canManage: boolean } & DispatchProp<any>) => renderLinkDelete(props.dispatch, props.item, props.canManage));
  
  export const ResourceLinkTailEmail = connect((state: RootState, props: { uuid: string }) => {
      const link = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
 +
 +    return resource || { email: '' };
++=======
+     const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
+     return resource || { email: "" };
++>>>>>>> main
  })(renderEmail);
  
  export const ResourceLinkTailUsername = connect((state: RootState, props: { uuid: string }) => {
      const link = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const resource = getResource<UserResource>(link?.tailUuid || '')(state.resources);
 +
 +    return resource || { username: '' };
++=======
+     const resource = getResource<UserResource>(link?.tailUuid || "")(state.resources);
+     return resource || { username: "" };
++>>>>>>> main
  })(renderUsername);
  
  const renderPermissionLevel = (dispatch: Dispatch, link: LinkResource, canManage: boolean) => {
          <Typography noWrap>
              {formatPermissionLevel(link.name as PermissionLevel)}
              {canManage ? (
++<<<<<<< HEAD
 +                <IconButton data-cy='edit-permission-button' onClick={(event) => dispatch<any>(openPermissionEditContextMenu(event, link))}>
 +                    <RenameIcon />
 +                </IconButton>
 +            ) : (
 +                ''
++=======
+                 <IconButton
+                     data-cy="edit-permission-button"
+                     onClick={event => dispatch<any>(openPermissionEditContextMenu(event, link))}
+                 >
+                     <RenameIcon />
+                 </IconButton>
+             ) : (
+                 ""
++>>>>>>> main
              )}
          </Typography>
      );
  
  export const ResourceLinkHeadPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
      const link = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
 +
 +    return {
 +        link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
++=======
+     const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
+     return {
+         link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
++>>>>>>> main
          canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
      };
  })((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
  
  export const ResourceLinkTailPermissionLevel = connect((state: RootState, props: { uuid: string }) => {
      const link = getResource<LinkResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    const isBuiltin = isBuiltinGroup(link?.headUuid || '') || isBuiltinGroup(link?.tailUuid || '');
 +
 +    return {
 +        link: link || { uuid: '', name: '', kind: ResourceKind.NONE },
++=======
+     const isBuiltin = isBuiltinGroup(link?.headUuid || "") || isBuiltinGroup(link?.tailUuid || "");
+     return {
+         link: link || { uuid: "", name: "", kind: ResourceKind.NONE },
++>>>>>>> main
          canManage: link && getResourceLinkCanManage(state, link) && !isBuiltin,
      };
  })((props: { link: LinkResource; canManage: boolean } & DispatchProp<any>) => renderPermissionLevel(props.dispatch, props.link, props.canManage));
@@@ -613,7 -673,7 +955,11 @@@ const resourceRunProcess = (dispatch: D
      return (
          <div>
              {uuid && (
++<<<<<<< HEAD
 +                <Tooltip title='Run process'>
++=======
+                 <Tooltip title="Run process">
++>>>>>>> main
                      <IconButton onClick={() => dispatch<any>(openRunProcess(uuid))}>
                          <ProcessIcon />
                      </IconButton>
  export const ResourceRunProcess = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
      return {
++<<<<<<< HEAD
 +        uuid: resource ? resource.uuid : '',
++=======
+         uuid: resource ? resource.uuid : "",
++>>>>>>> main
      };
  })((props: { uuid: string } & DispatchProp<any>) => resourceRunProcess(props.dispatch, props.uuid));
  
@@@ -639,7 -699,10 +989,14 @@@ const renderWorkflowStatus = (uuidPrefi
  };
  
  const renderStatus = (status: string) => (
++<<<<<<< HEAD
 +    <Typography noWrap style={{ width: '60px' }}>
++=======
+     <Typography
+         noWrap
+         style={{ width: "60px" }}
+     >
++>>>>>>> main
          {status}
      </Typography>
  );
@@@ -648,32 -711,41 +1005,65 @@@ export const ResourceWorkflowStatus = c
      const resource = getResource<WorkflowResource>(props.uuid)(state.resources);
      const uuidPrefix = getUuidPrefix(state);
      return {
++<<<<<<< HEAD
 +        ownerUuid: resource ? resource.ownerUuid : '',
++=======
+         ownerUuid: resource ? resource.ownerUuid : "",
++>>>>>>> main
          uuidPrefix,
      };
  })((props: { ownerUuid?: string; uuidPrefix: string }) => renderWorkflowStatus(props.uuidPrefix, props.ownerUuid));
  
  export const ResourceContainerUuid = connect((state: RootState, props: { uuid: string }) => {
      const process = getProcess(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { uuid: process?.container?.uuid ? process?.container?.uuid : '' };
 +})((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
 +
 +enum ColumnSelection {
 +    OUTPUT_UUID = 'outputUuid',
 +    LOG_UUID = 'logUuid',
++=======
+     return { uuid: process?.container?.uuid ? process?.container?.uuid : "" };
+ })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
+ enum ColumnSelection {
+     OUTPUT_UUID = "outputUuid",
+     LOG_UUID = "logUuid",
++>>>>>>> main
  }
  
  const renderUuidLinkWithCopyIcon = (dispatch: Dispatch, item: ProcessResource, column: string) => {
      const selectedColumnUuid = item[column];
      return (
++<<<<<<< HEAD
 +        <Grid container alignItems='center' wrap='nowrap'>
 +            <Grid item>
 +                {selectedColumnUuid ? (
 +                    <Typography color='primary' style={{ width: 'auto', cursor: 'pointer' }} noWrap onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}>
 +                        {selectedColumnUuid}
 +                    </Typography>
 +                ) : (
 +                    '-'
++=======
+         <Grid
+             container
+             alignItems="center"
+             wrap="nowrap"
+         >
+             <Grid item>
+                 {selectedColumnUuid ? (
+                     <Typography
+                         color="primary"
+                         style={{ width: "auto", cursor: "pointer" }}
+                         noWrap
+                         onClick={() => dispatch<any>(navigateTo(selectedColumnUuid))}
+                     >
+                         {selectedColumnUuid}
+                     </Typography>
+                 ) : (
+                     "-"
++>>>>>>> main
                  )}
              </Grid>
              <Grid item>{selectedColumnUuid && renderUuidCopyIcon({ uuid: selectedColumnUuid })}</Grid>
@@@ -693,36 -765,39 +1083,67 @@@ export const ResourceLogUuid = connect(
  
  export const ResourceParentProcess = connect((state: RootState, props: { uuid: string }) => {
      const process = getProcess(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { parentProcess: process?.containerRequest?.requestingContainerUuid || '' };
++=======
+     return { parentProcess: process?.containerRequest?.requestingContainerUuid || "" };
++>>>>>>> main
  })((props: { parentProcess: string }) => renderUuid({ uuid: props.parentProcess }));
  
  export const ResourceModifiedByUserUuid = connect((state: RootState, props: { uuid: string }) => {
      const process = getProcess(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { userUuid: process?.containerRequest?.modifiedByUserUuid || '' };
++=======
+     return { userUuid: process?.containerRequest?.modifiedByUserUuid || "" };
++>>>>>>> main
  })((props: { userUuid: string }) => renderUuid({ uuid: props.userUuid }));
  
  export const ResourceCreatedAtDate = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { date: resource ? resource.createdAt : '' };
++=======
+     return { date: resource ? resource.createdAt : "" };
++>>>>>>> main
  })((props: { date: string }) => renderDate(props.date));
  
  export const ResourceLastModifiedDate = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { date: resource ? resource.modifiedAt : '' };
++=======
+     return { date: resource ? resource.modifiedAt : "" };
++>>>>>>> main
  })((props: { date: string }) => renderDate(props.date));
  
  export const ResourceTrashDate = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<TrashableResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { date: resource ? resource.trashAt : '' };
++=======
+     return { date: resource ? resource.trashAt : "" };
++>>>>>>> main
  })((props: { date: string }) => renderDate(props.date));
  
  export const ResourceDeleteDate = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<TrashableResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { date: resource ? resource.deleteAt : '' };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const renderFileSize = (fileSize?: number) => (
 +    <Typography noWrap style={{ minWidth: '45px' }}>
++=======
+     return { date: resource ? resource.deleteAt : "" };
+ })((props: { date: string }) => renderDate(props.date));
+ export const renderFileSize = (fileSize?: number) => (
+     <Typography
+         noWrap
+         style={{ minWidth: "45px" }}
+     >
++>>>>>>> main
          {formatFileSize(fileSize)}
      </Typography>
  );
@@@ -731,38 -806,38 +1152,66 @@@ export const ResourceFileSize = connect
      const resource = getResource<CollectionResource>(props.uuid)(state.resources);
  
      if (resource && resource.kind !== ResourceKind.COLLECTION) {
++<<<<<<< HEAD
 +        return { fileSize: '' };
++=======
+         return { fileSize: "" };
++>>>>>>> main
      }
  
      return { fileSize: resource ? resource.fileSizeTotal : 0 };
  })((props: { fileSize?: number }) => renderFileSize(props.fileSize));
  
++<<<<<<< HEAD
 +const renderOwner = (owner: string) => <Typography noWrap>{owner || '-'}</Typography>;
 +
 +export const ResourceOwner = connect((state: RootState, props: { uuid: string }) => {
 +    const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
 +    return { owner: resource ? resource.ownerUuid : '' };
++=======
+ const renderOwner = (owner: string) => <Typography noWrap>{owner || "-"}</Typography>;
+ export const ResourceOwner = connect((state: RootState, props: { uuid: string }) => {
+     const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
+     return { owner: resource ? resource.ownerUuid : "" };
++>>>>>>> main
  })((props: { owner: string }) => renderOwner(props.owner));
  
  export const ResourceOwnerName = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
      const ownerNameState = state.ownerName;
++<<<<<<< HEAD
 +    const ownerName = ownerNameState.find((it) => it.uuid === resource!.ownerUuid);
++=======
+     const ownerName = ownerNameState.find(it => it.uuid === resource!.ownerUuid);
++>>>>>>> main
      return { owner: ownerName ? ownerName!.name : resource!.ownerUuid };
  })((props: { owner: string }) => renderOwner(props.owner));
  
  export const ResourceUUID = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<CollectionResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { uuid: resource ? resource.uuid : '' };
 +})((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
 +
 +const renderVersion = (version: number) => {
 +    return <Typography>{version ?? '-'}</Typography>;
++=======
+     return { uuid: resource ? resource.uuid : "" };
+ })((props: { uuid: string }) => renderUuid({ uuid: props.uuid }));
+ const renderVersion = (version: number) => {
+     return <Typography>{version ?? "-"}</Typography>;
++>>>>>>> main
  };
  
  export const ResourceVersion = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<CollectionResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { version: resource ? resource.version : '' };
++=======
+     return { version: resource ? resource.version : "" };
++>>>>>>> main
  })((props: { version: number }) => renderVersion(props.version));
  
  const renderPortableDataHash = (portableDataHash: string | null) => (
                  <CopyToClipboardSnackbar value={portableDataHash} />
              </>
          ) : (
++<<<<<<< HEAD
 +            '-'
++=======
+             "-"
++>>>>>>> main
          )}
      </Typography>
  );
  
  export const ResourcePortableDataHash = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<CollectionResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { portableDataHash: resource ? resource.portableDataHash : '' };
 +})((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
 +
 +const renderFileCount = (fileCount: number) => {
 +    return <Typography>{fileCount ?? '-'}</Typography>;
++=======
+     return { portableDataHash: resource ? resource.portableDataHash : "" };
+ })((props: { portableDataHash: string }) => renderPortableDataHash(props.portableDataHash));
+ const renderFileCount = (fileCount: number) => {
+     return <Typography>{fileCount ?? "-"}</Typography>;
++>>>>>>> main
  };
  
  export const ResourceFileCount = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<CollectionResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { fileCount: resource ? resource.fileCount : '' };
 +})((props: { fileCount: number }) => renderFileCount(props.fileCount));
 +
 +const userFromID = connect((state: RootState, props: { uuid: string }) => {
 +    let userFullname = '';
++=======
+     return { fileCount: resource ? resource.fileCount : "" };
+ })((props: { fileCount: number }) => renderFileCount(props.fileCount));
+ const userFromID = connect((state: RootState, props: { uuid: string }) => {
+     let userFullname = "";
++>>>>>>> main
      const resource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
  
      if (resource) {
  const ownerFromResourceId = compose(
      connect((state: RootState, props: { uuid: string }) => {
          const childResource = getResource<GroupContentsResource & UserResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +        return { uuid: childResource ? (childResource as Resource).ownerUuid : '' };
++=======
+         return { uuid: childResource ? (childResource as Resource).ownerUuid : "" };
++>>>>>>> main
      }),
      userFromID
  );
@@@ -816,17 -891,25 +1289,36 @@@ const _resourceWithName = withStyles
      { withTheme: true }
  )((props: { uuid: string; userFullname: string; dispatch: Dispatch; theme: ArvadosTheme }) => {
      const { uuid, userFullname, dispatch, theme } = props;
++<<<<<<< HEAD
 +    if (userFullname === '') {
 +        dispatch<any>(loadResource(uuid, false));
 +        return (
 +            <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
++=======
+     if (userFullname === "") {
+         dispatch<any>(loadResource(uuid, false));
+         return (
+             <Typography
+                 style={{ color: theme.palette.primary.main }}
+                 inline
+                 noWrap
+             >
++>>>>>>> main
                  {uuid}
              </Typography>
          );
      }
  
      return (
++<<<<<<< HEAD
 +        <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
++=======
+         <Typography
+             style={{ color: theme.palette.primary.main }}
+             inline
+             noWrap
+         >
++>>>>>>> main
              {userFullname} ({uuid})
          </Typography>
      );
@@@ -839,7 -922,7 +1331,11 @@@ export const ResourceWithName = userFro
  export const UserNameFromID = compose(userFromID)((props: { uuid: string; displayAsText?: string; userFullname: string; dispatch: Dispatch }) => {
      const { uuid, userFullname, dispatch } = props;
  
++<<<<<<< HEAD
 +    if (userFullname === '') {
++=======
+     if (userFullname === "") {
++>>>>>>> main
          dispatch<any>(loadResource(uuid, false));
      }
      return <span>{userFullname ? userFullname : uuid}</span>;
  
  export const ResponsiblePerson = compose(
      connect((state: RootState, props: { uuid: string; parentRef: HTMLElement | null }) => {
++<<<<<<< HEAD
 +        let responsiblePersonName: string = '';
 +        let responsiblePersonUUID: string = '';
 +        let responsiblePersonProperty: string = '';
++=======
+         let responsiblePersonName: string = "";
+         let responsiblePersonUUID: string = "";
+         let responsiblePersonProperty: string = "";
++>>>>>>> main
  
          if (state.auth.config.clusterConfig.Collections.ManagedProperties) {
              let index = 0;
  
              while (!responsiblePersonProperty && keys[index]) {
                  const key = keys[index];
++<<<<<<< HEAD
 +                if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === 'original_owner') {
++=======
+                 if (state.auth.config.clusterConfig.Collections.ManagedProperties[key].Function === "original_owner") {
++>>>>>>> main
                      responsiblePersonProperty = key;
                  }
                  index++;
      const { uuid, responsiblePersonName, parentRef, theme } = props;
  
      if (!uuid && parentRef) {
++<<<<<<< HEAD
 +        parentRef.style.display = 'none';
 +        return null;
 +    } else if (parentRef) {
 +        parentRef.style.display = 'block';
++=======
+         parentRef.style.display = "none";
+         return null;
+     } else if (parentRef) {
+         parentRef.style.display = "block";
++>>>>>>> main
      }
  
      if (!responsiblePersonName) {
          return (
++<<<<<<< HEAD
 +            <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
++=======
+             <Typography
+                 style={{ color: theme.palette.primary.main }}
+                 inline
+                 noWrap
+             >
++>>>>>>> main
                  {uuid}
              </Typography>
          );
      }
  
      return (
++<<<<<<< HEAD
 +        <Typography style={{ color: theme.palette.primary.main }} inline noWrap>
++=======
+         <Typography
+             style={{ color: theme.palette.primary.main }}
+             inline
+             noWrap
+         >
++>>>>>>> main
              {responsiblePersonName} ({uuid})
          </Typography>
      );
@@@ -907,19 -998,27 +1436,39 @@@ const renderType = (type: string, subty
  
  export const ResourceType = connect((state: RootState, props: { uuid: string }) => {
      const resource = getResource<GroupContentsResource>(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { type: resource ? resource.kind : '', subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : '' };
++=======
+     return { type: resource ? resource.kind : "", subtype: resource && resource.kind === ResourceKind.GROUP ? resource.groupClass : "" };
++>>>>>>> main
  })((props: { type: string; subtype: string }) => renderType(props.type, props.subtype));
  
  export const ResourceStatus = connect((state: RootState, props: { uuid: string }) => {
      return { resource: getResource<GroupContentsResource>(props.uuid)(state.resources) };
  })((props: { resource: GroupContentsResource }) =>
++<<<<<<< HEAD
 +    props.resource && props.resource.kind === ResourceKind.COLLECTION ? <CollectionStatus uuid={props.resource.uuid} /> : <ProcessStatus uuid={props.resource.uuid} />
++=======
+     props.resource && props.resource.kind === ResourceKind.COLLECTION ? (
+         <CollectionStatus uuid={props.resource.uuid} />
+     ) : (
+         <ProcessStatus uuid={props.resource.uuid} />
+     )
++>>>>>>> main
  );
  
  export const CollectionStatus = connect((state: RootState, props: { uuid: string }) => {
      return { collection: getResource<CollectionResource>(props.uuid)(state.resources) };
  })((props: { collection: CollectionResource }) =>
++<<<<<<< HEAD
 +    props.collection.uuid !== props.collection.currentVersionUuid ? <Typography>version {props.collection.version}</Typography> : <Typography>head version</Typography>
++=======
+     props.collection.uuid !== props.collection.currentVersionUuid ? (
+         <Typography>version {props.collection.version}</Typography>
+     ) : (
+         <Typography>head version</Typography>
+     )
++>>>>>>> main
  );
  
  export const CollectionName = connect((state: RootState, props: { uuid: string; className?: string }) => {
@@@ -945,7 -1044,7 +1494,11 @@@ export const ProcessStatus = compose
                  height: props.theme.spacing.unit * 3,
                  width: props.theme.spacing.unit * 12,
                  ...getProcessStatusStyles(getProcessStatus(props.process), props.theme),
++<<<<<<< HEAD
 +                fontSize: '0.875rem',
++=======
+                 fontSize: "0.875rem",
++>>>>>>> main
                  borderRadius: props.theme.spacing.unit * 0.625,
              }}
          />
  
  export const ProcessStartDate = connect((state: RootState, props: { uuid: string }) => {
      const process = getProcess(props.uuid)(state.resources);
++<<<<<<< HEAD
 +    return { date: process && process.container ? process.container.startedAt : '' };
 +})((props: { date: string }) => renderDate(props.date));
 +
 +export const renderRunTime = (time: number) => (
 +    <Typography noWrap style={{ minWidth: '45px' }}>
++=======
+     return { date: process && process.container ? process.container.startedAt : "" };
+ })((props: { date: string }) => renderDate(props.date));
+ export const renderRunTime = (time: number) => (
+     <Typography
+         noWrap
+         style={{ minWidth: "45px" }}
+     >
++>>>>>>> main
          {formatTime(time, true)}
      </Typography>
  );
index 85265b1333e4dfcd99ae5d401ade3ec2c78aa4c7,607bdeb763a95ae84d0d82a4b00209ef8e906e6c..b5afbf6545ed19f2eb84156f02534c3fa09ab3f8
@@@ -2,8 -2,8 +2,8 @@@
  //
  // SPDX-License-Identifier: AGPL-3.0
  
- import React, { ReactElement, useState } from "react";
 -import React, { ReactElement, memo, useState } from 'react';
 -import { Dispatch } from 'redux';
++import React, { ReactElement, memo, useState } from "react";
 +import { Dispatch } from "redux";
  import {
      StyleRulesCallback,
      WithStyles,
@@@ -47,25 -56,26 +47,26 @@@ import 
      isPrimitiveOfType,
      StringArrayCommandInputParameter,
      StringCommandInputParameter,
 -    getEnumType
++    getEnumType,
  } from "models/workflow";
 -import { CommandOutputParameter } from 'cwlts/mappings/v1.0/CommandOutputParameter';
 -import { File } from 'models/workflow';
 -import { getInlineFileUrl } from 'views-components/context-menu/actions/helpers';
 -import { AuthState } from 'store/auth/auth-reducer';
 -import mime from 'mime';
 -import { DefaultView } from 'components/default-view/default-view';
 -import { getNavUrl } from 'routes/routes';
 -import { Link as RouterLink } from 'react-router-dom';
 -import { Link as MuiLink } from '@material-ui/core';
 -import { InputCollectionMount } from 'store/processes/processes-actions';
 -import { connect } from 'react-redux';
 -import { RootState } from 'store/store';
 -import { ProcessOutputCollectionFiles } from './process-output-collection-files';
 -import { Process } from 'store/processes/process';
 -import { navigateTo } from 'store/navigation/navigation-action';
 -import classNames from 'classnames';
 -import { DefaultCodeSnippet } from 'components/default-code-snippet/default-code-snippet';
 -import { KEEP_URL_REGEX } from 'models/resource';
 +import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
 +import { File } from "models/workflow";
 +import { getInlineFileUrl } from "views-components/context-menu/actions/helpers";
 +import { AuthState } from "store/auth/auth-reducer";
 +import mime from "mime";
 +import { DefaultView } from "components/default-view/default-view";
 +import { getNavUrl } from "routes/routes";
 +import { Link as RouterLink } from "react-router-dom";
 +import { Link as MuiLink } from "@material-ui/core";
 +import { InputCollectionMount } from "store/processes/processes-actions";
 +import { connect } from "react-redux";
 +import { RootState } from "store/store";
 +import { ProcessOutputCollectionFiles } from "./process-output-collection-files";
 +import { Process } from "store/processes/process";
 +import { navigateTo } from "store/navigation/navigation-action";
 +import classNames from "classnames";
 +import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
 +import { KEEP_URL_REGEX } from "models/resource";
  
  type CssRules =
      | "card"
@@@ -231,239 -241,128 +232,253 @@@ const mapDispatchToProps = (dispatch: D
  
  type ProcessIOCardProps = ProcessIOCardDataProps & ProcessIOCardActionProps & WithStyles<CssRules> & MPVPanelProps;
  
 -export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps)(
 -    ({ classes, label, params, raw, mounts, outputUuid, doHidePanel, doMaximizePanel,
 -        doUnMaximizePanel, panelMaximized, panelName, process, navigateTo, showParams }: ProcessIOCardProps) => {
 -        const [mainProcTabState, setMainProcTabState] = useState(0);
 -        const [subProcTabState, setSubProcTabState] = useState(0);
 -        const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
 -            setMainProcTabState(value);
 -        }
 -        const handleSubProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
 -            setSubProcTabState(value);
 -        }
 -
 -        const [showImagePreview, setShowImagePreview] = useState(false);
 -
 -        const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
 -        const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
 -
 -        const loading = raw === null || raw === undefined || params === null;
 -        const hasRaw = !!(raw && Object.keys(raw).length > 0);
 -        const hasParams = !!(params && params.length > 0);
 -
 -        // Subprocess
 -        const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
 -        const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
 -
 -        return <Card className={classes.card} data-cy="process-io-card">
 -            <CardHeader
 -                className={classes.header}
 -                classes={{
 -                    content: classes.title,
 -                    avatar: classes.avatar,
 -                }}
 -                avatar={<PanelIcon className={classes.iconHeader} />}
 -                title={
 -                    <Typography noWrap variant='h6' color='inherit'>
 -                        {label}
 -                    </Typography>
 -                }
 -                action={
 -                    <div>
 -                        {mainProcess && <Tooltip title={"Toggle Image Preview"} disableFocusListener>
 -                            <IconButton data-cy="io-preview-image-toggle" onClick={() => { setShowImagePreview(!showImagePreview) }}>{showImagePreview ? <ImageIcon /> : <ImageOffIcon />}</IconButton>
 -                        </Tooltip>}
 -                        {doUnMaximizePanel && panelMaximized &&
 -                            <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
 -                                <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
 -                            </Tooltip>}
 -                        {doMaximizePanel && !panelMaximized &&
 -                            <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
 -                                <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
 -                            </Tooltip>}
 -                        {doHidePanel &&
 -                            <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
 -                                <IconButton disabled={panelMaximized} onClick={doHidePanel}><CloseIcon /></IconButton>
 -                            </Tooltip>}
 -                    </div>
 -                } />
 -            <CardContent className={classes.content}>
 -                {(mainProcess || showParams) ?
 -                    (<>
 -                        {/* raw is undefined until params are loaded */}
 -                        {loading && <Grid container item alignItems='center' justify='center'>
 -                            <CircularProgress />
 -                        </Grid>}
 -                        {/* Once loaded, either raw or params may still be empty
 -                        *   Raw when all params are empty
 -                        *   Params when raw is provided by containerRequest properties but workflow mount is absent for preview
 -                        */}
 -                        {(!loading && (hasRaw || hasParams)) &&
 +export const ProcessIOCard = withStyles(styles)(
 +    connect(
 +        null,
 +        mapDispatchToProps
 +    )(
 +        ({
 +            classes,
 +            label,
 +            params,
 +            raw,
 +            mounts,
 +            outputUuid,
 +            doHidePanel,
 +            doMaximizePanel,
 +            doUnMaximizePanel,
 +            panelMaximized,
 +            panelName,
 +            process,
 +            navigateTo,
 +            showParams,
 +        }: ProcessIOCardProps) => {
 +            const [mainProcTabState, setMainProcTabState] = useState(0);
 +            const [subProcTabState, setSubProcTabState] = useState(0);
 +            const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
 +                setMainProcTabState(value);
 +            };
 +            const handleSubProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
 +                setSubProcTabState(value);
 +            };
 +
 +            const [showImagePreview, setShowImagePreview] = useState(false);
 +
 +            const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
 +            const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
 +
 +            const loading = raw === null || raw === undefined || params === null;
 +            const hasRaw = !!(raw && Object.keys(raw).length > 0);
 +            const hasParams = !!(params && params.length > 0);
 +
 +            // Subprocess
 +            const hasInputMounts = !!(label === ProcessIOCardType.INPUT && mounts && mounts.length);
 +            const hasOutputCollecton = !!(label === ProcessIOCardType.OUTPUT && outputUuid);
 +
 +            return (
 +                <Card
 +                    className={classes.card}
-                     data-cy="process-io-card">
-                     {/* here */}
++                    data-cy="process-io-card"
++                >
 +                    <CardHeader
 +                        className={classes.header}
 +                        classes={{
 +                            content: classes.title,
 +                            avatar: classes.avatar,
 +                        }}
 +                        avatar={<PanelIcon className={classes.iconHeader} />}
 +                        title={
 +                            <Typography
 +                                noWrap
 +                                variant="h6"
-                                 color="inherit">
++                                color="inherit"
++                            >
 +                                {label}
 +                            </Typography>
 +                        }
 +                        action={
 +                            <div>
 +                                {mainProcess && (
 +                                    <Tooltip
 +                                        title={"Toggle Image Preview"}
-                                         disableFocusListener>
++                                        disableFocusListener
++                                    >
 +                                        <IconButton
 +                                            data-cy="io-preview-image-toggle"
 +                                            onClick={() => {
 +                                                setShowImagePreview(!showImagePreview);
-                                             }}>
++                                            }}
++                                        >
 +                                            {showImagePreview ? <ImageIcon /> : <ImageOffIcon />}
 +                                        </IconButton>
 +                                    </Tooltip>
 +                                )}
 +                                {doUnMaximizePanel && panelMaximized && (
 +                                    <Tooltip
 +                                        title={`Unmaximize ${panelName || "panel"}`}
-                                         disableFocusListener>
++                                        disableFocusListener
++                                    >
 +                                        <IconButton onClick={doUnMaximizePanel}>
 +                                            <UnMaximizeIcon />
 +                                        </IconButton>
 +                                    </Tooltip>
 +                                )}
 +                                {doMaximizePanel && !panelMaximized && (
 +                                    <Tooltip
 +                                        title={`Maximize ${panelName || "panel"}`}
-                                         disableFocusListener>
++                                        disableFocusListener
++                                    >
 +                                        <IconButton onClick={doMaximizePanel}>
 +                                            <MaximizeIcon />
 +                                        </IconButton>
 +                                    </Tooltip>
 +                                )}
 +                                {doHidePanel && (
 +                                    <Tooltip
 +                                        title={`Close ${panelName || "panel"}`}
-                                         disableFocusListener>
++                                        disableFocusListener
++                                    >
 +                                        <IconButton
 +                                            disabled={panelMaximized}
-                                             onClick={doHidePanel}>
++                                            onClick={doHidePanel}
++                                        >
 +                                            <CloseIcon />
 +                                        </IconButton>
 +                                    </Tooltip>
 +                                )}
 +                            </div>
 +                        }
 +                    />
 +                    <CardContent className={classes.content}>
 +                        {mainProcess || showParams ? (
                              <>
 -                                <Tabs value={mainProcTabState} onChange={handleMainProcTabChange} variant="fullWidth" className={classes.symmetricTabs}>
 -                                    {/* params will be empty on processes without workflow definitions in mounts, so we only show raw */}
 -                                    {hasParams && <Tab label="Parameters" />}
 -                                    {!showParams && <Tab label="JSON" />}
 -                                </Tabs>
 -                                {(mainProcTabState === 0 && params && hasParams) && <div className={classes.tableWrapper}>
 -                                    <ProcessIOPreview data={params} showImagePreview={showImagePreview} valueLabel={showParams ? "Default value" : "Value"} />
 -                                </div>}
 -                                {(mainProcTabState === 1 || !hasParams) && <div className={classes.tableWrapper}>
 -                                    <ProcessIORaw data={raw} />
 -                                </div>}
 -                            </>}
 -                        {!loading && !hasRaw && !hasParams && <Grid container item alignItems='center' justify='center'>
 -                            <DefaultView messages={["No parameters found"]} />
 -                        </Grid>}
 -                    </>) :
 -                    // Subprocess
 -                    (<>
 -                        {loading && <Grid container item alignItems='center' justify='center'>
 -                            <CircularProgress />
 -                        </Grid>}
 -                        {!loading && (hasInputMounts || hasOutputCollecton || hasRaw) ?
 +                                {/* raw is undefined until params are loaded */}
 +                                {loading && (
 +                                    <Grid
 +                                        container
 +                                        item
 +                                        alignItems="center"
-                                         justify="center">
++                                        justify="center"
++                                    >
 +                                        <CircularProgress />
 +                                    </Grid>
 +                                )}
 +                                {/* Once loaded, either raw or params may still be empty
 +                                 *   Raw when all params are empty
 +                                 *   Params when raw is provided by containerRequest properties but workflow mount is absent for preview
 +                                 */}
 +                                {!loading && (hasRaw || hasParams) && (
 +                                    <>
 +                                        <Tabs
 +                                            value={mainProcTabState}
 +                                            onChange={handleMainProcTabChange}
 +                                            variant="fullWidth"
-                                             className={classes.symmetricTabs}>
++                                            className={classes.symmetricTabs}
++                                        >
 +                                            {/* params will be empty on processes without workflow definitions in mounts, so we only show raw */}
 +                                            {hasParams && <Tab label="Parameters" />}
 +                                            {!showParams && <Tab label="JSON" />}
 +                                        </Tabs>
 +                                        {mainProcTabState === 0 && params && hasParams && (
 +                                            <div className={classes.tableWrapper}>
 +                                                <ProcessIOPreview
 +                                                    data={params}
 +                                                    showImagePreview={showImagePreview}
 +                                                    valueLabel={showParams ? "Default value" : "Value"}
 +                                                />
 +                                            </div>
 +                                        )}
 +                                        {(mainProcTabState === 1 || !hasParams) && (
 +                                            <div className={classes.tableWrapper}>
 +                                                <ProcessIORaw data={raw} />
 +                                            </div>
 +                                        )}
 +                                    </>
 +                                )}
 +                                {!loading && !hasRaw && !hasParams && (
 +                                    <Grid
 +                                        container
 +                                        item
 +                                        alignItems="center"
-                                         justify="center">
++                                        justify="center"
++                                    >
 +                                        <DefaultView messages={["No parameters found"]} />
 +                                    </Grid>
 +                                )}
 +                            </>
 +                        ) : (
 +                            // Subprocess
                              <>
 -                                <Tabs value={subProcTabState} onChange={handleSubProcTabChange} variant="fullWidth" className={classes.symmetricTabs}>
 -                                    {hasInputMounts && <Tab label="Collections" />}
 -                                    {hasOutputCollecton && <Tab label="Collection" />}
 -                                    <Tab label="JSON" />
 -                                </Tabs>
 -                                <div className={classes.tableWrapper}>
 -                                    {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
 -                                    {subProcTabState === 0 && hasOutputCollecton && <>
 -                                        {outputUuid && <Typography className={classes.collectionLink}>
 -                                            Output Collection: <MuiLink className={classes.keepLink} onClick={() => { navigateTo(outputUuid || "") }}>
 -                                                {outputUuid}
 -                                            </MuiLink></Typography>}
 -                                        <ProcessOutputCollectionFiles isWritable={false} currentItemUuid={outputUuid} />
 -                                    </>}
 -                                    {(subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && <div className={classes.tableWrapper}>
 -                                        <ProcessIORaw data={raw} />
 -                                    </div>}
 -                                </div>
 -                            </> :
 -                            <Grid container item alignItems='center' justify='center'>
 -                                <DefaultView messages={["No data to display"]} />
 -                            </Grid>
 -                        }
 -                    </>)
 -                }
 -            </CardContent>
 -        </Card>;
 -    }
 -));
 +                                {loading && (
 +                                    <Grid
 +                                        container
 +                                        item
 +                                        alignItems="center"
-                                         justify="center">
++                                        justify="center"
++                                    >
 +                                        <CircularProgress />
 +                                    </Grid>
 +                                )}
 +                                {!loading && (hasInputMounts || hasOutputCollecton || hasRaw) ? (
 +                                    <>
 +                                        <Tabs
 +                                            value={subProcTabState}
 +                                            onChange={handleSubProcTabChange}
 +                                            variant="fullWidth"
-                                             className={classes.symmetricTabs}>
++                                            className={classes.symmetricTabs}
++                                        >
 +                                            {hasInputMounts && <Tab label="Collections" />}
 +                                            {hasOutputCollecton && <Tab label="Collection" />}
 +                                            <Tab label="JSON" />
 +                                        </Tabs>
 +                                        <div className={classes.tableWrapper}>
 +                                            {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
 +                                            {subProcTabState === 0 && hasOutputCollecton && (
 +                                                <>
 +                                                    {outputUuid && (
 +                                                        <Typography className={classes.collectionLink}>
 +                                                            Output Collection:{" "}
 +                                                            <MuiLink
 +                                                                className={classes.keepLink}
 +                                                                onClick={() => {
 +                                                                    navigateTo(outputUuid || "");
-                                                                 }}>
++                                                                }}
++                                                            >
 +                                                                {outputUuid}
 +                                                            </MuiLink>
 +                                                        </Typography>
 +                                                    )}
 +                                                    <ProcessOutputCollectionFiles
 +                                                        isWritable={false}
 +                                                        currentItemUuid={outputUuid}
 +                                                    />
 +                                                </>
 +                                            )}
 +                                            {(subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && (
 +                                                <div className={classes.tableWrapper}>
 +                                                    <ProcessIORaw data={raw} />
 +                                                </div>
 +                                            )}
 +                                        </div>
 +                                    </>
 +                                ) : (
 +                                    <Grid
 +                                        container
 +                                        item
 +                                        alignItems="center"
-                                         justify="center">
++                                        justify="center"
++                                    >
 +                                        <DefaultView messages={["No data to display"]} />
 +                                    </Grid>
 +                                )}
 +                            </>
 +                        )}
 +                    </CardContent>
 +                </Card>
 +            );
 +        }
 +    )
 +);
  
  export type ProcessIOValue = {
      display: ReactElement<any, any>;
@@@ -486,77 -385,64 +501,82 @@@ interface ProcessIOPreviewDataProps 
  
  type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
  
- const ProcessIOPreview = withStyles(styles)(({ classes, data, showImagePreview, valueLabel }: ProcessIOPreviewProps) => {
-     const showLabel = data.some((param: ProcessIOParameter) => param.label);
-     return (
-         <Table
-             className={classes.tableRoot}
-             aria-label="Process IO Preview">
-             <TableHead>
-                 <TableRow>
-                     <TableCell>Name</TableCell>
-                     {showLabel && <TableCell className={classes.labelColumn}>Label</TableCell>}
-                     <TableCell>{valueLabel}</TableCell>
-                     <TableCell>Collection</TableCell>
-                 </TableRow>
-             </TableHead>
-             <TableBody>
-                 {data.map((param: ProcessIOParameter) => {
-                     const firstVal = param.value.length > 0 ? param.value[0] : undefined;
-                     const rest = param.value.slice(1);
-                     const mainRowClasses = {
-                         [classes.noBorderRow]: rest.length > 0,
-                     };
-                     return (
-                         <React.Fragment key={param.id}>
-                             <TableRow
-                                 className={classNames(mainRowClasses)}
-                                 data-cy="process-io-param">
-                                 <TableCell>{param.id}</TableCell>
-                                 {showLabel && <TableCell>{param.label}</TableCell>}
-                                 <TableCell>
-                                     {firstVal && (
-                                         <ProcessValuePreview
-                                             value={firstVal}
-                                             showImagePreview={showImagePreview}
-                                         />
-                                     )}
-                                 </TableCell>
-                                 <TableCell className={firstVal?.imageUrl ? classes.rowWithPreview : undefined}>
-                                     <Typography className={classes.paramValue}>{firstVal?.collection}</Typography>
-                                 </TableCell>
-                             </TableRow>
-                             {rest.map((val, i) => {
-                                 const rowClasses = {
-                                     [classes.noBorderRow]: i < rest.length - 1,
-                                     [classes.secondaryRow]: val.secondary,
-                                 };
-                                 return (
-                                     <TableRow
-                                         className={classNames(rowClasses)}
-                                         key={i}>
-                                         <TableCell />
-                                         {showLabel && <TableCell />}
-                                         <TableCell>
 -const ProcessIOPreview = memo(withStyles(styles)(
 -    ({ classes, data, showImagePreview, valueLabel }: ProcessIOPreviewProps) => {
++const ProcessIOPreview = memo(
++    withStyles(styles)(({ classes, data, showImagePreview, valueLabel }: ProcessIOPreviewProps) => {
+         const showLabel = data.some((param: ProcessIOParameter) => param.label);
 -        return <Table className={classes.tableRoot} aria-label="Process IO Preview">
 -            <TableHead>
 -                <TableRow>
 -                    <TableCell>Name</TableCell>
 -                    {showLabel && <TableCell className={classes.labelColumn}>Label</TableCell>}
 -                    <TableCell>{valueLabel}</TableCell>
 -                    <TableCell>Collection</TableCell>
 -                </TableRow>
 -            </TableHead>
 -            <TableBody>
 -                {data.map((param: ProcessIOParameter) => {
 -                    const firstVal = param.value.length > 0 ? param.value[0] : undefined;
 -                    const rest = param.value.slice(1);
 -                    const mainRowClasses = {
 -                        [classes.noBorderRow]: (rest.length > 0),
 -                    };
 -
 -                    return <React.Fragment key={param.id}>
 -                        <TableRow className={classNames(mainRowClasses)} data-cy="process-io-param">
 -                            <TableCell>
 -                                {param.id}
 -                            </TableCell>
 -                            {showLabel && <TableCell >{param.label}</TableCell>}
 -                            <TableCell>
 -                                {firstVal && <ProcessValuePreview value={firstVal} showImagePreview={showImagePreview} />}
 -                            </TableCell>
 -                            <TableCell className={firstVal?.imageUrl ? classes.rowWithPreview : undefined}>
 -                                <Typography className={classes.paramValue}>
 -                                    {firstVal?.collection}
 -                                </Typography>
 -                            </TableCell>
 -                        </TableRow>
 -                        {rest.map((val, i) => {
 -                            const rowClasses = {
 -                                [classes.noBorderRow]: (i < rest.length - 1),
 -                                [classes.secondaryRow]: val.secondary,
 -                            };
 -                            return <TableRow className={classNames(rowClasses)} key={i}>
 -                                <TableCell />
 -                                {showLabel && <TableCell />}
 -                                <TableCell>
 -                                    <ProcessValuePreview value={val} showImagePreview={showImagePreview} />
 -                                </TableCell>
 -                                <TableCell className={firstVal?.imageUrl ? classes.rowWithPreview : undefined}>
 -                                    <Typography className={classes.paramValue}>
 -                                        {val.collection}
 -                                    </Typography>
 -                                </TableCell>
 -                            </TableRow>
 -                        })}
 -                    </React.Fragment>;
 -                })}
 -            </TableBody>
 -        </Table >;
 -    }));
++        return (
++            <Table
++                className={classes.tableRoot}
++                aria-label="Process IO Preview"
++            >
++                <TableHead>
++                    <TableRow>
++                        <TableCell>Name</TableCell>
++                        {showLabel && <TableCell className={classes.labelColumn}>Label</TableCell>}
++                        <TableCell>{valueLabel}</TableCell>
++                        <TableCell>Collection</TableCell>
++                    </TableRow>
++                </TableHead>
++                <TableBody>
++                    {data.map((param: ProcessIOParameter) => {
++                        const firstVal = param.value.length > 0 ? param.value[0] : undefined;
++                        const rest = param.value.slice(1);
++                        const mainRowClasses = {
++                            [classes.noBorderRow]: rest.length > 0,
++                        };
++
++                        return (
++                            <React.Fragment key={param.id}>
++                                <TableRow
++                                    className={classNames(mainRowClasses)}
++                                    data-cy="process-io-param"
++                                >
++                                    <TableCell>{param.id}</TableCell>
++                                    {showLabel && <TableCell>{param.label}</TableCell>}
++                                    <TableCell>
++                                        {firstVal && (
 +                                            <ProcessValuePreview
-                                                 value={val}
++                                                value={firstVal}
 +                                                showImagePreview={showImagePreview}
 +                                            />
-                                         </TableCell>
-                                         <TableCell className={firstVal?.imageUrl ? classes.rowWithPreview : undefined}>
-                                             <Typography className={classes.paramValue}>{val.collection}</Typography>
-                                         </TableCell>
-                                     </TableRow>
-                                 );
-                             })}
-                         </React.Fragment>
-                     );
-                 })}
-             </TableBody>
-         </Table>
-     );
- });
++                                        )}
++                                    </TableCell>
++                                    <TableCell className={firstVal?.imageUrl ? classes.rowWithPreview : undefined}>
++                                        <Typography className={classes.paramValue}>{firstVal?.collection}</Typography>
++                                    </TableCell>
++                                </TableRow>
++                                {rest.map((val, i) => {
++                                    const rowClasses = {
++                                        [classes.noBorderRow]: i < rest.length - 1,
++                                        [classes.secondaryRow]: val.secondary,
++                                    };
++                                    return (
++                                        <TableRow
++                                            className={classNames(rowClasses)}
++                                            key={i}
++                                        >
++                                            <TableCell />
++                                            {showLabel && <TableCell />}
++                                            <TableCell>
++                                                <ProcessValuePreview
++                                                    value={val}
++                                                    showImagePreview={showImagePreview}
++                                                />
++                                            </TableCell>
++                                            <TableCell className={firstVal?.imageUrl ? classes.rowWithPreview : undefined}>
++                                                <Typography className={classes.paramValue}>{val.collection}</Typography>
++                                            </TableCell>
++                                        </TableRow>
++                                    );
++                                })}
++                            </React.Fragment>
++                        );
++                    })}
++                </TableBody>
++            </Table>
++        );
++    })
++);
  
  interface ProcessValuePreviewProps {
      value: ProcessIOValue;
@@@ -598,38 -477,28 +618,40 @@@ interface ProcessInputMountsDataProps 
  
  type ProcessInputMountsProps = ProcessInputMountsDataProps & WithStyles<CssRules>;
  
 -const ProcessInputMounts = withStyles(styles)(connect((state: RootState) => ({
 -    auth: state.auth,
 -}))(({ mounts, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => (
 -    <Table className={classes.tableRoot} aria-label="Process Input Mounts">
 -        <TableHead>
 -            <TableRow>
 -                <TableCell>Path</TableCell>
 -                <TableCell>Portable Data Hash</TableCell>
 -            </TableRow>
 -        </TableHead>
 -        <TableBody>
 -            {mounts.map(mount => (
 -                <TableRow key={mount.path}>
 -                    <TableCell><pre>{mount.path}</pre></TableCell>
 -                    <TableCell>
 -                        <RouterLink to={getNavUrl(mount.pdh, auth)} className={classes.keepLink}>{mount.pdh}</RouterLink>
 -                    </TableCell>
 +const ProcessInputMounts = withStyles(styles)(
 +    connect((state: RootState) => ({
 +        auth: state.auth,
 +    }))(({ mounts, classes, auth }: ProcessInputMountsProps & { auth: AuthState }) => (
 +        <Table
 +            className={classes.tableRoot}
-             aria-label="Process Input Mounts">
++            aria-label="Process Input Mounts"
++        >
 +            <TableHead>
 +                <TableRow>
 +                    <TableCell>Path</TableCell>
 +                    <TableCell>Portable Data Hash</TableCell>
                  </TableRow>
 -            ))}
 -        </TableBody>
 -    </Table>
 -)));
 +            </TableHead>
 +            <TableBody>
 +                {mounts.map(mount => (
 +                    <TableRow key={mount.path}>
 +                        <TableCell>
 +                            <pre>{mount.path}</pre>
 +                        </TableCell>
 +                        <TableCell>
 +                            <RouterLink
 +                                to={getNavUrl(mount.pdh, auth)}
-                                 className={classes.keepLink}>
++                                className={classes.keepLink}
++                            >
 +                                {mount.pdh}
 +                            </RouterLink>
 +                        </TableCell>
 +                    </TableRow>
 +                ))}
 +            </TableBody>
 +        </Table>
 +    ))
 +);
  
  type FileWithSecondaryFiles = {
      secondaryFiles: File[];
@@@ -677,13 -552,16 +699,13 @@@ export const getIOParamDisplayValue = (
  
          case isPrimitiveOfType(input, CWLType.DIRECTORY):
              const directory = (input as DirectoryCommandInputParameter).value;
 -            return directory !== undefined &&
 -                !(Array.isArray(directory) && directory.length === 0) ?
 -                [directoryToProcessIOValue(directory, auth, pdh)] :
 -                [{ display: <EmptyValue /> }];
 +            return directory !== undefined && !(Array.isArray(directory) && directory.length === 0)
 +                ? [directoryToProcessIOValue(directory, auth, pdh)]
 +                : [{ display: <EmptyValue /> }];
  
-         case typeof input.type === "object" && !(input.type instanceof Array) && input.type.type === "enum":
+         case getEnumType(input) !== null:
              const enumValue = (input as EnumCommandInputParameter).value;
 -            return enumValue !== undefined && enumValue ?
 -                [{ display: <pre>{enumValue}</pre> }] :
 -                [{ display: <EmptyValue /> }];
 +            return enumValue !== undefined && enumValue ? [{ display: <pre>{enumValue}</pre> }] : [{ display: <EmptyValue /> }];
  
          case isArrayOfType(input, CWLType.STRING):
              const strArray = (input as StringArrayCommandInputParameter).value || [];
          case isArrayOfType(input, CWLType.FLOAT):
          case isArrayOfType(input, CWLType.DOUBLE):
              const floatArray = (input as FloatArrayCommandInputParameter).value || [];
 -            return floatArray.length ?
 -                [{ display: <>{floatArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
 -                [{ display: <EmptyValue /> }];
 +            return floatArray.length ? [{ display: <>{floatArray.map(val => renderPrimitiveValue(val, true))}</> }] : [{ display: <EmptyValue /> }];
  
          case isArrayOfType(input, CWLType.FILE):
 -            const fileArrayMainFiles = ((input as FileArrayCommandInputParameter).value || []);
 -            const firstMainFilePdh = (fileArrayMainFiles.length > 0 && fileArrayMainFiles[0]) ? getResourcePdhUrl(fileArrayMainFiles[0], pdh) : "";
 +            const fileArrayMainFiles = (input as FileArrayCommandInputParameter).value || [];
 +            const firstMainFilePdh = fileArrayMainFiles.length > 0 && fileArrayMainFiles[0] ? getResourcePdhUrl(fileArrayMainFiles[0], pdh) : "";
  
-             // Convert each main file into separate arrays of ProcessIOValue to preserve secondaryFile grouping
-             const fileArrayValues = fileArrayMainFiles
-                 .map((mainFile: File, i): ProcessIOValue[] => {
-                     const secondaryFiles = (mainFile as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
-                     return [
-                         // Pass firstMainFilePdh to secondary files and every main file besides the first to hide pdh if equal
-                         ...(mainFile ? [fileToProcessIOValue(mainFile, false, auth, pdh, i > 0 ? firstMainFilePdh : "")] : []),
-                         ...secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh)),
-                     ];
-                     // Reduce each mainFile/secondaryFile group into single array preserving ordering
-                 })
-                 .reduce((acc: ProcessIOValue[], mainFile: ProcessIOValue[]) => acc.concat(mainFile), []);
+             // Convert each main and secondaryFiles into array of ProcessIOValue preserving ordering
+             let fileArrayValues: ProcessIOValue[] = [];
+             for (let i = 0; i < fileArrayMainFiles.length; i++) {
 -                const secondaryFiles = ((fileArrayMainFiles[i] as unknown) as FileWithSecondaryFiles)?.secondaryFiles || [];
++                const secondaryFiles = (fileArrayMainFiles[i] as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
+                 fileArrayValues.push(
+                     // Pass firstMainFilePdh to secondary files and every main file besides the first to hide pdh if equal
+                     ...(fileArrayMainFiles[i] ? [fileToProcessIOValue(fileArrayMainFiles[i], false, auth, pdh, i > 0 ? firstMainFilePdh : "")] : []),
 -                    ...(secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh)))
++                    ...secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh))
+                 );
+             }
  
 -            return fileArrayValues.length ?
 -                fileArrayValues :
 -                [{ display: <EmptyValue /> }];
 +            return fileArrayValues.length ? fileArrayValues : [{ display: <EmptyValue /> }];
  
          case isArrayOfType(input, CWLType.DIRECTORY):
              const directories = (input as DirectoryArrayCommandInputParameter).value || [];
@@@ -767,38 -648,20 +787,40 @@@ const KeepUrlBase = withStyles(styles)(
      const pdhUrl = getResourcePdhUrl(res, pdh);
      // Passing a pdh always returns a relative wb2 collection url
      const pdhWbPath = getNavUrl(pdhUrl, auth);
 -    return pdhUrl && pdhWbPath ?
 -        <Tooltip title={"View collection in Workbench"}><RouterLink to={pdhWbPath} className={classes.keepLink}>{pdhUrl}</RouterLink></Tooltip> :
 -        <></>;
 +    return pdhUrl && pdhWbPath ? (
 +        <Tooltip title={"View collection in Workbench"}>
 +            <RouterLink
 +                to={pdhWbPath}
-                 className={classes.keepLink}>
++                className={classes.keepLink}
++            >
 +                {pdhUrl}
 +            </RouterLink>
 +        </Tooltip>
 +    ) : (
 +        <></>
 +    );
  });
  
  const KeepUrlPath = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
      const keepUrl = getKeepUrl(res, pdh);
 -    const keepUrlParts = keepUrl ? keepUrl.split('/') : [];
 -    const keepUrlPath = keepUrlParts.length > 1 ? keepUrlParts.slice(1).join('/') : '';
 +    const keepUrlParts = keepUrl ? keepUrl.split("/") : [];
 +    const keepUrlPath = keepUrlParts.length > 1 ? keepUrlParts.slice(1).join("/") : "";
  
      const keepUrlPathNav = getKeepNavUrl(auth, res, pdh);
 -    return keepUrlPathNav ?
 -        <Tooltip title={"View in keep-web"}><a className={classes.keepLink} href={keepUrlPathNav} target="_blank" rel="noopener noreferrer">{keepUrlPath || '/'}</a></Tooltip> :
 -        <EmptyValue />;
 +    return keepUrlPathNav ? (
 +        <Tooltip title={"View in keep-web"}>
 +            <a
 +                className={classes.keepLink}
 +                href={keepUrlPathNav}
 +                target="_blank"
-                 rel="noopener noreferrer">
++                rel="noopener noreferrer"
++            >
 +                {keepUrlPath || "/"}
 +            </a>
 +        </Tooltip>
 +    ) : (
 +        <EmptyValue />
 +    );
  });
  
  const getKeepNavUrl = (auth: AuthState, file: File | Directory, pdh?: string): string => {
@@@ -867,13 -708,7 +889,14 @@@ const fileToProcessIOValue = (file: Fil
  
      if (isFileUrl(file.location)) {
          return {
 -            display: <MuiLink href={file.location} target="_blank">{file.location}</MuiLink>,
 +            display: (
 +                <MuiLink
 +                    href={file.location}
-                     target="_blank">
++                    target="_blank"
++                >
 +                    {file.location}
 +                </MuiLink>
 +            ),
              secondary,
          };
      }
index d4c8d9375d6f3071431927cda7cdb191cbeca768,c04cf62afc41db0a492d5d460ce27536b53faa16..d019d1418fcf0c8841a94989e1211daffaf864b6
@@@ -156,18 -141,17 +157,19 @@@ export const ProcessPanelRoot = withSty
                          lines={getProcessPanelLogs(processLogsPanel)}
                          selectedFilter={{
                              label: processLogsPanel.selectedFilter,
 -                            value: processLogsPanel.selectedFilter
 +                            value: processLogsPanel.selectedFilter,
                          }}
 -                        filters={processLogsPanel.filters.map(
 -                            filter => ({ label: filter, value: filter })
 -                        )}
 +                        filters={processLogsPanel.filters.map(filter => ({ label: filter, value: filter }))}
                          onLogFilterChange={props.onLogFilterChange}
                          navigateToLog={props.navigateToLog}
+                         pollProcessLogs={props.pollProcessLogs}
                      />
                  </MPVPanelContent>
 -                <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-inputs">
 +                <MPVPanelContent
 +                    forwardProps
 +                    xs
 +                    maxHeight="50%"
 +                    data-cy="process-inputs">
                      <ProcessIOCard
                          label={ProcessIOCardType.INPUT}
                          process={process}
index afad4832cc4ab9e250b715fc486c650fb5eea515,575c6591ef0481902a42797fa490a441180e7bc2..f9e02540eb92a62b931c34798faace31aa9fb843
@@@ -16,14 -23,14 +16,14 @@@ import 
      loadOutputs,
      toggleProcessPanelFilter,
      updateOutputParams,
 -    loadNodeJson
 -} from 'store/process-panel/process-panel-actions';
 -import { cancelRunningWorkflow, resumeOnHoldWorkflow, startWorkflow } from 'store/processes/processes-actions';
 -import { navigateToLogCollection, pollProcessLogs, setProcessLogsPanelFilter } from 'store/process-logs-panel/process-logs-panel-actions';
 -import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 +    loadNodeJson,
 +} from "store/process-panel/process-panel-actions";
 +import { cancelRunningWorkflow, resumeOnHoldWorkflow, startWorkflow } from "store/processes/processes-actions";
- import { navigateToLogCollection, setProcessLogsPanelFilter } from "store/process-logs-panel/process-logs-panel-actions";
++import { navigateToLogCollection, pollProcessLogs, setProcessLogsPanelFilter } from "store/process-logs-panel/process-logs-panel-actions";
 +import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
  
  const mapStateToProps = ({ router, auth, resources, processPanel, processLogsPanel }: RootState): ProcessPanelRootDataProps => {
 -    const uuid = getProcessPanelCurrentUuid(router) || '';
 +    const uuid = getProcessPanelCurrentUuid(router) || "";
      const subprocesses = getSubprocesses(uuid)(resources);
      return {
          process: getProcess(uuid)(resources),
@@@ -56,16 -61,17 +56,17 @@@ const mapDispatchToProps = (dispatch: D
      onToggle: status => {
          dispatch<any>(toggleProcessPanelFilter(status));
      },
 -    cancelProcess: (uuid) => dispatch<any>(cancelRunningWorkflow(uuid)),
 -    startProcess: (uuid) => dispatch<any>(startWorkflow(uuid)),
 -    resumeOnHoldWorkflow: (uuid) => dispatch<any>(resumeOnHoldWorkflow(uuid)),
 -    onLogFilterChange: (filter) => dispatch(setProcessLogsPanelFilter(filter.value)),
 -    navigateToLog: (uuid) => dispatch<any>(navigateToLogCollection(uuid)),
 -    loadInputs: (containerRequest) => dispatch<any>(loadInputs(containerRequest)),
 -    loadOutputs: (containerRequest) => dispatch<any>(loadOutputs(containerRequest)),
 -    loadOutputDefinitions: (containerRequest) => dispatch<any>(loadOutputDefinitions(containerRequest)),
 +    cancelProcess: uuid => dispatch<any>(cancelRunningWorkflow(uuid)),
 +    startProcess: uuid => dispatch<any>(startWorkflow(uuid)),
 +    resumeOnHoldWorkflow: uuid => dispatch<any>(resumeOnHoldWorkflow(uuid)),
 +    onLogFilterChange: filter => dispatch(setProcessLogsPanelFilter(filter.value)),
 +    navigateToLog: uuid => dispatch<any>(navigateToLogCollection(uuid)),
 +    loadInputs: containerRequest => dispatch<any>(loadInputs(containerRequest)),
 +    loadOutputs: containerRequest => dispatch<any>(loadOutputs(containerRequest)),
 +    loadOutputDefinitions: containerRequest => dispatch<any>(loadOutputDefinitions(containerRequest)),
      updateOutputParams: () => dispatch<any>(updateOutputParams()),
 -    loadNodeJson: (containerRequest) => dispatch<any>(loadNodeJson(containerRequest)),
 -    pollProcessLogs: (processUuid) => dispatch<any>(pollProcessLogs(processUuid)),
 +    loadNodeJson: containerRequest => dispatch<any>(loadNodeJson(containerRequest)),
++    pollProcessLogs: processUuid => dispatch<any>(pollProcessLogs(processUuid)),
  });
  
  const getFilters = (processPanel: ProcessPanelState, processes: Process[]) => {
index f7b8e8337bc2896118730c08684f026a27d8803d,ce9307465e19ebdcec22c46cbbe9236312649603..be2542515bb6e478103e4fb38802ed32d2dec7c6
  //
  // SPDX-License-Identifier: AGPL-3.0
  
 -import React from 'react';
 -import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core/styles';
 +import React from "react";
 +import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core/styles";
  import { Route, Switch } from "react-router";
  import { ProjectPanel } from "views/project-panel/project-panel";
 -import { DetailsPanel } from 'views-components/details-panel/details-panel';
 -import { ArvadosTheme } from 'common/custom-theme';
 +import { DetailsPanel } from "views-components/details-panel/details-panel";
 +import { ArvadosTheme } from "common/custom-theme";
  import { ContextMenu } from "views-components/context-menu/context-menu";
  import { FavoritePanel } from "../favorite-panel/favorite-panel";
 -import { TokenDialog } from 'views-components/token-dialog/token-dialog';
 -import { RichTextEditorDialog } from 'views-components/rich-text-editor-dialog/rich-text-editor-dialog';
 -import { Snackbar } from 'views-components/snackbar/snackbar';
 -import { CollectionPanel } from '../collection-panel/collection-panel';
 -import { RenameFileDialog } from 'views-components/rename-file-dialog/rename-file-dialog';
 -import { FileRemoveDialog } from 'views-components/file-remove-dialog/file-remove-dialog';
 -import { MultipleFilesRemoveDialog } from 'views-components/file-remove-dialog/multiple-files-remove-dialog';
 -import { Routes } from 'routes/routes';
 -import { SidePanel } from 'views-components/side-panel/side-panel';
 -import { ProcessPanel } from 'views/process-panel/process-panel';
 -import { ChangeWorkflowDialog } from 'views-components/run-process-dialog/change-workflow-dialog';
 -import { CreateProjectDialog } from 'views-components/dialog-forms/create-project-dialog';
 -import { CreateCollectionDialog } from 'views-components/dialog-forms/create-collection-dialog';
 -import { CopyCollectionDialog } from 'views-components/dialog-forms/copy-collection-dialog';
 -import { CopyProcessDialog } from 'views-components/dialog-forms/copy-process-dialog';
 -import { UpdateCollectionDialog } from 'views-components/dialog-forms/update-collection-dialog';
 -import { UpdateProcessDialog } from 'views-components/dialog-forms/update-process-dialog';
 -import { UpdateProjectDialog } from 'views-components/dialog-forms/update-project-dialog';
 -import { MoveProcessDialog } from 'views-components/dialog-forms/move-process-dialog';
 -import { MoveProjectDialog } from 'views-components/dialog-forms/move-project-dialog';
 -import { MoveCollectionDialog } from 'views-components/dialog-forms/move-collection-dialog';
 -import { FilesUploadCollectionDialog } from 'views-components/dialog-forms/files-upload-collection-dialog';
 -import { PartialCopyToNewCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-new-collection-dialog';
 -import { PartialCopyToExistingCollectionDialog } from 'views-components/dialog-forms/partial-copy-to-existing-collection-dialog';
 -import { PartialCopyToSeparateCollectionsDialog } from 'views-components/dialog-forms/partial-copy-to-separate-collections-dialog';
 -import { PartialMoveToNewCollectionDialog } from 'views-components/dialog-forms/partial-move-to-new-collection-dialog';
 -import { PartialMoveToExistingCollectionDialog } from 'views-components/dialog-forms/partial-move-to-existing-collection-dialog';
 -import { PartialMoveToSeparateCollectionsDialog } from 'views-components/dialog-forms/partial-move-to-separate-collections-dialog';
 -import { RemoveProcessDialog } from 'views-components/process-remove-dialog/process-remove-dialog';
 -import { MainContentBar } from 'views-components/main-content-bar/main-content-bar';
 -import { Grid } from '@material-ui/core';
 +import { TokenDialog } from "views-components/token-dialog/token-dialog";
 +import { RichTextEditorDialog } from "views-components/rich-text-editor-dialog/rich-text-editor-dialog";
 +import { Snackbar } from "views-components/snackbar/snackbar";
 +import { CollectionPanel } from "../collection-panel/collection-panel";
 +import { RenameFileDialog } from "views-components/rename-file-dialog/rename-file-dialog";
 +import { FileRemoveDialog } from "views-components/file-remove-dialog/file-remove-dialog";
 +import { MultipleFilesRemoveDialog } from "views-components/file-remove-dialog/multiple-files-remove-dialog";
 +import { Routes } from "routes/routes";
 +import { SidePanel } from "views-components/side-panel/side-panel";
 +import { ProcessPanel } from "views/process-panel/process-panel";
 +import { ChangeWorkflowDialog } from "views-components/run-process-dialog/change-workflow-dialog";
 +import { CreateProjectDialog } from "views-components/dialog-forms/create-project-dialog";
 +import { CreateCollectionDialog } from "views-components/dialog-forms/create-collection-dialog";
 +import { CopyCollectionDialog, CopyMultiCollectionDialog } from "views-components/dialog-forms/copy-collection-dialog";
 +import { CopyProcessDialog } from "views-components/dialog-forms/copy-process-dialog";
 +import { UpdateCollectionDialog } from "views-components/dialog-forms/update-collection-dialog";
 +import { UpdateProcessDialog } from "views-components/dialog-forms/update-process-dialog";
 +import { UpdateProjectDialog } from "views-components/dialog-forms/update-project-dialog";
 +import { MoveProcessDialog } from "views-components/dialog-forms/move-process-dialog";
 +import { MoveProjectDialog } from "views-components/dialog-forms/move-project-dialog";
 +import { MoveCollectionDialog } from "views-components/dialog-forms/move-collection-dialog";
 +import { FilesUploadCollectionDialog } from "views-components/dialog-forms/files-upload-collection-dialog";
- import { PartialCopyCollectionDialog } from "views-components/dialog-forms/partial-copy-collection-dialog";
++import { PartialCopyToNewCollectionDialog } from "views-components/dialog-forms/partial-copy-to-new-collection-dialog";
++import { PartialCopyToExistingCollectionDialog } from "views-components/dialog-forms/partial-copy-to-existing-collection-dialog";
++import { PartialCopyToSeparateCollectionsDialog } from "views-components/dialog-forms/partial-copy-to-separate-collections-dialog";
++import { PartialMoveToNewCollectionDialog } from "views-components/dialog-forms/partial-move-to-new-collection-dialog";
++import { PartialMoveToExistingCollectionDialog } from "views-components/dialog-forms/partial-move-to-existing-collection-dialog";
++import { PartialMoveToSeparateCollectionsDialog } from "views-components/dialog-forms/partial-move-to-separate-collections-dialog";
 +import { RemoveProcessDialog } from "views-components/process-remove-dialog/process-remove-dialog";
 +import { MainContentBar } from "views-components/main-content-bar/main-content-bar";
 +import { Grid } from "@material-ui/core";
  import { TrashPanel } from "views/trash-panel/trash-panel";
 -import { SharedWithMePanel } from 'views/shared-with-me-panel/shared-with-me-panel';
 -import { RunProcessPanel } from 'views/run-process-panel/run-process-panel';
 -import SplitterLayout from 'react-splitter-layout';
 -import { WorkflowPanel } from 'views/workflow-panel/workflow-panel';
 -import { RegisteredWorkflowPanel } from 'views/workflow-panel/registered-workflow-panel';
 -import { SearchResultsPanel } from 'views/search-results-panel/search-results-panel';
 -import { SshKeyPanel } from 'views/ssh-key-panel/ssh-key-panel';
 -import { SshKeyAdminPanel } from 'views/ssh-key-panel/ssh-key-admin-panel';
 +import { SharedWithMePanel } from "views/shared-with-me-panel/shared-with-me-panel";
 +import { RunProcessPanel } from "views/run-process-panel/run-process-panel";
 +import SplitterLayout from "react-splitter-layout";
 +import { WorkflowPanel } from "views/workflow-panel/workflow-panel";
 +import { RegisteredWorkflowPanel } from "views/workflow-panel/registered-workflow-panel";
 +import { SearchResultsPanel } from "views/search-results-panel/search-results-panel";
 +import { SshKeyPanel } from "views/ssh-key-panel/ssh-key-panel";
 +import { SshKeyAdminPanel } from "views/ssh-key-panel/ssh-key-admin-panel";
  import { SiteManagerPanel } from "views/site-manager-panel/site-manager-panel";
 -import { UserProfilePanel } from 'views/user-profile-panel/user-profile-panel';
 -import { SharingDialog } from 'views-components/sharing-dialog/sharing-dialog';
 -import { NotFoundDialog } from 'views-components/not-found-dialog/not-found-dialog';
 -import { AdvancedTabDialog } from 'views-components/advanced-tab-dialog/advanced-tab-dialog';
 -import { ProcessInputDialog } from 'views-components/process-input-dialog/process-input-dialog';
 -import { VirtualMachineUserPanel } from 'views/virtual-machine-panel/virtual-machine-user-panel';
 -import { VirtualMachineAdminPanel } from 'views/virtual-machine-panel/virtual-machine-admin-panel';
 -import { RepositoriesPanel } from 'views/repositories-panel/repositories-panel';
 -import { KeepServicePanel } from 'views/keep-service-panel/keep-service-panel';
 -import { ApiClientAuthorizationPanel } from 'views/api-client-authorization-panel/api-client-authorization-panel';
 -import { LinkPanel } from 'views/link-panel/link-panel';
 -import { RepositoriesSampleGitDialog } from 'views-components/repositories-sample-git-dialog/repositories-sample-git-dialog';
 -import { RepositoryAttributesDialog } from 'views-components/repository-attributes-dialog/repository-attributes-dialog';
 -import { CreateRepositoryDialog } from 'views-components/dialog-forms/create-repository-dialog';
 -import { RemoveRepositoryDialog } from 'views-components/repository-remove-dialog/repository-remove-dialog';
 -import { CreateSshKeyDialog } from 'views-components/dialog-forms/create-ssh-key-dialog';
 -import { PublicKeyDialog } from 'views-components/ssh-keys-dialog/public-key-dialog';
 -import { RemoveApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/remove-dialog';
 -import { RemoveKeepServiceDialog } from 'views-components/keep-services-dialog/remove-dialog';
 -import { RemoveLinkDialog } from 'views-components/links-dialog/remove-dialog';
 -import { RemoveSshKeyDialog } from 'views-components/ssh-keys-dialog/remove-dialog';
 -import { VirtualMachineAttributesDialog } from 'views-components/virtual-machines-dialog/attributes-dialog';
 -import { RemoveVirtualMachineDialog } from 'views-components/virtual-machines-dialog/remove-dialog';
 -import { RemoveVirtualMachineLoginDialog } from 'views-components/virtual-machines-dialog/remove-login-dialog';
 -import { VirtualMachineAddLoginDialog } from 'views-components/virtual-machines-dialog/add-login-dialog';
 -import { AttributesApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/attributes-dialog';
 -import { AttributesKeepServiceDialog } from 'views-components/keep-services-dialog/attributes-dialog';
 -import { AttributesLinkDialog } from 'views-components/links-dialog/attributes-dialog';
 -import { AttributesSshKeyDialog } from 'views-components/ssh-keys-dialog/attributes-dialog';
 -import { UserPanel } from 'views/user-panel/user-panel';
 -import { UserAttributesDialog } from 'views-components/user-dialog/attributes-dialog';
 -import { CreateUserDialog } from 'views-components/dialog-forms/create-user-dialog';
 -import { HelpApiClientAuthorizationDialog } from 'views-components/api-client-authorizations-dialog/help-dialog';
 -import { DeactivateDialog } from 'views-components/user-dialog/deactivate-dialog';
 -import { ActivateDialog } from 'views-components/user-dialog/activate-dialog';
 -import { SetupDialog } from 'views-components/user-dialog/setup-dialog';
 -import { GroupsPanel } from 'views/groups-panel/groups-panel';
 -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 { PublicFavoritePanel } from 'views/public-favorites-panel/public-favorites-panel';
 -import { LinkAccountPanel } from 'views/link-account-panel/link-account-panel';
 -import { FedLogin } from './fed-login';
 -import { CollectionsContentAddressPanel } from 'views/collection-content-address-panel/collection-content-address-panel';
 -import { AllProcessesPanel } from '../all-processes-panel/all-processes-panel';
 -import { NotFoundPanel } from '../not-found-panel/not-found-panel';
 -import { AutoLogout } from 'views-components/auto-logout/auto-logout';
 -import { RestoreCollectionVersionDialog } from 'views-components/collections-dialog/restore-version-dialog';
 -import { WebDavS3InfoDialog } from 'views-components/webdav-s3-dialog/webdav-s3-dialog';
 -import { pluginConfig } from 'plugins';
 -import { ElementListReducer } from 'common/plugintypes';
 -import { COLLAPSE_ICON_SIZE } from 'views-components/side-panel-toggle/side-panel-toggle'
 -import { Banner } from 'views-components/baner/banner';
 +import { UserProfilePanel } from "views/user-profile-panel/user-profile-panel";
 +import { SharingDialog } from "views-components/sharing-dialog/sharing-dialog";
 +import { NotFoundDialog } from "views-components/not-found-dialog/not-found-dialog";
 +import { AdvancedTabDialog } from "views-components/advanced-tab-dialog/advanced-tab-dialog";
 +import { ProcessInputDialog } from "views-components/process-input-dialog/process-input-dialog";
 +import { VirtualMachineUserPanel } from "views/virtual-machine-panel/virtual-machine-user-panel";
 +import { VirtualMachineAdminPanel } from "views/virtual-machine-panel/virtual-machine-admin-panel";
 +import { RepositoriesPanel } from "views/repositories-panel/repositories-panel";
 +import { KeepServicePanel } from "views/keep-service-panel/keep-service-panel";
 +import { ApiClientAuthorizationPanel } from "views/api-client-authorization-panel/api-client-authorization-panel";
 +import { LinkPanel } from "views/link-panel/link-panel";
 +import { RepositoriesSampleGitDialog } from "views-components/repositories-sample-git-dialog/repositories-sample-git-dialog";
 +import { RepositoryAttributesDialog } from "views-components/repository-attributes-dialog/repository-attributes-dialog";
 +import { CreateRepositoryDialog } from "views-components/dialog-forms/create-repository-dialog";
 +import { RemoveRepositoryDialog } from "views-components/repository-remove-dialog/repository-remove-dialog";
 +import { CreateSshKeyDialog } from "views-components/dialog-forms/create-ssh-key-dialog";
 +import { PublicKeyDialog } from "views-components/ssh-keys-dialog/public-key-dialog";
 +import { RemoveApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/remove-dialog";
 +import { RemoveKeepServiceDialog } from "views-components/keep-services-dialog/remove-dialog";
 +import { RemoveLinkDialog } from "views-components/links-dialog/remove-dialog";
 +import { RemoveSshKeyDialog } from "views-components/ssh-keys-dialog/remove-dialog";
 +import { VirtualMachineAttributesDialog } from "views-components/virtual-machines-dialog/attributes-dialog";
 +import { RemoveVirtualMachineDialog } from "views-components/virtual-machines-dialog/remove-dialog";
 +import { RemoveVirtualMachineLoginDialog } from "views-components/virtual-machines-dialog/remove-login-dialog";
 +import { VirtualMachineAddLoginDialog } from "views-components/virtual-machines-dialog/add-login-dialog";
 +import { AttributesApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/attributes-dialog";
 +import { AttributesKeepServiceDialog } from "views-components/keep-services-dialog/attributes-dialog";
 +import { AttributesLinkDialog } from "views-components/links-dialog/attributes-dialog";
 +import { AttributesSshKeyDialog } from "views-components/ssh-keys-dialog/attributes-dialog";
 +import { UserPanel } from "views/user-panel/user-panel";
 +import { UserAttributesDialog } from "views-components/user-dialog/attributes-dialog";
 +import { CreateUserDialog } from "views-components/dialog-forms/create-user-dialog";
 +import { HelpApiClientAuthorizationDialog } from "views-components/api-client-authorizations-dialog/help-dialog";
 +import { DeactivateDialog } from "views-components/user-dialog/deactivate-dialog";
 +import { ActivateDialog } from "views-components/user-dialog/activate-dialog";
 +import { SetupDialog } from "views-components/user-dialog/setup-dialog";
 +import { GroupsPanel } from "views/groups-panel/groups-panel";
 +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 { 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";
 +import { FedLogin } from "./fed-login";
 +import { CollectionsContentAddressPanel } from "views/collection-content-address-panel/collection-content-address-panel";
 +import { AllProcessesPanel } from "../all-processes-panel/all-processes-panel";
 +import { NotFoundPanel } from "../not-found-panel/not-found-panel";
 +import { AutoLogout } from "views-components/auto-logout/auto-logout";
 +import { RestoreCollectionVersionDialog } from "views-components/collections-dialog/restore-version-dialog";
 +import { WebDavS3InfoDialog } from "views-components/webdav-s3-dialog/webdav-s3-dialog";
 +import { pluginConfig } from "plugins";
 +import { ElementListReducer } from "common/plugintypes";
 +import { COLLAPSE_ICON_SIZE } from "views-components/side-panel-toggle/side-panel-toggle";
 +import { Banner } from "views-components/baner/banner";
  
 -type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 +type CssRules = "root" | "container" | "splitter" | "asidePanel" | "contentWrapper" | "content";
  
  const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
      root: {
@@@ -156,202 -160,81 +160,209 @@@ const getSplitterInitialSize = () => 
      return splitterSize ? Number(splitterSize) : defaultSplitterSize;
  };
  
 -const saveSplitterSize = (size: number) => localStorage.setItem('splitterSize', size.toString());
 +const saveSplitterSize = (size: number) => localStorage.setItem("splitterSize", size.toString());
  
 -let routes = <>
 -    <Route path={Routes.PROJECTS} component={ProjectPanel} />
 -    <Route path={Routes.COLLECTIONS} component={CollectionPanel} />
 -    <Route path={Routes.FAVORITES} component={FavoritePanel} />
 -    <Route path={Routes.ALL_PROCESSES} component={AllProcessesPanel} />
 -    <Route path={Routes.PROCESSES} component={ProcessPanel} />
 -    <Route path={Routes.TRASH} component={TrashPanel} />
 -    <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
 -    <Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
 -    <Route path={Routes.REGISTEREDWORKFLOW} component={RegisteredWorkflowPanel} />
 -    <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
 -    <Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
 -    <Route path={Routes.VIRTUAL_MACHINES_USER} component={VirtualMachineUserPanel} />
 -    <Route path={Routes.VIRTUAL_MACHINES_ADMIN} component={VirtualMachineAdminPanel} />
 -    <Route path={Routes.REPOSITORIES} component={RepositoriesPanel} />
 -    <Route path={Routes.SSH_KEYS_USER} component={SshKeyPanel} />
 -    <Route path={Routes.SSH_KEYS_ADMIN} component={SshKeyAdminPanel} />
 -    <Route path={Routes.SITE_MANAGER} component={SiteManagerPanel} />
 -    <Route path={Routes.KEEP_SERVICES} component={KeepServicePanel} />
 -    <Route path={Routes.USERS} component={UserPanel} />
 -    <Route path={Routes.API_CLIENT_AUTHORIZATIONS} component={ApiClientAuthorizationPanel} />
 -    <Route path={Routes.MY_ACCOUNT} component={UserProfilePanel} />
 -    <Route path={Routes.USER_PROFILE} component={UserProfilePanel} />
 -    <Route path={Routes.GROUPS} component={GroupsPanel} />
 -    <Route path={Routes.GROUP_DETAILS} component={GroupDetailsPanel} />
 -    <Route path={Routes.LINKS} component={LinkPanel} />
 -    <Route path={Routes.PUBLIC_FAVORITES} component={PublicFavoritePanel} />
 -    <Route path={Routes.LINK_ACCOUNT} component={LinkAccountPanel} />
 -    <Route path={Routes.COLLECTIONS_CONTENT_ADDRESS} component={CollectionsContentAddressPanel} />
 -</>;
 +let routes = (
 +    <>
 +        <Route
 +            path={Routes.PROJECTS}
 +            component={ProjectPanel}
 +        />
 +        <Route
 +            path={Routes.COLLECTIONS}
 +            component={CollectionPanel}
 +        />
 +        <Route
 +            path={Routes.FAVORITES}
 +            component={FavoritePanel}
 +        />
 +        <Route
 +            path={Routes.ALL_PROCESSES}
 +            component={AllProcessesPanel}
 +        />
 +        <Route
 +            path={Routes.PROCESSES}
 +            component={ProcessPanel}
 +        />
 +        <Route
 +            path={Routes.TRASH}
 +            component={TrashPanel}
 +        />
 +        <Route
 +            path={Routes.SHARED_WITH_ME}
 +            component={SharedWithMePanel}
 +        />
 +        <Route
 +            path={Routes.RUN_PROCESS}
 +            component={RunProcessPanel}
 +        />
 +        <Route
 +            path={Routes.REGISTEREDWORKFLOW}
 +            component={RegisteredWorkflowPanel}
 +        />
 +        <Route
 +            path={Routes.WORKFLOWS}
 +            component={WorkflowPanel}
 +        />
 +        <Route
 +            path={Routes.SEARCH_RESULTS}
 +            component={SearchResultsPanel}
 +        />
 +        <Route
 +            path={Routes.VIRTUAL_MACHINES_USER}
 +            component={VirtualMachineUserPanel}
 +        />
 +        <Route
 +            path={Routes.VIRTUAL_MACHINES_ADMIN}
 +            component={VirtualMachineAdminPanel}
 +        />
 +        <Route
 +            path={Routes.REPOSITORIES}
 +            component={RepositoriesPanel}
 +        />
 +        <Route
 +            path={Routes.SSH_KEYS_USER}
 +            component={SshKeyPanel}
 +        />
 +        <Route
 +            path={Routes.SSH_KEYS_ADMIN}
 +            component={SshKeyAdminPanel}
 +        />
 +        <Route
 +            path={Routes.SITE_MANAGER}
 +            component={SiteManagerPanel}
 +        />
 +        <Route
 +            path={Routes.KEEP_SERVICES}
 +            component={KeepServicePanel}
 +        />
 +        <Route
 +            path={Routes.USERS}
 +            component={UserPanel}
 +        />
 +        <Route
 +            path={Routes.API_CLIENT_AUTHORIZATIONS}
 +            component={ApiClientAuthorizationPanel}
 +        />
 +        <Route
 +            path={Routes.MY_ACCOUNT}
 +            component={UserProfilePanel}
 +        />
 +        <Route
 +            path={Routes.USER_PROFILE}
 +            component={UserProfilePanel}
 +        />
 +        <Route
 +            path={Routes.GROUPS}
 +            component={GroupsPanel}
 +        />
 +        <Route
 +            path={Routes.GROUP_DETAILS}
 +            component={GroupDetailsPanel}
 +        />
 +        <Route
 +            path={Routes.LINKS}
 +            component={LinkPanel}
 +        />
 +        <Route
 +            path={Routes.PUBLIC_FAVORITES}
 +            component={PublicFavoritePanel}
 +        />
 +        <Route
 +            path={Routes.LINK_ACCOUNT}
 +            component={LinkAccountPanel}
 +        />
 +        <Route
 +            path={Routes.COLLECTIONS_CONTENT_ADDRESS}
 +            component={CollectionsContentAddressPanel}
 +        />
 +    </>
 +);
  
 -const reduceRoutesFn: (a: React.ReactElement[],
 -    b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
 +const reduceRoutesFn: (a: React.ReactElement[], b: ElementListReducer) => React.ReactElement[] = (a, b) => b(a);
  
 -routes = React.createElement(React.Fragment, null, pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children)));
 +routes = React.createElement(
 +    React.Fragment,
 +    null,
 +    pluginConfig.centerPanelList.reduce(reduceRoutesFn, React.Children.toArray(routes.props.children))
 +);
  
 -const applyCollapsedState = (isCollapsed) => {
 -    const rightPanel: Element = document.getElementsByClassName('layout-pane')[1]
 -    const totalWidth: number = document.getElementsByClassName('splitter-layout')[0]?.clientWidth
 -    const rightPanelExpandedWidth = ((totalWidth - COLLAPSE_ICON_SIZE)) / (totalWidth / 100)
 +const applyCollapsedState = isCollapsed => {
 +    const rightPanel: Element = document.getElementsByClassName("layout-pane")[1];
 +    const totalWidth: number = document.getElementsByClassName("splitter-layout")[0]?.clientWidth;
 +    const rightPanelExpandedWidth = (totalWidth - COLLAPSE_ICON_SIZE) / (totalWidth / 100);
      if (rightPanel) {
 -        rightPanel.setAttribute('style', `width: ${isCollapsed ? rightPanelExpandedWidth : getSplitterInitialSize()}%`)
 +        rightPanel.setAttribute("style", `width: ${isCollapsed ? rightPanelExpandedWidth : getSplitterInitialSize()}%`);
      }
 -    const splitter = document.getElementsByClassName('layout-splitter')[0]
 -    isCollapsed ? splitter?.classList.add('layout-splitter-disabled') : splitter?.classList.remove('layout-splitter-disabled')
 -
 -}
 -
 -export const WorkbenchPanel =
 -    withStyles(styles)((props: WorkbenchPanelProps) => {
 +    const splitter = document.getElementsByClassName("layout-splitter")[0];
 +    isCollapsed ? splitter?.classList.add("layout-splitter-disabled") : splitter?.classList.remove("layout-splitter-disabled");
 +};
  
 -        //panel size will not scale automatically on window resize, so we do it manually
 -        window.addEventListener('resize', () => applyCollapsedState(props.sidePanelIsCollapsed))
 -        applyCollapsedState(props.sidePanelIsCollapsed)
 +export const WorkbenchPanel = withStyles(styles)((props: WorkbenchPanelProps) => {
 +    //panel size will not scale automatically on window resize, so we do it manually
 +    window.addEventListener("resize", () => applyCollapsedState(props.sidePanelIsCollapsed));
 +    applyCollapsedState(props.sidePanelIsCollapsed);
  
 -        return <Grid container item xs className={props.classes.root}>
 +    return (
 +        <Grid
 +            container
 +            item
 +            xs
-             className={props.classes.root}>
++            className={props.classes.root}
++        >
              {props.sessionIdleTimeout > 0 && <AutoLogout />}
 -            <Grid container item xs className={props.classes.container}>
 -                <SplitterLayout customClassName={props.classes.splitter} percentage={true}
 -                    primaryIndex={0} primaryMinSize={10}
 -                    secondaryInitialSize={getSplitterInitialSize()} secondaryMinSize={40}
 -                    onSecondaryPaneSizeChange={saveSplitterSize}>
 -                    {props.isUserActive && props.isNotLinking && <Grid container item xs component='aside' direction='column' className={props.classes.asidePanel}>
 -                        <SidePanel />
 -                    </Grid>}
 -                    <Grid container item xs component="main" direction="column" className={props.classes.contentWrapper}>
 -                        <Grid item xs>
 +            <Grid
 +                container
 +                item
 +                xs
-                 className={props.classes.container}>
++                className={props.classes.container}
++            >
 +                <SplitterLayout
 +                    customClassName={props.classes.splitter}
 +                    percentage={true}
 +                    primaryIndex={0}
 +                    primaryMinSize={10}
 +                    secondaryInitialSize={getSplitterInitialSize()}
 +                    secondaryMinSize={40}
-                     onSecondaryPaneSizeChange={saveSplitterSize}>
++                    onSecondaryPaneSizeChange={saveSplitterSize}
++                >
 +                    {props.isUserActive && props.isNotLinking && (
 +                        <Grid
 +                            container
 +                            item
 +                            xs
 +                            component="aside"
 +                            direction="column"
-                             className={props.classes.asidePanel}>
++                            className={props.classes.asidePanel}
++                        >
 +                            <SidePanel />
 +                        </Grid>
 +                    )}
 +                    <Grid
 +                        container
 +                        item
 +                        xs
 +                        component="main"
 +                        direction="column"
-                         className={props.classes.contentWrapper}>
++                        className={props.classes.contentWrapper}
++                    >
 +                        <Grid
 +                            item
-                             xs>
++                            xs
++                        >
                              {props.isNotLinking && <MainContentBar />}
                          </Grid>
 -                        <Grid item xs className={props.classes.content}>
 +                        <Grid
 +                            item
 +                            xs
-                             className={props.classes.content}>
++                            className={props.classes.content}
++                        >
                              <Switch>
                                  {routes.props.children}
 -                                <Route path={Routes.NO_MATCH} component={NotFoundPanel} />
 +                                <Route
 +                                    path={Routes.NO_MATCH}
 +                                    component={NotFoundPanel}
 +                                />
                              </Switch>
                          </Grid>
                      </Grid>