--- /dev/null
+# Copyright (C) The Arvados Authors. All rights reserved.
+#
+# SPDX-License-Identifier: AGPL-3.0
+
+---
+"$graph":
+- class: Workflow
+ cwlVersion: v1.2
+ hints:
+ - acrContainerImage: 7009415fdc959d0c2819ee2e9db96561+261
+ class: http://arvados.org/cwl#WorkflowRunnerResources
+ id: "#main"
+ inputs:
+ - id: "#main/directoryInputName"
+ type:
+ items: Directory
+ type: array
+ outputs: []
+ steps: []
+cwlVersion: v1.2
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Collection panel tests', function () {
+describe('Banner / tooltip tests', function () {
let activeUser;
let adminUser;
let collectionUUID;
//
// SPDX-License-Identifier: AGPL-3.0
-const path = require('path');
+const path = require("path");
-describe('Collection panel tests', function () {
+describe("Collection panel tests", function () {
let activeUser;
let adminUser;
let downloadsFolder;
// aliases are cleaned up after every test. Also it doesn't make sense
// to set the same users on beforeEach() over and over again, so we
// separate a little from Cypress' 'Best Practices' here.
- cy.getUser('admin', 'Admin', 'User', true, true)
- .as('adminUser').then(function () {
+ cy.getUser("admin", "Admin", "User", true, true)
+ .as("adminUser")
+ .then(function () {
adminUser = this.adminUser;
- }
- );
- cy.getUser('collectionuser1', 'Collection', 'User', false, true)
- .as('activeUser').then(function () {
+ });
+ cy.getUser("collectionuser1", "Collection", "User", false, true)
+ .as("activeUser")
+ .then(function () {
activeUser = this.activeUser;
- }
- );
- downloadsFolder = Cypress.config('downloadsFolder');
+ });
+ downloadsFolder = Cypress.config("downloadsFolder");
});
beforeEach(function () {
cy.clearLocalStorage();
});
- it('allows to download mountain duck config for a collection', () => {
+ it("allows to download mountain duck config for a collection", () => {
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 (testCollection) {
- cy.loginAs(activeUser);
- cy.goToPath(`/collections/${testCollection.uuid}`);
-
- cy.get('[data-cy=collection-panel-options-btn]').click();
- cy.get('[data-cy=context-menu]').contains('Open with 3rd party client').click();
- cy.get('[data-cy=download-button').click();
-
- const filename = path.join(downloadsFolder, `${testCollection.name}.duck`);
-
- cy.readFile(filename, { timeout: 15000 })
- .then((body) => {
- const childrenCollection = Array.prototype.slice.call(Cypress.$(body).find('dict')[0].children);
- const map = {};
- let i, j = 2;
-
- for (i=0; i < childrenCollection.length; i += j) {
- map[childrenCollection[i].outerText] = childrenCollection[i + 1].outerText;
- }
+ .as("testCollection")
+ .then(function (testCollection) {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/collections/${testCollection.uuid}`);
- cy.get('#simple-tabpanel-0').find('a')
- .then((a) => {
- const [host, port] = a.text().split('@')[1].split('/')[0].split(':');
- expect(map['Protocol']).to.equal('davs');
- expect(map['UUID']).to.equal(testCollection.uuid);
- expect(map['Username']).to.equal(activeUser.user.username);
- expect(map['Port']).to.equal(port);
- expect(map['Hostname']).to.equal(host);
- if (map['Path']) {
- expect(map['Path']).to.equal(`/c=${testCollection.uuid}`);
- }
- });
- })
- .then(() => cy.task('clearDownload', { filename }));
- });
+ cy.get("[data-cy=collection-panel-options-btn]").click();
+ cy.get("[data-cy=context-menu]").contains("Open with 3rd party client").click();
+ cy.get("[data-cy=download-button").click();
+
+ const filename = path.join(downloadsFolder, `${testCollection.name}.duck`);
+
+ cy.readFile(filename, { timeout: 15000 })
+ .then(body => {
+ const childrenCollection = Array.prototype.slice.call(Cypress.$(body).find("dict")[0].children);
+ const map = {};
+ let i,
+ j = 2;
+
+ for (i = 0; i < childrenCollection.length; i += j) {
+ map[childrenCollection[i].outerText] = childrenCollection[i + 1].outerText;
+ }
+
+ cy.get("#simple-tabpanel-0")
+ .find("a")
+ .then(a => {
+ const [host, port] = a.text().split("@")[1].split("/")[0].split(":");
+ expect(map["Protocol"]).to.equal("davs");
+ expect(map["UUID"]).to.equal(testCollection.uuid);
+ expect(map["Username"]).to.equal(activeUser.user.username);
+ expect(map["Port"]).to.equal(port);
+ expect(map["Hostname"]).to.equal(host);
+ if (map["Path"]) {
+ expect(map["Path"]).to.equal(`/c=${testCollection.uuid}`);
+ }
+ });
+ })
+ .then(() => cy.task("clearDownload", { filename }));
+ });
});
- it('attempts to use a preexisting name creating or updating a collection', function() {
+ it("attempts to use a preexisting name creating or updating a collection", function () {
const name = `Test collection ${Math.floor(Math.random() * 999999)}`;
cy.createCollection(adminUser.token, {
name: name,
owner_uuid: activeUser.user.uuid,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
});
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");
// Attempt to create new collection with a duplicate name
- cy.get('[data-cy=side-panel-button]').click();
- cy.get('[data-cy=side-panel-new-collection]').click();
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'New collection')
+ cy.get("[data-cy=side-panel-button]").click();
+ cy.get("[data-cy=side-panel-new-collection]").click();
+ cy.get("[data-cy=form-dialog]")
+ .should("contain", "New collection")
.within(() => {
- cy.get('[data-cy=name-field]').within(() => {
- cy.get('input').type(name);
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type(name);
});
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[data-cy=form-submit-btn]").click();
});
// Error message should display, allowing editing the name
- cy.get('[data-cy=form-dialog]').should('exist')
- .and('contain', 'Collection with the same name already exists')
+ cy.get("[data-cy=form-dialog]")
+ .should("exist")
+ .and("contain", "Collection with the same name already exists")
.within(() => {
- cy.get('[data-cy=name-field]').within(() => {
- cy.get('input').type(' renamed');
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type(" renamed");
});
- 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=form-dialog]").should("not.exist");
// Attempt to rename the collection with the duplicate name
- 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(() => {
- cy.get('[data-cy=name-field]').within(() => {
- cy.get('input')
- .type('{selectall}{backspace}')
- .type(name);
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type("{selectall}{backspace}").type(name);
});
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[data-cy=form-submit-btn]").click();
});
- cy.get('[data-cy=form-dialog]').should('exist')
- .and('contain', 'Collection with the same name already exists');
+ cy.get("[data-cy=form-dialog]").should("exist").and("contain", "Collection with the same name already exists");
});
- it('uses the property editor (from edit dialog) with vocabulary terms', function () {
+ it("uses the property editor (from edit dialog) with vocabulary terms", function () {
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}`);
- cy.get('[data-cy=collection-info-panel')
- .should('contain', this.testCollection.name)
- .and('not.contain', 'Color: Magenta');
+ cy.get("[data-cy=collection-info-panel").should("contain", this.testCollection.name).and("not.contain", "Color: Magenta");
- 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', 'Properties');
+ 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", "Properties");
// Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
- cy.get('[data-cy=resource-properties-form]').within(() => {
- cy.get('[data-cy=property-field-key]').within(() => {
- cy.get('input').type('Color');
+ cy.get("[data-cy=resource-properties-form]").within(() => {
+ cy.get("[data-cy=property-field-key]").within(() => {
+ cy.get("input").type("Color");
});
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('Magenta');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("Magenta");
});
cy.root().submit();
});
// Confirm proper vocabulary labels are displayed on the UI.
- cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
- cy.get('[data-cy=form-dialog]').contains('Save').click();
- cy.get('[data-cy=form-dialog]').should('not.exist');
+ cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
+ cy.get("[data-cy=form-dialog]").contains("Save").click();
+ cy.get("[data-cy=form-dialog]").should("not.exist");
// Confirm proper vocabulary IDs were saved on the backend.
- cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
- .its('body').as('collection')
+ cy.doRequest("GET", `/arvados/v1/collections/${this.testCollection.uuid}`)
+ .its("body")
+ .as("collection")
.then(function () {
- expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3');
+ expect(this.collection.properties.IDTAGCOLORS).to.equal("IDVALCOLORS3");
});
// Confirm the property is displayed on the UI.
- cy.get('[data-cy=collection-info-panel')
- .should('contain', this.testCollection.name)
- .and('contain', 'Color: Magenta');
+ cy.get("[data-cy=collection-info-panel").should("contain", this.testCollection.name).and("contain", "Color: Magenta");
});
});
- it('uses the editor (from details panel) with vocabulary terms', function () {
+ it("uses the editor (from details panel) with vocabulary terms", function () {
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}`);
- cy.get('[data-cy=collection-info-panel')
- .should('contain', this.testCollection.name)
- .and('not.contain', 'Color: Magenta')
- .and('not.contain', 'Size: S');
- cy.get('[data-cy=additional-info-icon]').click();
+ cy.get("[data-cy=collection-info-panel")
+ .should("contain", this.testCollection.name)
+ .and("not.contain", "Color: Magenta")
+ .and("not.contain", "Size: S");
+ cy.get("[data-cy=additional-info-icon]").click();
- cy.get('[data-cy=details-panel]').within(() => {
- cy.get('[data-cy=details-panel-edit-btn]').click();
+ cy.get("[data-cy=details-panel]").within(() => {
+ cy.get("[data-cy=details-panel-edit-btn]").click();
});
- cy.get('[data-cy=form-dialog').contains('Edit Collection');
+ cy.get("[data-cy=form-dialog").contains("Edit Collection");
// Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
- cy.get('[data-cy=resource-properties-form]').within(() => {
- cy.get('[data-cy=property-field-key]').within(() => {
- cy.get('input').type('Color');
+ cy.get("[data-cy=resource-properties-form]").within(() => {
+ cy.get("[data-cy=property-field-key]").within(() => {
+ cy.get("input").type("Color");
});
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('Magenta');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("Magenta");
});
cy.root().submit();
});
// Confirm proper vocabulary labels are displayed on the UI.
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'Color: Magenta');
+ cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
// Case-insensitive on-blur auto-selection test
// Key: Size (IDTAGSIZES) - Value: Small (IDVALSIZES2)
- cy.get('[data-cy=resource-properties-form]').within(() => {
- cy.get('[data-cy=property-field-key]').within(() => {
- cy.get('input').type('sIzE');
+ cy.get("[data-cy=resource-properties-form]").within(() => {
+ cy.get("[data-cy=property-field-key]").within(() => {
+ cy.get("input").type("sIzE");
});
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('sMaLL');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("sMaLL");
});
// Cannot "type()" TAB on Cypress so let's click another field
// to trigger the onBlur event.
- cy.get('[data-cy=property-field-key]').click();
+ cy.get("[data-cy=property-field-key]").click();
cy.root().submit();
});
// Confirm proper vocabulary labels are displayed on the UI.
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'Size: S');
+ cy.get("[data-cy=form-dialog]").should("contain", "Size: S");
- cy.get('[data-cy=form-dialog]').contains('Save').click();
- cy.get('[data-cy=form-dialog]').should('not.exist');
+ cy.get("[data-cy=form-dialog]").contains("Save").click();
+ cy.get("[data-cy=form-dialog]").should("not.exist");
// Confirm proper vocabulary IDs were saved on the backend.
- cy.doRequest('GET', `/arvados/v1/collections/${this.testCollection.uuid}`)
- .its('body').as('collection')
+ cy.doRequest("GET", `/arvados/v1/collections/${this.testCollection.uuid}`)
+ .its("body")
+ .as("collection")
.then(function () {
- expect(this.collection.properties.IDTAGCOLORS).to.equal('IDVALCOLORS3');
- expect(this.collection.properties.IDTAGSIZES).to.equal('IDVALSIZES2');
+ expect(this.collection.properties.IDTAGCOLORS).to.equal("IDVALCOLORS3");
+ expect(this.collection.properties.IDTAGSIZES).to.equal("IDVALSIZES2");
});
// Confirm properties display on the UI.
- cy.get('[data-cy=collection-info-panel')
- .should('contain', this.testCollection.name)
- .and('contain', 'Color: Magenta')
- .and('contain', 'Size: S');
+ cy.get("[data-cy=collection-info-panel")
+ .should("contain", this.testCollection.name)
+ .and("contain", "Color: Magenta")
+ .and("contain", "Size: S");
});
});
- it('shows collection by URL', function () {
+ it("shows collection by URL", function () {
cy.loginAs(activeUser);
[true, false].map(function (isWritable) {
// Using different file names to avoid test flakyness: the second iteration
// on this loop may pass an assertion from the first iteration by looking
// for the same file name.
- const fileName = isWritable ? 'bar' : 'foo';
- const subDirName = 'subdir';
+ const fileName = isWritable ? "bar" : "foo";
+ const subDirName = "subdir";
cy.createGroup(adminUser.token, {
- name: 'Shared project',
- group_class: 'project',
- }).as('sharedGroup').then(function () {
- // Creates the collection using the admin token so we can set up
- // a bogus manifest text without block signatures.
- cy.doRequest('GET', '/arvados/v1/config', null, null)
- .its('body').should((clusterConfig) => {
- expect(clusterConfig.Collections, "clusterConfig").to.have.property("TrustAllContent", true);
- expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAV").have.property("ExternalURL");
- expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAVDownload").have.property("ExternalURL");
- const inlineUrl = clusterConfig.Services.WebDAV.ExternalURL !== ""
- ? clusterConfig.Services.WebDAV.ExternalURL
- : clusterConfig.Services.WebDAVDownload.ExternalURL;
- expect(inlineUrl).to.not.contain("*");
- })
- .createCollection(adminUser.token, {
- name: 'Test collection',
- owner_uuid: this.sharedGroup.uuid,
- properties: { someKey: 'someValue' },
- manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`
- })
- .as('testCollection').then(function () {
- // Share the group with active user.
- cy.createLink(adminUser.token, {
- name: isWritable ? 'can_write' : 'can_read',
- link_class: 'permission',
- head_uuid: this.sharedGroup.uuid,
- tail_uuid: activeUser.user.uuid
+ name: "Shared project",
+ group_class: "project",
+ })
+ .as("sharedGroup")
+ .then(function () {
+ // Creates the collection using the admin token so we can set up
+ // a bogus manifest text without block signatures.
+ cy.doRequest("GET", "/arvados/v1/config", null, null)
+ .its("body")
+ .should(clusterConfig => {
+ expect(clusterConfig.Collections, "clusterConfig").to.have.property("TrustAllContent", true);
+ expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAV").have.property("ExternalURL");
+ expect(clusterConfig.Services, "clusterConfig").to.have.property("WebDAVDownload").have.property("ExternalURL");
+ const inlineUrl =
+ clusterConfig.Services.WebDAV.ExternalURL !== ""
+ ? clusterConfig.Services.WebDAV.ExternalURL
+ : clusterConfig.Services.WebDAVDownload.ExternalURL;
+ expect(inlineUrl).to.not.contain("*");
+ })
+ .createCollection(adminUser.token, {
+ name: "Test collection",
+ owner_uuid: this.sharedGroup.uuid,
+ properties: { someKey: "someValue" },
+ manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n./${subDirName} 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
})
- cy.goToPath(`/collections/${this.testCollection.uuid}`);
-
- // Check that name & uuid are correct.
- cy.get('[data-cy=collection-info-panel]')
- .should('contain', this.testCollection.name)
- .and('contain', this.testCollection.uuid)
- .and('not.contain', 'This is an old version');
- // Check for the read-only icon
- cy.get('[data-cy=read-only-icon]').should(`${isWritable ? 'not.' : ''}exist`);
- // Check that both read and write operations are available on
- // the 'More options' menu.
- cy.get('[data-cy=collection-panel-options-btn]')
- .click()
- cy.get('[data-cy=context-menu]')
- .should('contain', 'Add to favorites')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection');
- cy.get('body').click(); // Collapse the menu avoiding details panel expansion
- cy.get('[data-cy=collection-info-panel]')
- .should('contain', 'someKey: someValue')
- .and('not.contain', 'anotherKey: anotherValue');
- // Check that the file listing show both read & write operations
- cy.waitForDom().get('[data-cy=collection-files-panel]').within(() => {
- cy.get('[data-cy=collection-files-right-panel]', { timeout: 5000 })
- .should('contain', fileName);
- if (isWritable) {
- cy.get('[data-cy=upload-button]')
- .should(`${isWritable ? '' : 'not.'}contain`, 'Upload data');
- }
+ .as("testCollection")
+ .then(function () {
+ // Share the group with active user.
+ cy.createLink(adminUser.token, {
+ name: isWritable ? "can_write" : "can_read",
+ link_class: "permission",
+ head_uuid: this.sharedGroup.uuid,
+ tail_uuid: activeUser.user.uuid,
+ });
+ cy.goToPath(`/collections/${this.testCollection.uuid}`);
+
+ // Check that name & uuid are correct.
+ cy.get("[data-cy=collection-info-panel]")
+ .should("contain", this.testCollection.name)
+ .and("contain", this.testCollection.uuid)
+ .and("not.contain", "This is an old version");
+ // Check for the read-only icon
+ cy.get("[data-cy=read-only-icon]").should(`${isWritable ? "not." : ""}exist`);
+ // Check that both read and write operations are available on
+ // the 'More options' menu.
+ cy.get("[data-cy=collection-panel-options-btn]").click();
+ cy.get("[data-cy=context-menu]")
+ .should("contain", "Add to favorites")
+ .and(`${isWritable ? "" : "not."}contain`, "Edit collection");
+ cy.get("body").click(); // Collapse the menu avoiding details panel expansion
+ cy.get("[data-cy=collection-info-panel]")
+ .should("contain", "someKey: someValue")
+ .and("not.contain", "anotherKey: anotherValue");
+ // Check that the file listing show both read & write operations
+ cy.waitForDom()
+ .get("[data-cy=collection-files-panel]")
+ .within(() => {
+ cy.get("[data-cy=collection-files-right-panel]", { timeout: 5000 }).should("contain", fileName);
+ if (isWritable) {
+ cy.get("[data-cy=upload-button]").should(`${isWritable ? "" : "not."}contain`, "Upload data");
+ }
+ });
+ // Test context menus
+ cy.get("[data-cy=collection-files-panel]").contains(fileName).rightclick();
+ cy.get("[data-cy=context-menu]")
+ .should("contain", "Download")
+ .and("contain", "Open in new tab")
+ .and("contain", "Copy to clipboard")
+ .and(`${isWritable ? "" : "not."}contain`, "Rename")
+ .and(`${isWritable ? "" : "not."}contain`, "Remove");
+ cy.get("body").click(); // Collapse the menu
+ cy.get("[data-cy=collection-files-panel]").contains(subDirName).rightclick();
+ cy.get("[data-cy=context-menu]")
+ .should("not.contain", "Download")
+ .and("contain", "Open in new tab")
+ .and("contain", "Copy to clipboard")
+ .and(`${isWritable ? "" : "not."}contain`, "Rename")
+ .and(`${isWritable ? "" : "not."}contain`, "Remove");
+ cy.get("body").click(); // Collapse the menu
+ // File/dir item 'more options' button
+ cy.get("[data-cy=file-item-options-btn").first().click();
+ cy.get("[data-cy=context-menu]").should(`${isWritable ? "" : "not."}contain`, "Remove");
+ cy.get("body").click(); // Collapse the menu
+ // Hamburger 'more options' menu button
+ cy.get("[data-cy=collection-files-panel-options-btn]").click();
+ cy.get("[data-cy=context-menu]").should("contain", "Select all").click();
+ cy.get("[data-cy=collection-files-panel-options-btn]").click();
+ cy.get("[data-cy=context-menu]").should(`${isWritable ? "" : "not."}contain`, "Remove selected");
+ cy.get("body").click(); // Collapse the menu
});
- // Test context menus
- cy.get('[data-cy=collection-files-panel]')
- .contains(fileName).rightclick();
- cy.get('[data-cy=context-menu]')
- .should('contain', 'Download')
- .and('contain', 'Open in new tab')
- .and('contain', 'Copy to clipboard')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
- cy.get('body').click(); // Collapse the menu
- cy.get('[data-cy=collection-files-panel]')
- .contains(subDirName).rightclick();
- cy.get('[data-cy=context-menu]')
- .should('not.contain', 'Download')
- .and('contain', 'Open in new tab')
- .and('contain', 'Copy to clipboard')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
- cy.get('body').click(); // Collapse the menu
- // File/dir item 'more options' button
- cy.get('[data-cy=file-item-options-btn')
- .first()
- .click()
- cy.get('[data-cy=context-menu]')
- .should(`${isWritable ? '' : 'not.'}contain`, 'Remove');
- cy.get('body').click(); // Collapse the menu
- // Hamburger 'more options' menu button
- cy.get('[data-cy=collection-files-panel-options-btn]')
- .click()
- cy.get('[data-cy=context-menu]')
- .should('contain', 'Select all')
- .click()
- cy.get('[data-cy=collection-files-panel-options-btn]')
- .click()
- cy.get('[data-cy=context-menu]')
- .should(`${isWritable ? '' : 'not.'}contain`, 'Remove selected')
- cy.get('body').click(); // Collapse the menu
- })
- })
- })
- })
+ });
+ });
+ });
- it('renames a file using valid names', function () {
- function eachPair(lst, func){
- for(var i=0; i < lst.length - 1; i++){
- func(lst[i], lst[i + 1])
+ it("renames a file using valid names", function () {
+ function eachPair(lst, func) {
+ for (var i = 0; i < lst.length - 1; i++) {
+ func(lst[i], lst[i + 1]);
}
}
// Creates the collection using the admin token so we can set up
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}`);
const names = [
- 'bar', // initial name already set
- '&',
- 'foo',
- '&',
- '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",
+ "&",
+ "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("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}`);
- })
+ 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 () {
+ 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,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
})
- .as('testCollection').then((testCollection) => {
+ .as("testCollection")
+ .then(testCollection => {
cy.loginAs(activeUser);
cy.goToPath(`/collections/${testCollection.uuid}`);
cy.wait(5000);
- cy.get('[data-cy=collection-info-panel]').contains(`Collection User`);
+ cy.get("[data-cy=collection-info-panel]").contains(`Collection User`);
});
});
- it('tries to rename a file with illegal names', function () {
+ it("tries to rename a file with illegal names", 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}`);
const illegalNamesFromUI = [
- ['.', "Name cannot be '.' or '..'"],
- ['..', "Name cannot be '.' or '..'"],
- ['', 'This field is required'],
- [' ', 'Leading/trailing whitespaces not allowed'],
- [' foo', 'Leading/trailing whitespaces not allowed'],
- ['foo ', 'Leading/trailing whitespaces not allowed'],
- ['//foo', 'Empty dir name not allowed']
- ]
+ [".", "Name cannot be '.' or '..'"],
+ ["..", "Name cannot be '.' or '..'"],
+ ["", "This field is required"],
+ [" ", "Leading/trailing whitespaces not allowed"],
+ [" foo", "Leading/trailing whitespaces not allowed"],
+ ["foo ", "Leading/trailing whitespaces not allowed"],
+ ["//foo", "Empty dir name not allowed"],
+ ];
illegalNamesFromUI.forEach(([name, errMsg]) => {
- cy.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')
+ cy.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}${name}`);
+ cy.get("input").type(`{selectall}{backspace}${name}`);
});
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'Rename')
+ cy.get("[data-cy=form-dialog]")
+ .should("contain", "Rename")
.within(() => {
cy.contains(`${errMsg}`);
});
- cy.get('[data-cy=form-cancel-btn]').click();
- })
+ cy.get("[data-cy=form-cancel-btn]").click();
+ });
});
});
- it('can correctly display old versions', function () {
+ it("can correctly display old versions", function () {
const colName = `Versioned Collection ${Math.floor(Math.random() * 999999)}`;
- let colUuid = '';
- let oldVersionUuid = '';
+ let colUuid = "";
+ let oldVersionUuid = "";
// Make sure no other collections with this name exist
- cy.doRequest('GET', '/arvados/v1/collections', null, {
+ cy.doRequest("GET", "/arvados/v1/collections", null, {
filters: `[["name", "=", "${colName}"]]`,
- include_old_versions: true
+ include_old_versions: true,
})
- .its('body.items').as('collections')
+ .its("body.items")
+ .as("collections")
.then(function () {
expect(this.collections).to.be.empty;
});
name: colName,
owner_uuid: activeUser.user.uuid,
preserve_version: true,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
})
- .as('originalVersion').then(function () {
+ .as("originalVersion")
+ .then(function () {
// Change the file name to create a new version.
cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
- })
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n",
+ });
colUuid = this.originalVersion.uuid;
});
// Confirm that there are 2 versions of the collection
- cy.doRequest('GET', '/arvados/v1/collections', null, {
+ cy.doRequest("GET", "/arvados/v1/collections", null, {
filters: `[["name", "=", "${colName}"]]`,
- include_old_versions: true
+ include_old_versions: true,
})
- .its('body.items').as('collections')
+ .its("body.items")
+ .as("collections")
.then(function () {
expect(this.collections).to.have.lengthOf(2);
this.collections.map(function (aCollection) {
}
});
// Check the old version displays as what it is.
- cy.loginAs(activeUser)
+ cy.loginAs(activeUser);
cy.goToPath(`/collections/${oldVersionUuid}`);
- 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-info-panel]').should('contain', colName);
- cy.get('[data-cy=collection-files-panel]').should('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-info-panel]").should("contain", colName);
+ cy.get("[data-cy=collection-files-panel]").should("contain", "bar");
});
});
- it('views & edits storage classes data', function () {
- const colName= `Test Collection ${Math.floor(Math.random() * 999999)}`;
+ it("views & edits storage classes data", function () {
+ const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
cy.createCollection(adminUser.token, {
name: colName,
owner_uuid: activeUser.user.uuid,
manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
- }).as('collection').then(function () {
- expect(this.collection.storage_classes_desired).to.deep.equal(['default'])
-
- cy.loginAs(activeUser)
- cy.goToPath(`/collections/${this.collection.uuid}`);
-
- // Initial check: it should show the 'default' storage class
- cy.get('[data-cy=collection-info-panel]')
- .should('contain', 'Storage classes')
- .and('contain', 'default')
- .and('not.contain', 'foo')
- .and('not.contain', 'bar');
- // Edit collection: add storage class 'foo'
- 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')
- .and('contain', 'Storage classes')
- .and('contain', 'default')
- .and('contain', 'foo')
- .and('contain', 'bar')
- .within(() => {
- cy.get('[data-cy=checkbox-foo]').click();
- });
- cy.get('[data-cy=form-submit-btn]').click();
- cy.get('[data-cy=collection-info-panel]')
- .should('contain', 'default')
- .and('contain', 'foo')
- .and('not.contain', 'bar');
- cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
- .its('body').as('updatedCollection')
- .then(function () {
- expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['default', 'foo']);
- });
- // Edit collection: remove storage class 'default'
- 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')
- .and('contain', 'Storage classes')
- .and('contain', 'default')
- .and('contain', 'foo')
- .and('contain', 'bar')
- .within(() => {
- cy.get('[data-cy=checkbox-default]').click();
- });
- cy.get('[data-cy=form-submit-btn]').click();
- cy.get('[data-cy=collection-info-panel]')
- .should('not.contain', 'default')
- .and('contain', 'foo')
- .and('not.contain', 'bar');
- cy.doRequest('GET', `/arvados/v1/collections/${this.collection.uuid}`)
- .its('body').as('updatedCollection')
- .then(function () {
- expect(this.updatedCollection.storage_classes_desired).to.deep.equal(['foo']);
- });
})
+ .as("collection")
+ .then(function () {
+ expect(this.collection.storage_classes_desired).to.deep.equal(["default"]);
+
+ cy.loginAs(activeUser);
+ cy.goToPath(`/collections/${this.collection.uuid}`);
+
+ // Initial check: it should show the 'default' storage class
+ cy.get("[data-cy=collection-info-panel]")
+ .should("contain", "Storage classes")
+ .and("contain", "default")
+ .and("not.contain", "foo")
+ .and("not.contain", "bar");
+ // Edit collection: add storage class 'foo'
+ 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")
+ .and("contain", "Storage classes")
+ .and("contain", "default")
+ .and("contain", "foo")
+ .and("contain", "bar")
+ .within(() => {
+ cy.get("[data-cy=checkbox-foo]").click();
+ });
+ cy.get("[data-cy=form-submit-btn]").click();
+ cy.get("[data-cy=collection-info-panel]").should("contain", "default").and("contain", "foo").and("not.contain", "bar");
+ cy.doRequest("GET", `/arvados/v1/collections/${this.collection.uuid}`)
+ .its("body")
+ .as("updatedCollection")
+ .then(function () {
+ expect(this.updatedCollection.storage_classes_desired).to.deep.equal(["default", "foo"]);
+ });
+ // Edit collection: remove storage class 'default'
+ 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")
+ .and("contain", "Storage classes")
+ .and("contain", "default")
+ .and("contain", "foo")
+ .and("contain", "bar")
+ .within(() => {
+ cy.get("[data-cy=checkbox-default]").click();
+ });
+ cy.get("[data-cy=form-submit-btn]").click();
+ cy.get("[data-cy=collection-info-panel]").should("not.contain", "default").and("contain", "foo").and("not.contain", "bar");
+ cy.doRequest("GET", `/arvados/v1/collections/${this.collection.uuid}`)
+ .its("body")
+ .as("updatedCollection")
+ .then(function () {
+ expect(this.updatedCollection.storage_classes_desired).to.deep.equal(["foo"]);
+ });
+ });
});
- it('moves a collection to a different project', function () {
+ it("moves a collection to a different project", function () {
const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
const projName = `Test Project ${Math.floor(Math.random() * 999999)}`;
const fileName = `Test_File_${Math.floor(Math.random() * 999999)}`;
name: collName,
owner_uuid: activeUser.user.uuid,
manifest_text: `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n`,
- }).as('testCollection');
+ }).as("testCollection");
cy.createGroup(adminUser.token, {
name: projName,
- group_class: 'project',
+ group_class: "project",
owner_uuid: activeUser.user.uuid,
- }).as('testProject');
+ }).as("testProject");
- cy.getAll('@testCollection', '@testProject')
- .then(function ([testCollection, testProject]) {
- cy.loginAs(activeUser);
- cy.goToPath(`/collections/${testCollection.uuid}`);
- cy.get('[data-cy=collection-files-panel]').should('contain', fileName);
- cy.get('[data-cy=collection-info-panel]')
- .should('not.contain', projName)
- .and('not.contain', testProject.uuid);
- cy.get('[data-cy=collection-panel-options-btn]').click();
- cy.get('[data-cy=context-menu]').contains('Move to').click();
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'Move to')
- .within(() => {
- // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
- cy.get('[data-cy=projects-tree-home-tree-picker]')
- .find('i')
- .then(el => el.click());
- cy.get('[data-cy=projects-tree-home-tree-picker]')
- .contains(projName)
- .click();
- });
- cy.get('[data-cy=form-submit-btn]').click();
- cy.get('[data-cy=snackbar]')
- .contains('Collection has been moved')
- cy.get('[data-cy=collection-info-panel]')
- .contains(projName).and('contain', testProject.uuid);
- // Double check that the collection is in the project
- cy.goToPath(`/projects/${testProject.uuid}`);
- cy.waitForDom().get('[data-cy=project-panel]').should('contain', collName);
- });
+ cy.getAll("@testCollection", "@testProject").then(function ([testCollection, testProject]) {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/collections/${testCollection.uuid}`);
+ cy.get("[data-cy=collection-files-panel]").should("contain", fileName);
+ cy.get("[data-cy=collection-info-panel]").should("not.contain", projName).and("not.contain", testProject.uuid);
+ cy.get("[data-cy=collection-panel-options-btn]").click();
+ cy.get("[data-cy=context-menu]").contains("Move to").click();
+ cy.get("[data-cy=form-dialog]")
+ .should("contain", "Move to")
+ .within(() => {
+ // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+ cy.get("[data-cy=projects-tree-home-tree-picker]")
+ .find("i")
+ .then(el => el.click());
+ cy.get("[data-cy=projects-tree-home-tree-picker]").contains(projName).click();
+ });
+ cy.get("[data-cy=form-submit-btn]").click();
+ cy.get("[data-cy=snackbar]").contains("Collection has been moved");
+ cy.get("[data-cy=collection-info-panel]").contains(projName).and("contain", testProject.uuid);
+ // Double check that the collection is in the project
+ cy.goToPath(`/projects/${testProject.uuid}`);
+ cy.waitForDom().get("[data-cy=project-panel]").should("contain", collName);
+ });
});
- it('automatically updates the collection UI contents without using the Refresh button', function () {
+ it("automatically updates the collection UI contents without using the Refresh button", function () {
const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
cy.createCollection(adminUser.token, {
name: collName,
owner_uuid: activeUser.user.uuid,
- }).as('testCollection');
+ }).as("testCollection");
- cy.getAll('@testCollection').then(function ([testCollection]) {
+ cy.getAll("@testCollection").then(function ([testCollection]) {
cy.loginAs(activeUser);
- const files = [
- "foobar",
- "anotherFile",
- "",
- "finalName",
- ];
+ const files = ["foobar", "anotherFile", "", "finalName"];
cy.goToPath(`/collections/${testCollection.uuid}`);
- cy.get('[data-cy=collection-files-panel]').should('contain', 'This collection is empty');
- cy.get('[data-cy=collection-files-panel]').should('not.contain', files[0]);
- cy.get('[data-cy=collection-info-panel]').should('contain', collName);
+ cy.get("[data-cy=collection-files-panel]").should("contain", "This collection is empty");
+ cy.get("[data-cy=collection-files-panel]").should("not.contain", files[0]);
+ cy.get("[data-cy=collection-info-panel]").should("contain", collName);
files.map((fileName, i, files) => {
cy.updateCollection(adminUser.token, testCollection.uuid, {
- name: `${collName + ' updated'}`,
+ name: `${collName + " updated"}`,
manifest_text: fileName ? `. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:${fileName}\n` : "",
- }).as('updatedCollection');
- cy.getAll('@updatedCollection').then(function ([updatedCollection]) {
- expect(updatedCollection.name).to.equal(`${collName + ' updated'}`);
- cy.get('[data-cy=collection-info-panel]').should('contain', updatedCollection.name);
+ }).as("updatedCollection");
+ cy.getAll("@updatedCollection").then(function ([updatedCollection]) {
+ expect(updatedCollection.name).to.equal(`${collName + " updated"}`);
+ cy.get("[data-cy=collection-info-panel]").should("contain", updatedCollection.name);
fileName
- ? cy.get('[data-cy=collection-files-panel]').should('contain', fileName)
- : cy.get('[data-cy=collection-files-panel]').should('not.contain', files[i-1]);;
+ ? cy.get("[data-cy=collection-files-panel]").should("contain", fileName)
+ : cy.get("[data-cy=collection-files-panel]").should("not.contain", files[i - 1]);
});
});
-
});
});
- it('makes a copy of an existing collection', function() {
+ it("makes a copy of an existing collection", function () {
const collName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
const copyName = `Copy of: ${collName}`;
name: collName,
owner_uuid: activeUser.user.uuid,
manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:some-file\n",
- }).as('collection').then(function () {
- cy.loginAs(activeUser)
- cy.goToPath(`/collections/${this.collection.uuid}`);
- cy.get('[data-cy=collection-files-panel]')
- .should('contain', 'some-file');
- cy.get('[data-cy=collection-panel-options-btn]').click();
- cy.get('[data-cy=context-menu]').contains('Make a copy').click();
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'Make a copy')
- .within(() => {
- cy.get('[data-cy=projects-tree-home-tree-picker]')
- .contains('Projects')
- .click();
- cy.get('[data-cy=form-submit-btn]').click();
- });
- cy.get('[data-cy=snackbar]')
- .contains('Collection has been copied.')
- cy.get('[data-cy=snackbar-goto-action]').click();
- cy.get('[data-cy=project-panel]')
- .contains(copyName).click();
- cy.get('[data-cy=collection-files-panel]')
- .should('contain', 'some-file');
- });
+ })
+ .as("collection")
+ .then(function () {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/collections/${this.collection.uuid}`);
+ cy.get("[data-cy=collection-files-panel]").should("contain", "some-file");
+ cy.get("[data-cy=collection-panel-options-btn]").click();
+ cy.get("[data-cy=context-menu]").contains("Make a copy").click();
+ cy.get("[data-cy=form-dialog]")
+ .should("contain", "Make a copy")
+ .within(() => {
+ cy.get("[data-cy=projects-tree-home-tree-picker]").contains("Projects").click();
+ cy.get("[data-cy=form-submit-btn]").click();
+ });
+ cy.get("[data-cy=snackbar]").contains("Collection has been copied.");
+ cy.get("[data-cy=snackbar-goto-action]").click();
+ cy.get("[data-cy=project-panel]").contains(copyName).click();
+ cy.get("[data-cy=collection-files-panel]").should("contain", "some-file");
+ });
});
- it('uses the collection version browser to view a previous version', function () {
+ it("uses the collection version browser to view a previous version", function () {
const colName = `Test Collection ${Math.floor(Math.random() * 999999)}`;
// Creates the collection using the admin token so we can set up
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.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('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("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 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())})]`;
// Select a storage class.
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'New collection')
- .and('contain', 'Storage classes')
- .and('contain', 'default')
- .and('contain', 'foo')
- .and('contain', 'bar')
+ cy.get("[data-cy=form-dialog]")
+ .should("contain", "New collection")
+ .and("contain", "Storage classes")
+ .and("contain", "default")
+ .and("contain", "foo")
+ .and("contain", "bar")
.within(() => {
- cy.get('[data-cy=parent-field]').within(() => {
- cy.get('input').should('have.value', 'Home project');
+ cy.get("[data-cy=parent-field]").within(() => {
+ cy.get("input").should("have.value", "Home project");
});
- cy.get('[data-cy=name-field]').within(() => {
- cy.get('input').type(collName);
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type(collName);
});
- cy.get('[data-cy=checkbox-foo]').click();
- })
+ cy.get("[data-cy=checkbox-foo]").click();
+ });
// Add a property.
// Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
- cy.get('[data-cy=form-dialog]').should('not.contain', 'Color: Magenta');
- cy.get('[data-cy=resource-properties-form]').within(() => {
- cy.get('[data-cy=property-field-key]').within(() => {
- cy.get('input').type('Color');
+ cy.get("[data-cy=form-dialog]").should("not.contain", "Color: Magenta");
+ cy.get("[data-cy=resource-properties-form]").within(() => {
+ cy.get("[data-cy=property-field-key]").within(() => {
+ cy.get("input").type("Color");
});
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('Magenta');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("Magenta");
});
cy.root().submit();
});
// Confirm proper vocabulary labels are displayed on the UI.
- cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
+ cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
// Value field should not complain about being required just after
// adding a new property. See #19732
- cy.get('[data-cy=form-dialog]').should('not.contain', 'This field is required');
+ cy.get("[data-cy=form-dialog]").should("not.contain", "This field is required");
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[data-cy=form-submit-btn]").click();
// Confirm that the user was taken to the newly created collection
- cy.get('[data-cy=form-dialog]').should('not.exist');
- cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
- cy.get('[data-cy=breadcrumb-last]').should('contain', collName);
- cy.get('[data-cy=collection-info-panel]')
- .should('contain', 'default')
- .and('contain', 'foo')
- .and('contain', 'Color: Magenta')
- .and('not.contain', 'bar');
+ cy.get("[data-cy=form-dialog]").should("not.exist");
+ cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
+ cy.get("[data-cy=breadcrumb-last]").should("contain", collName);
+ cy.get("[data-cy=collection-info-panel]")
+ .should("contain", "default")
+ .and("contain", "foo")
+ .and("contain", "Color: Magenta")
+ .and("not.contain", "bar");
// Confirm that the collection's properties has the real values.
- cy.doRequest('GET', '/arvados/v1/collections', null, {
+ cy.doRequest("GET", "/arvados/v1/collections", null, {
filters: `[["name", "=", "${collName}"]]`,
})
- .its('body.items').as('collections')
- .then(function() {
- expect(this.collections).to.have.lengthOf(1);
- expect(this.collections[0].properties).to.have.property(
- 'IDTAGCOLORS', 'IDVALCOLORS3');
- });
+ .its("body.items")
+ .as("collections")
+ .then(function () {
+ expect(this.collections).to.have.lengthOf(1);
+ expect(this.collections[0].properties).to.have.property("IDTAGCOLORS", "IDVALCOLORS3");
+ });
});
- it('shows responsible person for collection if available', () => {
+ it("shows responsible person for collection if available", () => {
cy.createCollection(adminUser.token, {
name: `Test collection ${Math.floor(Math.random() * 999999)}`,
owner_uuid: activeUser.user.uuid,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).as('testCollection1');
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).as("testCollection1");
cy.createCollection(adminUser.token, {
name: `Test collection ${Math.floor(Math.random() * 999999)}`,
owner_uuid: adminUser.user.uuid,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).as('testCollection2').then(function (testCollection2) {
- cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, 'can_write');
- });
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ })
+ .as("testCollection2")
+ .then(function (testCollection2) {
+ cy.shareWith(adminUser.token, activeUser.user.uuid, testCollection2.uuid, "can_write");
+ });
- cy.getAll('@testCollection1', '@testCollection2')
- .then(function ([testCollection1, testCollection2]) {
- cy.loginAs(activeUser);
+ cy.getAll("@testCollection1", "@testCollection2").then(function ([testCollection1, testCollection2]) {
+ cy.loginAs(activeUser);
- cy.goToPath(`/collections/${testCollection1.uuid}`);
- cy.get('[data-cy=responsible-person-wrapper]')
- .contains(activeUser.user.uuid);
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
+ cy.get("[data-cy=responsible-person-wrapper]").contains(activeUser.user.uuid);
- cy.goToPath(`/collections/${testCollection2.uuid}`);
- cy.get('[data-cy=responsible-person-wrapper]')
- .contains(adminUser.user.uuid);
- });
+ cy.goToPath(`/collections/${testCollection2.uuid}`);
+ cy.get("[data-cy=responsible-person-wrapper]").contains(adminUser.user.uuid);
+ });
});
- describe('file upload', () => {
+ describe("file upload", () => {
beforeEach(() => {
cy.createCollection(adminUser.token, {
name: `Test collection ${Math.floor(Math.random() * 999999)}`,
owner_uuid: activeUser.user.uuid,
- manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).as('testCollection1');
+ manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).as("testCollection1");
});
- it('uploads a file and checks the collection UI to be fresh', () => {
- cy.getAll('@testCollection1')
- .then(function([testCollection1]) {
- cy.loginAs(activeUser);
- cy.goToPath(`/collections/${testCollection1.uuid}`);
- cy.get('[data-cy=upload-button]').click();
- cy.get('[data-cy=collection-files-panel]')
- .contains('5mb_a.bin').should('not.exist');
- cy.get('[data-cy=collection-file-count]').should('contain', '2');
- cy.fixture('files/5mb.bin', 'base64').then(content => {
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
- cy.get('[data-cy=form-submit-btn]').click();
- cy.get('[data-cy=form-submit-btn]').should('not.exist');
- cy.get('[data-cy=collection-files-panel]')
- .contains('5mb_a.bin').should('exist');
- cy.get('[data-cy=collection-file-count]').should('contain', '3');
-
- cy.get('[data-cy=collection-files-panel]').contains('subdir').click();
- cy.get('[data-cy=upload-button]').click();
- cy.fixture('files/5mb.bin', 'base64').then(content => {
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
- cy.get('[data-cy=form-submit-btn]').click();
- cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
- // subdir gets unselected, I think this is a bug but
- // for the time being let's just make sure the test works.
- cy.get('[data-cy=collection-files-panel]').contains('subdir').click();
- cy.waitForDom().get('[data-cy=collection-files-right-panel]')
- .contains('5mb_b.bin').should('exist');
- });
+ it("uploads a file and checks the collection UI to be fresh", () => {
+ cy.getAll("@testCollection1").then(function ([testCollection1]) {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
+ cy.get("[data-cy=upload-button]").click();
+ cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("not.exist");
+ cy.get("[data-cy=collection-file-count]").should("contain", "2");
+ cy.fixture("files/5mb.bin", "base64").then(content => {
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
+ cy.get("[data-cy=form-submit-btn]").click();
+ cy.get("[data-cy=form-submit-btn]").should("not.exist");
+ cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("exist");
+ cy.get("[data-cy=collection-file-count]").should("contain", "3");
+
+ cy.get("[data-cy=collection-files-panel]").contains("subdir").click();
+ cy.get("[data-cy=upload-button]").click();
+ cy.fixture("files/5mb.bin", "base64").then(content => {
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
+ cy.get("[data-cy=form-submit-btn]").click();
+ cy.waitForDom().get("[data-cy=form-submit-btn]").should("not.exist");
+ // subdir gets unselected, I think this is a bug but
+ // for the time being let's just make sure the test works.
+ cy.get("[data-cy=collection-files-panel]").contains("subdir").click();
+ cy.waitForDom().get("[data-cy=collection-files-right-panel]").contains("5mb_b.bin").should("exist");
});
});
+ });
});
- it('allows to cancel running upload', () => {
- cy.getAll('@testCollection1')
- .then(function([testCollection1]) {
- cy.loginAs(activeUser);
+ it("allows to cancel running upload", () => {
+ cy.getAll("@testCollection1").then(function ([testCollection1]) {
+ cy.loginAs(activeUser);
- cy.goToPath(`/collections/${testCollection1.uuid}`);
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
- cy.get('[data-cy=upload-button]').click();
+ cy.get("[data-cy=upload-button]").click();
- cy.fixture('files/5mb.bin', 'base64').then(content => {
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+ cy.fixture("files/5mb.bin", "base64").then(content => {
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[data-cy=form-submit-btn]").click();
- cy.get('button').contains('Cancel').click();
+ cy.get("button").contains("Cancel").click();
- cy.get('[data-cy=form-submit-btn]').should('not.exist');
- });
+ cy.get("[data-cy=form-submit-btn]").should("not.exist");
});
+ });
});
- it('allows to cancel single file from the running upload', () => {
- cy.getAll('@testCollection1')
- .then(function([testCollection1]) {
- cy.loginAs(activeUser);
+ it("allows to cancel single file from the running upload", () => {
+ cy.getAll("@testCollection1").then(function ([testCollection1]) {
+ cy.loginAs(activeUser);
- cy.goToPath(`/collections/${testCollection1.uuid}`);
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
- cy.get('[data-cy=upload-button]').click();
+ cy.get("[data-cy=upload-button]").click();
- cy.fixture('files/5mb.bin', 'base64').then(content => {
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
+ cy.fixture("files/5mb.bin", "base64").then(content => {
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[data-cy=form-submit-btn]").click();
- cy.get('button[aria-label=Remove]').eq(1).click();
+ cy.get("button[aria-label=Remove]").eq(1).click();
- cy.get('[data-cy=form-submit-btn]').should('not.exist');
+ cy.get("[data-cy=form-submit-btn]").should("not.exist");
- cy.get('[data-cy=collection-files-panel]').contains('5mb_a.bin').should('exist');
- });
+ cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("exist");
});
+ });
});
- it('allows to cancel all files from the running upload', () => {
- cy.getAll('@testCollection1')
- .then(function([testCollection1]) {
- cy.loginAs(activeUser);
-
- cy.goToPath(`/collections/${testCollection1.uuid}`);
-
- // Confirm initial collection state.
- cy.get('[data-cy=collection-files-panel]')
- .contains('bar').should('exist');
- cy.get('[data-cy=collection-files-panel]')
- .contains('5mb_a.bin').should('not.exist');
- cy.get('[data-cy=collection-files-panel]')
- .contains('5mb_b.bin').should('not.exist');
-
- cy.get('[data-cy=upload-button]').click();
-
- cy.fixture('files/5mb.bin', 'base64').then(content => {
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_a.bin');
- cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
-
- cy.get('[data-cy=form-submit-btn]').click();
-
- cy.get('button[aria-label=Remove]').should('exist');
- cy.get('button[aria-label=Remove]')
- .click({ multiple: true, force: true });
-
- cy.get('[data-cy=form-submit-btn]').should('not.exist');
-
- // Confirm final collection state.
- cy.get('[data-cy=collection-files-panel]')
- .contains('bar').should('exist');
- // The following fails, but doesn't seem to happen
- // in the real world. Maybe there's a race between
- // the PUT request finishing and the 'Remove' button
- // dissapearing, because sometimes just one of the 2
- // files gets uploaded.
- // Maybe this will be needed to simulate a slow network:
- // https://docs.cypress.io/api/commands/intercept#Convenience-functions-1
- // cy.get('[data-cy=collection-files-panel]')
- // .contains('5mb_a.bin').should('not.exist');
- // cy.get('[data-cy=collection-files-panel]')
- // .contains('5mb_b.bin').should('not.exist');
- });
+ it("allows to cancel all files from the running upload", () => {
+ cy.getAll("@testCollection1").then(function ([testCollection1]) {
+ cy.loginAs(activeUser);
+
+ cy.goToPath(`/collections/${testCollection1.uuid}`);
+
+ // Confirm initial collection state.
+ cy.get("[data-cy=collection-files-panel]").contains("bar").should("exist");
+ cy.get("[data-cy=collection-files-panel]").contains("5mb_a.bin").should("not.exist");
+ cy.get("[data-cy=collection-files-panel]").contains("5mb_b.bin").should("not.exist");
+
+ cy.get("[data-cy=upload-button]").click();
+
+ cy.fixture("files/5mb.bin", "base64").then(content => {
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_a.bin");
+ cy.get("[data-cy=drag-and-drop]").upload(content, "5mb_b.bin");
+
+ cy.get("[data-cy=form-submit-btn]").click();
+
+ cy.get("button[aria-label=Remove]").should("exist");
+ cy.get("button[aria-label=Remove]").click({ multiple: true, force: true });
+
+ cy.get("[data-cy=form-submit-btn]").should("not.exist");
+
+ // Confirm final collection state.
+ cy.get("[data-cy=collection-files-panel]").contains("bar").should("exist");
+ // The following fails, but doesn't seem to happen
+ // in the real world. Maybe there's a race between
+ // the PUT request finishing and the 'Remove' button
+ // dissapearing, because sometimes just one of the 2
+ // files gets uploaded.
+ // Maybe this will be needed to simulate a slow network:
+ // https://docs.cypress.io/api/commands/intercept#Convenience-functions-1
+ // cy.get('[data-cy=collection-files-panel]')
+ // .contains('5mb_a.bin').should('not.exist');
+ // cy.get('[data-cy=collection-files-panel]')
+ // .contains('5mb_b.bin').should('not.exist');
});
+ });
});
});
-})
+});
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Multi-file deletion tests', function () {
+describe('Create workflow tests', function () {
let activeUser;
let adminUser;
});
});
}));
+
+ it('allows selecting collection subdirectories and reselects existing selections', () => {
+ cy.createProject({
+ owningUser: activeUser,
+ projectName: 'myProject1',
+ addToFavorites: true
+ });
+
+ cy.createCollection(adminUser.token, {
+ name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+ owner_uuid: activeUser.user.uuid,
+ manifest_text: "./subdir/dir1 d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\n./subdir/dir2 d41d8cd98f00b204e9800998ecf8427e+0 0:0:\\056\n"
+ })
+ .as('testCollection');
+
+ cy.getAll('@myProject1', '@testCollection')
+ .then(function ([myProject1, testCollection]) {
+ cy.readFile('cypress/fixtures/workflow_directory_array.yaml').then(workflow => {
+ cy.createWorkflow(adminUser.token, {
+ name: `TestWorkflow${Math.floor(Math.random() * 999999)}.cwl`,
+ definition: workflow,
+ owner_uuid: myProject1.uuid,
+ })
+ .as('testWorkflow');
+ });
+
+ cy.loginAs(activeUser);
+
+ cy.get('main').contains(myProject1.name).click();
+
+ cy.get('[data-cy=side-panel-button]').click();
+
+ cy.get('#aside-menu-list').contains('Run a workflow').click();
+
+ cy.get('@testWorkflow')
+ .then((testWorkflow) => {
+ cy.get('main').contains(testWorkflow.name).click();
+ cy.get('[data-cy=run-process-next-button]').click();
+
+ cy.get('label').contains('directoryInputName').parent('div').find('input').click();
+ cy.get('div[role=dialog]')
+ .within(() => {
+ // must use .then to avoid selecting instead of expanding https://github.com/cypress-io/cypress/issues/5529
+ cy.get('p').contains('Home Projects').closest('ul')
+ .find('i')
+ .then(el => el.click());
+
+ cy.get(`[data-id=${testCollection.uuid}]`)
+ .find('i').click();
+
+ cy.get(`[data-id="${testCollection.uuid}/subdir"]`)
+ .find('i').click();
+
+ cy.contains('dir1').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+ cy.contains('dir2').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').click();
+
+ cy.get('[data-cy=ok-button]').click();
+ });
+
+ // Verify subdirectories were selected
+ cy.get('label').contains('directoryInputName').parent('div')
+ .within(() => {
+ cy.contains('dir1');
+ cy.contains('dir2');
+ });
+
+ // Reopen tree picker and verify subdirectories are preselected
+ cy.get('label').contains('directoryInputName').parent('div').find('input').click();
+ cy.waitForDom().get('div[role=dialog]')
+ .within(() => {
+ cy.contains('dir1').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').should('be.checked');
+ cy.contains('dir2').closest('[data-action=TOGGLE_ACTIVE]').parent().find('input[type=checkbox]').should('be.checked');
+ });
+ });
+
+ });
+ })
})
//
// 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;
- before(function() {
+ before(function () {
// Only set up common users once. These aren't set up as aliases because
// aliases are cleaned up after every test. Also it doesn't make sense
// to set the same users on beforeEach() over and over again, so we
// separate a little from Cypress' 'Best Practices' here.
- cy.getUser('admin', 'Admin', 'User', true, true)
- .as('adminUser').then(function() {
+ cy.getUser("admin", "Admin", "User", true, true)
+ .as("adminUser")
+ .then(function () {
adminUser = this.adminUser;
- }
- );
- cy.getUser('user', 'Active', 'User', false, true)
- .as('activeUser').then(function() {
+ });
+ cy.getUser("user", "Active", "User", false, true)
+ .as("activeUser")
+ .then(function () {
activeUser = this.activeUser;
- }
- );
+ });
});
- beforeEach(function() {
+ beforeEach(function () {
cy.clearCookies();
cy.clearLocalStorage();
});
function setupDockerImage(image_name) {
// Create a collection that will be used as a docker image for the tests.
cy.createCollection(adminUser.token, {
- name: 'docker_image',
- manifest_text: ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n"
- }).as('dockerImage').then(function(dockerImage) {
- // Give read permissions to the active user on the docker image.
- cy.createLink(adminUser.token, {
- link_class: 'permission',
- name: 'can_read',
- tail_uuid: activeUser.user.uuid,
- head_uuid: dockerImage.uuid
- }).as('dockerImagePermission').then(function() {
- // Set-up docker image collection tags
- cy.createLink(activeUser.token, {
- link_class: 'docker_image_repo+tag',
- name: image_name,
+ name: "docker_image",
+ manifest_text:
+ ". d21353cfe035e3e384563ee55eadbb2f+67108864 5c77a43e329b9838cbec18ff42790e57+55605760 0:122714624:sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678.tar\n",
+ })
+ .as("dockerImage")
+ .then(function (dockerImage) {
+ // Give read permissions to the active user on the docker image.
+ cy.createLink(adminUser.token, {
+ link_class: "permission",
+ name: "can_read",
+ tail_uuid: activeUser.user.uuid,
head_uuid: dockerImage.uuid,
- }).as('dockerImageRepoTag');
- cy.createLink(activeUser.token, {
- link_class: 'docker_image_hash',
- name: 'sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678',
- head_uuid: dockerImage.uuid,
- }).as('dockerImageHash');
- })
- });
- return cy.getAll('@dockerImage', '@dockerImageRepoTag', '@dockerImageHash',
- '@dockerImagePermission').then(function([dockerImage]) {
- return dockerImage;
+ })
+ .as("dockerImagePermission")
+ .then(function () {
+ // Set-up docker image collection tags
+ cy.createLink(activeUser.token, {
+ link_class: "docker_image_repo+tag",
+ name: image_name,
+ head_uuid: dockerImage.uuid,
+ }).as("dockerImageRepoTag");
+ cy.createLink(activeUser.token, {
+ link_class: "docker_image_hash",
+ name: "sha256:d8309758b8fe2c81034ffc8a10c36460b77db7bc5e7b448c4e5b684f9d95a678",
+ head_uuid: dockerImage.uuid,
+ }).as("dockerImageHash");
+ });
});
+ return cy.getAll("@dockerImage", "@dockerImageRepoTag", "@dockerImageHash", "@dockerImagePermission").then(function ([dockerImage]) {
+ return dockerImage;
+ });
}
- function createContainerRequest(user, name, docker_image, command, reuse = false, state = 'Uncommitted') {
- return setupDockerImage(docker_image).then(function(dockerImage) {
+ function createContainerRequest(user, name, docker_image, command, reuse = false, state = "Uncommitted") {
+ return setupDockerImage(docker_image).then(function (dockerImage) {
return cy.createContainerRequest(user.token, {
name: name,
command: command,
container_image: dockerImage.portable_data_hash, // for some reason, docker_image doesn't work here
- output_path: 'stdout.txt',
+ output_path: "stdout.txt",
priority: 1,
runtime_constraints: {
vcpus: 1,
state: state,
mounts: {
foo: {
- kind: 'tmp',
- path: '/tmp/foo',
- }
- }
+ kind: "tmp",
+ path: "/tmp/foo",
+ },
+ },
});
});
}
- 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('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");
});
});
- 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");
});
- const getFakeContainer = (fakeContainerUuid) => ({
+ const getFakeContainer = fakeContainerUuid => ({
href: `/containers/${fakeContainerUuid}`,
kind: "arvados#container",
etag: "ecfosljpnxfari9a8m7e4yv06",
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",
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"],
subrequests_cost: 0.0,
});
- 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: {},
+ }
+ );
// 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}`);
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");
});
});
-
});
-
- 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.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");
});
// 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");
});
});
});
- 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)]);
});
});
});
- 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");
});
});
});
- 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");
});
});
});
-
});
- 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,
+ },
},
{
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": "input2.tar",
- "class": "File",
- "location": "keep:00000000000000000000000000000000+02/input2.tar"
+ basename: "input2.tar",
+ class: "File",
+ location: "keep:00000000000000000000000000000000+02/input2.tar",
},
{
- "basename": "input3.tar",
- "class": "File",
- "location": "keep:00000000000000000000000000000000+03/input3.tar",
- "secondaryFiles": [
+ basename: "input3.tar",
+ class: "File",
+ 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",
+ },
+ ],
},
{
- "$import": "import_path"
- }
- ]
- }
+ $import: "import_path",
+ },
+ ],
+ },
},
{
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_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_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",
+ },
+ ],
+ },
},
{
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_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",
+ },
+ ],
+ },
},
{
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_bool_include",
- "type": "boolean"
+ id: "#main/input_bool_include",
+ type: "boolean",
},
input: {
- "input_bool_include": {
- "$include": "include_path"
- }
- }
+ input_bool_include: {
+ $include: "include_path",
+ },
+ },
},
{
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_float_include",
- "type": "float"
+ id: "#main/input_float_include",
+ type: "float",
},
input: {
- "input_float_include": {
- "$include": "include_path"
- }
- }
+ input_float_include: {
+ $include: "include_path",
+ },
+ },
},
{
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_file_include",
- "type": "File"
+ id: "#main/input_file_include",
+ type: "File",
},
input: {
- "input_file_include": {
- "$include": "include_path"
- }
- }
+ input_file_include: {
+ $include: "include_path",
+ },
+ },
},
{
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_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",
+ },
+ },
+ },
];
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: "cat.png",
+ },
+ },
},
{
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: "main.dat",
+ secondaryFiles: [
{
- "basename": "secondary.dat",
- "class": "File",
- "location": "secondary.dat"
+ basename: "secondary.dat",
+ class: "File",
+ location: "secondary.dat",
},
{
- "basename": "secondary2.dat",
- "class": "File",
- "location": "secondary2.dat"
- }
- ]
- }
- }
+ basename: "secondary2.dat",
+ class: "File",
+ location: "secondary2.dat",
+ },
+ ],
+ },
+ },
},
{
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: "outdir1",
+ },
+ },
},
{
definition: {
- "id": "#main/output_bool",
- "type": "boolean"
+ id: "#main/output_bool",
+ type: "boolean",
},
output: {
- "output_bool": true
- }
+ output_bool: true,
+ },
},
{
definition: {
- "id": "#main/output_int",
- "type": "int"
+ id: "#main/output_int",
+ type: "int",
},
output: {
- "output_int": 1
- }
+ output_int: 1,
+ },
},
{
definition: {
- "id": "#main/output_long",
- "type": "long"
+ id: "#main/output_long",
+ type: "long",
},
output: {
- "output_long": 1
- }
+ output_long: 1,
+ },
},
{
definition: {
- "id": "#main/output_float",
- "type": "float"
+ id: "#main/output_float",
+ type: "float",
},
output: {
- "output_float": 100.5
- }
+ output_float: 100.5,
+ },
},
{
definition: {
- "id": "#main/output_double",
- "type": "double"
+ id: "#main/output_double",
+ type: "double",
},
output: {
- "output_double": 100.3
- }
+ output_double: 100.3,
+ },
},
{
definition: {
- "id": "#main/output_string",
- "type": "string"
+ id: "#main/output_string",
+ type: "string",
},
output: {
- "output_string": "Hello output"
- }
+ output_string: "Hello output",
+ },
},
{
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": "output2.tar",
- "class": "File",
- "location": "output2.tar"
+ basename: "output2.tar",
+ class: "File",
+ location: "output2.tar",
},
{
- "basename": "output3.tar",
- "class": "File",
- "location": "output3.tar"
- }
- ]
- }
+ basename: "output3.tar",
+ class: "File",
+ location: "output3.tar",
+ },
+ ],
+ },
},
{
definition: {
- "id": "#main/output_dir_array",
- "type": {
- "items": "Directory",
- "type": "array"
- }
+ id: "#main/output_dir_array",
+ type: {
+ items: "Directory",
+ 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",
+ },
+ ],
+ },
},
{
definition: {
- "id": "#main/output_int_array",
- "type": {
- "items": "int",
- "type": "array"
- }
+ id: "#main/output_int_array",
+ type: {
+ items: "int",
+ type: "array",
+ },
},
output: {
- "output_int_array": [
- 10,
- 11,
- 12
- ]
- }
+ output_int_array: [10, 11, 12],
+ },
},
{
definition: {
- "id": "#main/output_long_array",
- "type": {
- "items": "long",
- "type": "array"
- }
+ id: "#main/output_long_array",
+ type: {
+ items: "long",
+ type: "array",
+ },
},
output: {
- "output_long_array": [
- 51,
- 52
- ]
- }
+ output_long_array: [51, 52],
+ },
},
{
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
- ]
- }
+ output_float_array: [100.2, 100.4, 100.6],
+ },
},
{
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],
+ },
},
{
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);
+ 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);
- }
- } 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);
- })
- });
+ 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");
+ });
// 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),
+ },
+ ],
+ },
};
});
});
// 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,
+ },
}
- });
+ );
// 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), {}),
+ }
+ );
// 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",
+ }
+ );
});
- 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", "!"]);
});
});
});
- 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 = fakeOutputUUID;
res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
- content: {}
+ 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))
- }]
- }
+ $graph: [
+ {
+ id: "#main",
+ inputs: testInputs.map(input => input.definition),
+ outputs: testOutputs.map(output => output.definition),
+ },
+ ],
+ },
};
});
});
// 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/${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.intercept(
+ { method: "GET", url: `**/c%3D${fakeOutputUUID}/cwl.output.json` },
+ {
+ statusCode: 200,
+ body: {},
+ }
+ );
- 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),
+ }
+ );
});
- 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.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");
});
});
});
});
});
-
});
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Project tests', function() {
+describe("Project tests", function () {
let activeUser;
let adminUser;
- before(function() {
+ before(function () {
// Only set up common users once. These aren't set up as aliases because
// aliases are cleaned up after every test. Also it doesn't make sense
// to set the same users on beforeEach() over and over again, so we
// separate a little from Cypress' 'Best Practices' here.
- cy.getUser('admin', 'Admin', 'User', true, true)
- .as('adminUser').then(function() {
+ cy.getUser("admin", "Admin", "User", true, true)
+ .as("adminUser")
+ .then(function () {
adminUser = this.adminUser;
- }
- );
- cy.getUser('user', 'Active', 'User', false, true)
- .as('activeUser').then(function() {
+ });
+ cy.getUser("user", "Active", "User", false, true)
+ .as("activeUser")
+ .then(function () {
activeUser = this.activeUser;
- }
- );
+ });
});
- beforeEach(function() {
+ beforeEach(function () {
cy.clearCookies();
cy.clearLocalStorage();
});
- it('creates a new project with multiple properties', function() {
+ it("creates a new project with multiple properties", function () {
const projName = `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(projName);
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type(projName);
});
-
});
// Key: Color (IDTAGCOLORS) - Value: Magenta (IDVALCOLORS3)
- cy.get('[data-cy=form-dialog]').should('not.contain', 'Color: Magenta');
- cy.get('[data-cy=resource-properties-form]').within(() => {
- cy.get('[data-cy=property-field-key]').within(() => {
- cy.get('input').type('Color');
+ cy.get("[data-cy=form-dialog]").should("not.contain", "Color: Magenta");
+ cy.get("[data-cy=resource-properties-form]").within(() => {
+ cy.get("[data-cy=property-field-key]").within(() => {
+ cy.get("input").type("Color");
});
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('Magenta');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("Magenta");
});
cy.root().submit();
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('Pink');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("Pink");
});
cy.root().submit();
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('Yellow');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("Yellow");
});
cy.root().submit();
});
// Confirm proper vocabulary labels are displayed on the UI.
- cy.get('[data-cy=form-dialog]').should('contain', 'Color: Magenta');
- cy.get('[data-cy=form-dialog]').should('contain', 'Color: Pink');
- cy.get('[data-cy=form-dialog]').should('contain', 'Color: Yellow');
+ cy.get("[data-cy=form-dialog]").should("contain", "Color: Magenta");
+ cy.get("[data-cy=form-dialog]").should("contain", "Color: Pink");
+ cy.get("[data-cy=form-dialog]").should("contain", "Color: Yellow");
- cy.get('[data-cy=resource-properties-form]').within(() => {
- cy.get('[data-cy=property-field-key]').within(() => {
- cy.get('input').focus();
+ cy.get("[data-cy=resource-properties-form]").within(() => {
+ cy.get("[data-cy=property-field-key]").within(() => {
+ cy.get("input").focus();
});
- cy.get('[data-cy=property-field-key]').should('not.contain', 'Color');
+ cy.get("[data-cy=property-field-key]").should("not.contain", "Color");
});
// Create project and confirm the properties' real values.
- cy.get('[data-cy=form-submit-btn]').click();
- cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
- cy.doRequest('GET', '/arvados/v1/groups', null, {
+ cy.get("[data-cy=form-submit-btn]").click();
+ cy.get("[data-cy=breadcrumb-last]").should("contain", projName);
+ cy.doRequest("GET", "/arvados/v1/groups", null, {
filters: `[["name", "=", "${projName}"], ["group_class", "=", "project"]]`,
})
- .its('body.items').as('projects')
- .then(function() {
- expect(this.projects).to.have.lengthOf(1);
- expect(this.projects[0].properties).to.deep.equal(
- // Pink is not in the test vocab
- {IDTAGCOLORS: ['IDVALCOLORS3', 'Pink', 'IDVALCOLORS1']});
- });
+ .its("body.items")
+ .as("projects")
+ .then(function () {
+ expect(this.projects).to.have.lengthOf(1);
+ expect(this.projects[0].properties).to.deep.equal(
+ // Pink is not in the test vocab
+ { IDTAGCOLORS: ["IDVALCOLORS3", "Pink", "IDVALCOLORS1"] }
+ );
+ });
// Open project edit via breadcrumbs
- cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
- cy.get('[data-cy=context-menu]').contains('Edit').click();
- cy.get('[data-cy=form-dialog]').within(() => {
- cy.get('[data-cy=resource-properties-list]').within(() => {
- cy.get('div[role=button]').contains('Color: Magenta');
- cy.get('div[role=button]').contains('Color: Pink');
- cy.get('div[role=button]').contains('Color: Yellow');
+ cy.get("[data-cy=breadcrumbs]").contains(projName).rightclick();
+ cy.get("[data-cy=context-menu]").contains("Edit").click();
+ cy.get("[data-cy=form-dialog]").within(() => {
+ cy.get("[data-cy=resource-properties-list]").within(() => {
+ cy.get("div[role=button]").contains("Color: Magenta");
+ cy.get("div[role=button]").contains("Color: Pink");
+ cy.get("div[role=button]").contains("Color: Yellow");
});
});
// Add another property
- cy.get('[data-cy=resource-properties-form]').within(() => {
- cy.get('[data-cy=property-field-key]').within(() => {
- cy.get('input').type('Animal');
+ cy.get("[data-cy=resource-properties-form]").within(() => {
+ cy.get("[data-cy=property-field-key]").within(() => {
+ cy.get("input").type("Animal");
});
- cy.get('[data-cy=property-field-value]').within(() => {
- cy.get('input').type('Dog');
+ cy.get("[data-cy=property-field-value]").within(() => {
+ cy.get("input").type("Dog");
});
cy.root().submit();
});
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[data-cy=form-submit-btn]").click({ force: true });
// Reopen edit via breadcrumbs and verify properties
- cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
- cy.get('[data-cy=context-menu]').contains('Edit').click();
- cy.get('[data-cy=form-dialog]').within(() => {
- cy.get('[data-cy=resource-properties-list]').within(() => {
- cy.get('div[role=button]').contains('Color: Magenta');
- cy.get('div[role=button]').contains('Color: Pink');
- cy.get('div[role=button]').contains('Color: Yellow');
- cy.get('div[role=button]').contains('Animal: Dog');
+ cy.get("[data-cy=breadcrumbs]").contains(projName).rightclick();
+ cy.get("[data-cy=context-menu]").contains("Edit").click();
+ cy.get("[data-cy=form-dialog]").within(() => {
+ cy.get("[data-cy=resource-properties-list]").within(() => {
+ cy.get("div[role=button]").contains("Color: Magenta");
+ cy.get("div[role=button]").contains("Color: Pink");
+ cy.get("div[role=button]").contains("Color: Yellow");
+ cy.get("div[role=button]").contains("Animal: Dog");
});
});
});
- it('creates a project without and with description', function() {
+ it("creates a project without and with description", function () {
const projName = `Test project (${Math.floor(999999 * Math.random())})`;
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 verifyProjectDescription = (name, description) => {
- cy.doRequest('GET', '/arvados/v1/groups', null, {
+ cy.doRequest("GET", "/arvados/v1/groups", null, {
filters: `[["name", "=", "${name}"], ["group_class", "=", "project"]]`,
})
- .its('body.items').as('projects')
- .then(function() {
- expect(this.projects).to.have.lengthOf(1);
- expect(this.projects[0].description).to.equal(description);
- });
+ .its("body.items")
+ .as("projects")
+ .then(function () {
+ expect(this.projects).to.have.lengthOf(1);
+ expect(this.projects[0].description).to.equal(description);
+ });
};
// Edit description
- editProjectDescription(projName, 'Test description');
+ editProjectDescription(projName, "Test description");
// Check description is set
verifyProjectDescription(projName, "<p>Test description</p>");
// Clear description
- editProjectDescription(projName, '{selectall}{backspace}');
+ editProjectDescription(projName, "{selectall}{backspace}");
// Check description is null
verifyProjectDescription(projName, null);
// Set description to contain whitespace
- editProjectDescription(projName, '{selectall}{backspace} x');
- editProjectDescription(projName, '{backspace}');
+ editProjectDescription(projName, "{selectall}{backspace} x");
+ editProjectDescription(projName, "{backspace}");
// Check description is null
verifyProjectDescription(projName, null);
-
});
- it('creates new project on home project and then a subproject inside it', function() {
- const createProject = function(name, parentName) {
- 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')
+ it("creates new project on home project and then a subproject inside it", function () {
+ const createProject = function (name, parentName) {
+ 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=parent-field]').within(() => {
- cy.get('input').invoke('val').then((val) => {
- expect(val).to.include(parentName);
- });
+ cy.get("[data-cy=parent-field]").within(() => {
+ cy.get("input")
+ .invoke("val")
+ .then(val => {
+ expect(val).to.include(parentName);
+ });
});
- cy.get('[data-cy=name-field]').within(() => {
- cy.get('input').type(name);
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type(name);
});
});
- cy.get('[data-cy=form-submit-btn]').click();
- }
+ cy.get("[data-cy=form-submit-btn]").click();
+ };
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 project
const projName = `Test project (${Math.floor(999999 * Math.random())})`;
- createProject(projName, 'Home project');
+ createProject(projName, "Home project");
// Confirm that the user was taken to the newly created thing
- cy.get('[data-cy=form-dialog]').should('not.exist');
- cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
- cy.get('[data-cy=breadcrumb-last]').should('contain', projName);
+ cy.get("[data-cy=form-dialog]").should("not.exist");
+ cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
+ cy.get("[data-cy=breadcrumb-last]").should("contain", projName);
// Create a subproject
const subProjName = `Test project (${Math.floor(999999 * Math.random())})`;
createProject(subProjName, projName);
- cy.get('[data-cy=form-dialog]').should('not.exist');
- cy.get('[data-cy=breadcrumb-first]').should('contain', 'Projects');
- cy.get('[data-cy=breadcrumb-last]').should('contain', subProjName);
+ cy.get("[data-cy=form-dialog]").should("not.exist");
+ cy.get("[data-cy=breadcrumb-first]").should("contain", "Projects");
+ cy.get("[data-cy=breadcrumb-last]").should("contain", subProjName);
});
- it('attempts to use a preexisting name creating a project', function() {
+ it("attempts to use a preexisting name creating a project", function () {
const name = `Test project ${Math.floor(Math.random() * 999999)}`;
cy.createGroup(activeUser.token, {
name: name,
- group_class: 'project',
+ group_class: "project",
});
cy.loginAs(activeUser);
cy.goToPath(`/projects/${activeUser.user.uuid}`);
// Attempt to create new collection with a duplicate name
- 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(name);
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type(name);
});
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[data-cy=form-submit-btn]").click();
});
// Error message should display, allowing editing the name
- cy.get('[data-cy=form-dialog]').should('exist')
- .and('contain', 'Project with the same name already exists')
+ cy.get("[data-cy=form-dialog]")
+ .should("exist")
+ .and("contain", "Project with the same name already exists")
.within(() => {
- cy.get('[data-cy=name-field]').within(() => {
- cy.get('input').type(' renamed');
+ cy.get("[data-cy=name-field]").within(() => {
+ cy.get("input").type(" renamed");
});
- 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=form-dialog]").should("not.exist");
});
- it('navigates to the parent project after trashing the one being displayed', function() {
+ it("navigates to the parent project after trashing the one being displayed", function () {
cy.createGroup(activeUser.token, {
name: `Test root project ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- }).as('testRootProject').then(function() {
- cy.createGroup(activeUser.token, {
- name : `Test subproject ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- owner_uuid: this.testRootProject.uuid,
- }).as('testSubProject');
- });
- cy.getAll('@testRootProject', '@testSubProject').then(function([testRootProject, testSubProject]) {
+ group_class: "project",
+ })
+ .as("testRootProject")
+ .then(function () {
+ cy.createGroup(activeUser.token, {
+ name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
+ group_class: "project",
+ owner_uuid: this.testRootProject.uuid,
+ }).as("testSubProject");
+ });
+ cy.getAll("@testRootProject", "@testSubProject").then(function ([testRootProject, testSubProject]) {
cy.loginAs(activeUser);
// Go to subproject and trash it.
cy.goToPath(`/projects/${testSubProject.uuid}`);
- cy.get('[data-cy=side-panel-tree]').should('contain', testSubProject.name);
- cy.get('[data-cy=breadcrumb-last]')
- .should('contain', testSubProject.name)
- .rightclick();
- cy.get('[data-cy=context-menu]').contains('Move to trash').click();
+ cy.get("[data-cy=side-panel-tree]").should("contain", testSubProject.name);
+ cy.get("[data-cy=breadcrumb-last]").should("contain", testSubProject.name).rightclick();
+ cy.get("[data-cy=context-menu]").contains("Move to trash").click();
// Confirm that the parent project should be displayed.
- cy.get('[data-cy=breadcrumb-last]').should('contain', testRootProject.name);
- cy.url().should('contain', `/projects/${testRootProject.uuid}`);
- cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubProject.name);
+ cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
+ cy.url().should("contain", `/projects/${testRootProject.uuid}`);
+ cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
// Checks for bugfix #17637.
- cy.get('[data-cy=not-found-content]').should('not.exist');
- cy.get('[data-cy=not-found-page]').should('not.exist');
+ cy.get("[data-cy=not-found-content]").should("not.exist");
+ cy.get("[data-cy=not-found-page]").should("not.exist");
});
});
- it('resets the search box only when navigating out of the current project', function() {
+ it("resets the search box only when navigating out of the current project", function () {
const fooProjectNameA = `Test foo project ${Math.floor(Math.random() * 999999)}`;
const fooProjectNameB = `Test foo project ${Math.floor(Math.random() * 999999)}`;
const barProjectNameA = `Test bar project ${Math.floor(Math.random() * 999999)}`;
[fooProjectNameA, fooProjectNameB, barProjectNameA].forEach(projName => {
cy.createGroup(activeUser.token, {
name: projName,
- group_class: 'project',
+ group_class: "project",
});
});
cy.loginAs(activeUser);
- cy.get('[data-cy=project-panel]')
- .should('contain', fooProjectNameA)
- .and('contain', fooProjectNameB)
- .and('contain', barProjectNameA);
+ cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("contain", barProjectNameA);
- cy.get('[data-cy=search-input]').type('foo');
- cy.get('[data-cy=project-panel]')
- .should('contain', fooProjectNameA)
- .and('contain', fooProjectNameB)
- .and('not.contain', barProjectNameA);
+ cy.get("[data-cy=search-input]").type("foo");
+ cy.get("[data-cy=project-panel]").should("contain", fooProjectNameA).and("contain", fooProjectNameB).and("not.contain", barProjectNameA);
// Click on the table row to select it, search should remain the same.
- cy.get(`p:contains(${fooProjectNameA})`)
- .parent().parent().parent().parent().click();
- cy.get('[data-cy=search-input] input').should('have.value', 'foo');
+ cy.get(`p:contains(${fooProjectNameA})`).parent().parent().parent().parent().click();
+ cy.get("[data-cy=search-input] input").should("have.value", "foo");
// Click to navigate to the project, search should be reset
cy.get(`p:contains(${fooProjectNameA})`).click();
- cy.get('[data-cy=search-input] input').should('not.have.value', 'foo');
+ cy.get("[data-cy=search-input] input").should("not.have.value", "foo");
});
- it('navigates to the root project after trashing the parent of the one being displayed', function() {
+ it("navigates to the root project after trashing the parent of the one being displayed", function () {
cy.createGroup(activeUser.token, {
name: `Test root project ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- }).as('testRootProject').then(function() {
- cy.createGroup(activeUser.token, {
- name : `Test subproject ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- owner_uuid: this.testRootProject.uuid,
- }).as('testSubProject').then(function() {
+ group_class: "project",
+ })
+ .as("testRootProject")
+ .then(function () {
cy.createGroup(activeUser.token, {
- name : `Test sub subproject ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- owner_uuid: this.testSubProject.uuid,
- }).as('testSubSubProject');
+ name: `Test subproject ${Math.floor(Math.random() * 999999)}`,
+ group_class: "project",
+ owner_uuid: this.testRootProject.uuid,
+ })
+ .as("testSubProject")
+ .then(function () {
+ cy.createGroup(activeUser.token, {
+ name: `Test sub subproject ${Math.floor(Math.random() * 999999)}`,
+ group_class: "project",
+ owner_uuid: this.testSubProject.uuid,
+ }).as("testSubSubProject");
+ });
});
- });
- cy.getAll('@testRootProject', '@testSubProject', '@testSubSubProject').then(function([testRootProject, testSubProject, testSubSubProject]) {
+ cy.getAll("@testRootProject", "@testSubProject", "@testSubSubProject").then(function ([testRootProject, testSubProject, testSubSubProject]) {
cy.loginAs(activeUser);
// Go to innermost project and trash its parent.
cy.goToPath(`/projects/${testSubSubProject.uuid}`);
- cy.get('[data-cy=side-panel-tree]').should('contain', testSubSubProject.name);
- cy.get('[data-cy=breadcrumb-last]').should('contain', testSubSubProject.name);
- cy.get('[data-cy=side-panel-tree]')
- .contains(testSubProject.name)
- .rightclick();
- cy.get('[data-cy=context-menu]').contains('Move to trash').click();
+ cy.get("[data-cy=side-panel-tree]").should("contain", testSubSubProject.name);
+ cy.get("[data-cy=breadcrumb-last]").should("contain", testSubSubProject.name);
+ cy.get("[data-cy=side-panel-tree]").contains(testSubProject.name).rightclick();
+ cy.get("[data-cy=context-menu]").contains("Move to trash").click();
// Confirm that the trashed project's parent should be displayed.
- cy.get('[data-cy=breadcrumb-last]').should('contain', testRootProject.name);
- cy.url().should('contain', `/projects/${testRootProject.uuid}`);
- cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubProject.name);
- cy.get('[data-cy=side-panel-tree]').should('not.contain', testSubSubProject.name);
+ cy.get("[data-cy=breadcrumb-last]").should("contain", testRootProject.name);
+ cy.url().should("contain", `/projects/${testRootProject.uuid}`);
+ cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubProject.name);
+ cy.get("[data-cy=side-panel-tree]").should("not.contain", testSubSubProject.name);
// Checks for bugfix #17637.
- cy.get('[data-cy=not-found-content]').should('not.exist');
- cy.get('[data-cy=not-found-page]').should('not.exist');
+ cy.get("[data-cy=not-found-content]").should("not.exist");
+ cy.get("[data-cy=not-found-page]").should("not.exist");
});
});
- it('shows details panel when clicking on the info icon', () => {
+ it("shows details panel when clicking on the info icon", () => {
cy.createGroup(activeUser.token, {
name: `Test root project ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- }).as('testRootProject').then(function(testRootProject) {
- cy.loginAs(activeUser);
+ group_class: "project",
+ })
+ .as("testRootProject")
+ .then(function (testRootProject) {
+ cy.loginAs(activeUser);
- cy.get('[data-cy=side-panel-tree]').contains(testRootProject.name).click();
+ cy.get("[data-cy=side-panel-tree]").contains(testRootProject.name).click();
- cy.get('[data-cy=additional-info-icon]').click();
+ cy.get("[data-cy=additional-info-icon]").click();
- cy.contains(testRootProject.uuid).should('exist');
- });
+ cy.contains(testRootProject.uuid).should("exist");
+ });
});
- it('clears search input when changing project', () => {
+ it("clears search input when changing project", () => {
cy.createGroup(activeUser.token, {
name: `Test root project ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- }).as('testProject1').then((testProject1) => {
- cy.shareWith(adminUser.token, activeUser.user.uuid, testProject1.uuid, 'can_write');
- });
+ group_class: "project",
+ })
+ .as("testProject1")
+ .then(testProject1 => {
+ cy.shareWith(adminUser.token, activeUser.user.uuid, testProject1.uuid, "can_write");
+ });
- cy.getAll('@testProject1').then(function([testProject1]) {
+ cy.getAll("@testProject1").then(function ([testProject1]) {
cy.loginAs(activeUser);
- cy.get('[data-cy=side-panel-tree]').contains(testProject1.name).click();
+ cy.get("[data-cy=side-panel-tree]").contains(testProject1.name).click();
- cy.get('[data-cy=search-input] input').type('test123');
+ cy.get("[data-cy=search-input] input").type("test123");
- cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+ cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
- cy.get('[data-cy=search-input] input').should('not.have.value', 'test123');
+ cy.get("[data-cy=search-input] input").should("not.have.value", "test123");
});
});
- it('opens advanced popup for project with username', () => {
+ it("opens advanced popup for project with username", () => {
const projectName = `Test project ${Math.floor(Math.random() * 999999)}`;
cy.createGroup(adminUser.token, {
name: projectName,
- group_class: 'project',
- }).as('mainProject')
+ group_class: "project",
+ }).as("mainProject");
- cy.getAll('@mainProject')
- .then(function ([mainProject]) {
- cy.loginAs(adminUser);
+ cy.getAll("@mainProject").then(function ([mainProject]) {
+ cy.loginAs(adminUser);
- cy.get('[data-cy=side-panel-tree]').contains('Groups').click();
+ cy.get("[data-cy=side-panel-tree]").contains("Groups").click();
- cy.get('[data-cy=uuid]').eq(0).invoke('text').then(uuid => {
+ cy.get("[data-cy=uuid]")
+ .eq(0)
+ .invoke("text")
+ .then(uuid => {
cy.createLink(adminUser.token, {
- name: 'can_write',
- link_class: 'permission',
+ name: "can_write",
+ link_class: "permission",
head_uuid: mainProject.uuid,
- tail_uuid: uuid
+ tail_uuid: uuid,
});
cy.createLink(adminUser.token, {
- name: 'can_write',
- link_class: 'permission',
+ name: "can_write",
+ link_class: "permission",
head_uuid: mainProject.uuid,
- tail_uuid: activeUser.user.uuid
+ tail_uuid: activeUser.user.uuid,
});
- cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
+ cy.get("[data-cy=side-panel-tree]").contains("Projects").click();
- cy.get('main').contains(projectName).rightclick();
+ cy.get("main").contains(projectName).rightclick();
- cy.get('[data-cy=context-menu]').contains('API Details').click();
+ cy.get("[data-cy=context-menu]").contains("API Details").click();
- cy.get('[role=tablist]').contains('METADATA').click();
+ cy.get("[role=tablist]").contains("METADATA").click();
- cy.get('td').contains(uuid).should('exist');
+ cy.get("td").contains(uuid).should("exist");
- cy.get('td').contains(activeUser.user.uuid).should('exist');
+ cy.get("td").contains(activeUser.user.uuid).should("exist");
});
});
});
- describe('Frozen projects', () => {
+ describe("Frozen projects", () => {
beforeEach(() => {
cy.createGroup(activeUser.token, {
name: `Main project ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- }).as('mainProject');
+ group_class: "project",
+ }).as("mainProject");
cy.createGroup(adminUser.token, {
name: `Admin project ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- }).as('adminProject').then((mainProject) => {
- cy.shareWith(adminUser.token, activeUser.user.uuid, mainProject.uuid, 'can_write');
- });
+ group_class: "project",
+ })
+ .as("adminProject")
+ .then(mainProject => {
+ cy.shareWith(adminUser.token, activeUser.user.uuid, mainProject.uuid, "can_write");
+ });
- cy.get('@mainProject').then((mainProject) => {
+ cy.get("@mainProject").then(mainProject => {
cy.createGroup(adminUser.token, {
- name : `Sub project ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
+ name: `Sub project ${Math.floor(Math.random() * 999999)}`,
+ group_class: "project",
owner_uuid: mainProject.uuid,
- }).as('subProject');
+ }).as("subProject");
cy.createCollection(adminUser.token, {
name: `Main collection ${Math.floor(Math.random() * 999999)}`,
owner_uuid: mainProject.uuid,
- manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).as('mainCollection');
+ manifest_text: "./subdir 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n. 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).as("mainCollection");
});
});
- it('should be able to froze own project', () => {
- cy.getAll('@mainProject').then(([mainProject]) => {
+ it("should be able to froze own project", () => {
+ cy.getAll("@mainProject").then(([mainProject]) => {
cy.loginAs(activeUser);
- cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+ cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Freeze').click();
+ cy.get("[data-cy=context-menu]").contains("Freeze").click();
- cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+ cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Freeze').should('not.exist');
+ cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
});
});
- it('should not be able to modify items within the frozen project', () => {
- cy.getAll('@mainProject', '@mainCollection').then(([mainProject, mainCollection]) => {
+ it("should not be able to modify items within the frozen project", () => {
+ cy.getAll("@mainProject", "@mainCollection").then(([mainProject, mainCollection]) => {
cy.loginAs(activeUser);
- cy.get('[data-cy=project-panel]').contains(mainProject.name).rightclick();
+ cy.get("[data-cy=project-panel]").contains(mainProject.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Freeze').click();
+ cy.get("[data-cy=context-menu]").contains("Freeze").click();
- cy.get('[data-cy=project-panel]').contains(mainProject.name).click();
+ cy.get("[data-cy=project-panel]").contains(mainProject.name).click();
- cy.get('[data-cy=project-panel]').contains(mainCollection.name).rightclick();
+ cy.get("[data-cy=project-panel]").contains(mainCollection.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Move to trash').should('not.exist');
+ cy.get("[data-cy=context-menu]").contains("Move to trash").should("not.exist");
});
});
- it('should be able to froze not owned project', () => {
- cy.getAll('@adminProject').then(([adminProject]) => {
+ it("should be able to froze not owned project", () => {
+ cy.getAll("@adminProject").then(([adminProject]) => {
cy.loginAs(activeUser);
- cy.get('[data-cy=side-panel-tree]').contains('Shared with me').click();
+ cy.get("[data-cy=side-panel-tree]").contains("Shared with me").click();
- cy.get('main').contains(adminProject.name).rightclick();
+ cy.get("main").contains(adminProject.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Freeze').should('not.exist');
+ cy.get("[data-cy=context-menu]").contains("Freeze").should("not.exist");
});
});
- it('should be able to unfroze project if user is an admin', () => {
- cy.getAll('@adminProject').then(([adminProject]) => {
+ it("should be able to unfroze project if user is an admin", () => {
+ cy.getAll("@adminProject").then(([adminProject]) => {
cy.loginAs(adminUser);
- cy.get('main').contains(adminProject.name).rightclick();
+ cy.get("main").contains(adminProject.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Freeze').click();
+ cy.get("[data-cy=context-menu]").contains("Freeze").click();
cy.wait(1000);
- cy.get('main').contains(adminProject.name).rightclick();
+ cy.get("main").contains(adminProject.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Unfreeze').click();
+ cy.get("[data-cy=context-menu]").contains("Unfreeze").click();
- cy.get('main').contains(adminProject.name).rightclick();
+ cy.get("main").contains(adminProject.name).rightclick();
- cy.get('[data-cy=context-menu]').contains('Freeze').should('exist');
+ cy.get("[data-cy=context-menu]").contains("Freeze").should("exist");
});
});
});
- it('copies project URL to clipboard', () => {
+ it("copies project URL to clipboard", () => {
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\:\/\/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.only("sorts displayed items correctly", () => {
cy.loginAs(activeUser);
cy.get('[data-cy=project-panel] button[title="Select columns"]').click();
- cy.get('div[role=presentation] ul > div[role=button]').contains('Date Created').click();
- cy.get('div[role=presentation] ul > div[role=button]').contains('Trash at').click();
- cy.get('div[role=presentation] ul > div[role=button]').contains('Delete at').click();
- cy.get('div[role=presentation] > div[aria-hidden=true]').click();
+ cy.get("div[role=presentation] ul > div[role=button]").contains("Date Created").click();
+ cy.get("div[role=presentation] ul > div[role=button]").contains("Trash at").click();
+ cy.get("div[role=presentation] ul > div[role=button]").contains("Delete at").click();
+ cy.get("div[role=presentation] > div[aria-hidden=true]").click();
- cy.intercept({method: 'GET', url: '**/arvados/v1/groups/*/contents*'}).as('filteredQuery');
+ cy.intercept({ method: "GET", url: "**/arvados/v1/groups/*/contents*" }).as("filteredQuery");
[
{
name: "Name",
- asc: "collections.name asc,container_requests.name asc,groups.name asc",
- desc: "collections.name desc,container_requests.name desc,groups.name desc"
+ asc: "collections.name asc,container_requests.name asc,groups.name asc,container_requests.created_at desc",
+ desc: "collections.name desc,container_requests.name desc,groups.name desc,container_requests.created_at desc",
},
{
name: "Last Modified",
- asc: "collections.modified_at asc,container_requests.modified_at asc,groups.modified_at asc",
- desc: "collections.modified_at desc,container_requests.modified_at desc,groups.modified_at desc"
+ asc: "collections.modified_at asc,container_requests.modified_at asc,groups.modified_at asc,container_requests.created_at desc",
+ desc: "collections.modified_at desc,container_requests.modified_at desc,groups.modified_at desc,container_requests.created_at desc",
},
{
name: "Date Created",
- asc: "collections.created_at asc,container_requests.created_at asc,groups.created_at asc",
- desc: "collections.created_at desc,container_requests.created_at desc,groups.created_at desc"
-
+ asc: "collections.created_at asc,container_requests.created_at asc,groups.created_at asc,container_requests.created_at desc",
+ desc: "collections.created_at desc,container_requests.created_at desc,groups.created_at desc,container_requests.created_at desc",
},
{
name: "Trash at",
- asc: "collections.trash_at asc,container_requests.trash_at asc,groups.trash_at asc",
- desc: "collections.trash_at desc,container_requests.trash_at desc,groups.trash_at desc"
-
+ asc: "collections.trash_at asc,container_requests.trash_at asc,groups.trash_at asc,container_requests.created_at desc",
+ desc: "collections.trash_at desc,container_requests.trash_at desc,groups.trash_at desc,container_requests.created_at desc",
},
{
name: "Delete at",
- asc: "collections.delete_at asc,container_requests.delete_at asc,groups.delete_at asc",
- desc: "collections.delete_at desc,container_requests.delete_at desc,groups.delete_at desc"
-
+ asc: "collections.delete_at asc,container_requests.delete_at asc,groups.delete_at asc,container_requests.created_at desc",
+ desc: "collections.delete_at desc,container_requests.delete_at desc,groups.delete_at desc,container_requests.created_at desc",
},
- ].forEach((test) => {
- cy.get('[data-cy=project-panel] table thead th').contains(test.name).click();
- cy.wait('@filteredQuery').then(interception => {
- const searchParams = new URLSearchParams((new URL(interception.request.url).search));
- expect(searchParams.get('order')).to.eq(test.asc);
+ ].forEach(test => {
+ cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
+ cy.wait("@filteredQuery").then(interception => {
+ const searchParams = new URLSearchParams(new URL(interception.request.url).search);
+ expect(searchParams.get("order")).to.eq(test.asc);
});
- cy.get('[data-cy=project-panel] table thead th').contains(test.name).click();
- cy.wait('@filteredQuery').then(interception => {
- const searchParams = new URLSearchParams((new URL(interception.request.url).search));
- expect(searchParams.get('order')).to.eq(test.desc);
+ cy.get("[data-cy=project-panel] table thead th").contains(test.name).click();
+ cy.wait("@filteredQuery").then(interception => {
+ const searchParams = new URLSearchParams(new URL(interception.request.url).search);
+ expect(searchParams.get("order")).to.eq(test.desc);
});
});
-
});
});
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Search tests', function() {
+describe("Search tests", function () {
let activeUser;
let adminUser;
- before(function() {
+ before(function () {
// Only set up common users once. These aren't set up as aliases because
// aliases are cleaned up after every test. Also it doesn't make sense
// to set the same users on beforeEach() over and over again, so we
// separate a little from Cypress' 'Best Practices' here.
- cy.getUser('admin', 'Admin', 'User', true, true)
- .as('adminUser').then(function() {
+ cy.getUser("admin", "Admin", "User", true, true)
+ .as("adminUser")
+ .then(function () {
adminUser = this.adminUser;
- }
- );
- cy.getUser('collectionuser1', 'Collection', 'User', false, true)
- .as('activeUser').then(function() {
+ });
+ cy.getUser("collectionuser1", "Collection", "User", false, true)
+ .as("activeUser")
+ .then(function () {
activeUser = this.activeUser;
- }
- );
- })
+ });
+ });
- beforeEach(function() {
- cy.clearCookies()
- cy.clearLocalStorage()
- })
+ beforeEach(function () {
+ cy.clearCookies();
+ cy.clearLocalStorage();
+ });
- it('can search for old collection versions', function() {
+ it("can search for old collection versions", function () {
const colName = `Versioned Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
- let colUuid = '';
- let oldVersionUuid = '';
+ let colUuid = "";
+ let oldVersionUuid = "";
// Make sure no other collections with this name exist
- cy.doRequest('GET', '/arvados/v1/collections', null, {
+ cy.doRequest("GET", "/arvados/v1/collections", null, {
filters: `[["name", "=", "${colName}"]]`,
- include_old_versions: true
+ include_old_versions: true,
})
- .its('body.items').as('collections')
- .then(function() {
- expect(this.collections).to.be.empty;
- });
+ .its("body.items")
+ .as("collections")
+ .then(function () {
+ expect(this.collections).to.be.empty;
+ });
// Creates the collection using the admin token so we can set up
// a bogus manifest text without block signatures.
cy.createCollection(adminUser.token, {
name: colName,
owner_uuid: activeUser.user.uuid,
preserve_version: true,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"})
- .as('originalVersion').then(function() {
- // Change the file name to create a new version.
- cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n"
- })
- colUuid = this.originalVersion.uuid;
- });
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ })
+ .as("originalVersion")
+ .then(function () {
+ // Change the file name to create a new version.
+ cy.updateCollection(adminUser.token, this.originalVersion.uuid, {
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:foo\n",
+ });
+ colUuid = this.originalVersion.uuid;
+ });
// Confirm that there are 2 versions of the collection
- cy.doRequest('GET', '/arvados/v1/collections', null, {
+ cy.doRequest("GET", "/arvados/v1/collections", null, {
filters: `[["name", "=", "${colName}"]]`,
- include_old_versions: true
+ include_old_versions: true,
})
- .its('body.items').as('collections')
- .then(function() {
- expect(this.collections).to.have.lengthOf(2);
- this.collections.map(function(aCollection) {
- expect(aCollection.current_version_uuid).to.equal(colUuid);
- if (aCollection.uuid !== aCollection.current_version_uuid) {
- oldVersionUuid = aCollection.uuid;
- }
+ .its("body.items")
+ .as("collections")
+ .then(function () {
+ expect(this.collections).to.have.lengthOf(2);
+ this.collections.map(function (aCollection) {
+ expect(aCollection.current_version_uuid).to.equal(colUuid);
+ if (aCollection.uuid !== aCollection.current_version_uuid) {
+ oldVersionUuid = aCollection.uuid;
+ }
+ });
+ cy.loginAs(activeUser);
+ const searchQuery = `${colName} type:arvados#collection`;
+ // Search for only collection's current version
+ cy.doSearch(`${searchQuery}`);
+ cy.get("[data-cy=search-results]").should("contain", "head version");
+ cy.get("[data-cy=search-results]").should("not.contain", "version 1");
+ // ...and then, include old versions.
+ cy.doSearch(`${searchQuery} is:pastVersion`);
+ cy.get("[data-cy=search-results]").should("contain", "head version");
+ cy.get("[data-cy=search-results]").should("contain", "version 1");
});
- cy.loginAs(activeUser);
- const searchQuery = `${colName} type:arvados#collection`;
- // Search for only collection's current version
- cy.doSearch(`${searchQuery}`);
- cy.get('[data-cy=search-results]').should('contain', 'head version');
- cy.get('[data-cy=search-results]').should('not.contain', 'version 1');
- // ...and then, include old versions.
- cy.doSearch(`${searchQuery} is:pastVersion`);
- cy.get('[data-cy=search-results]').should('contain', 'head version');
- cy.get('[data-cy=search-results]').should('contain', 'version 1');
- });
});
- it('can display path of the selected item', function() {
+ it("can display path of the selected item", function () {
const colName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
// Creates the collection using the admin token so we can set up
name: colName,
owner_uuid: activeUser.user.uuid,
preserve_version: true,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).then(function() {
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).then(function () {
cy.loginAs(activeUser);
cy.doSearch(colName);
- cy.get('[data-cy=search-results]').should('contain', colName);
+ cy.get("[data-cy=search-results]").should("contain", colName);
- cy.get('[data-cy=search-results]').contains(colName).closest('tr').click();
+ cy.get("[data-cy=search-results]").contains(colName).closest("tr").click();
- cy.get('[data-cy=element-path]').should('contain', `/ Projects / ${colName}`);
+ cy.get("[data-cy=element-path]").should("contain", `/ Projects / ${colName}`);
});
});
- it('can search items using quotes', function() {
+ it("can search items using quotes", function () {
const random = Math.floor(Math.random() * Math.floor(999999));
const colName = `Collection ${random}`;
const colName2 = `Collection test ${random}`;
name: colName,
owner_uuid: activeUser.user.uuid,
preserve_version: true,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).as('collection1');
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).as("collection1");
cy.createCollection(adminUser.token, {
name: colName2,
owner_uuid: activeUser.user.uuid,
preserve_version: true,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).as('collection2');
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).as("collection2");
- cy.getAll('@collection1', '@collection2')
- .then(function() {
- cy.loginAs(activeUser);
+ cy.getAll("@collection1", "@collection2").then(function () {
+ cy.loginAs(activeUser);
- cy.doSearch(colName);
- cy.get('[data-cy=search-results] table tbody tr').should('have.length', 2);
+ cy.doSearch(colName);
+ cy.get("[data-cy=search-results] table tbody tr").should("have.length", 2);
- cy.doSearch(`"${colName}"`);
- cy.get('[data-cy=search-results] table tbody tr').should('have.length', 1);
- });
+ cy.doSearch(`"${colName}"`);
+ cy.get("[data-cy=search-results] table tbody tr").should("have.length", 1);
+ });
});
- it('can display owner of the item', function() {
+ it("can display owner of the item", function () {
const colName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
cy.createCollection(adminUser.token, {
name: colName,
owner_uuid: activeUser.user.uuid,
preserve_version: true,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).then(function() {
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).then(function () {
cy.loginAs(activeUser);
cy.doSearch(colName);
- cy.get('[data-cy=search-results]').should('contain', colName);
+ cy.get("[data-cy=search-results]").should("contain", colName);
- cy.get('[data-cy=search-results]').contains(colName).closest('tr')
+ cy.get("[data-cy=search-results]")
+ .contains(colName)
+ .closest("tr")
.within(() => {
- cy.get('p').contains(activeUser.user.uuid).should('contain', activeUser.user.full_name);
+ cy.get("p").contains(activeUser.user.uuid).should("contain", activeUser.user.full_name);
});
});
});
- it('shows search context menu', function() {
+ it("shows search context menu", function () {
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: '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: "",
+ },
};
});
});
// Fake remote cluster config
cy.intercept(
- {
- method: "GET",
- hostname: "xxxxx.fakecluster.tld",
- url: "**/arvados/v1/config",
- },
- {
- statusCode: 200,
- body: {
- API: {},
- ClusterID: "xxxxx",
- Collections: {},
- Containers: {},
- InstanceTypes: {},
- Login: {},
- Mail: { SupportEmailAddress: "arvados@example.com" },
- RemoteClusters: {
- "*": {
- ActivateUsers: false,
- Host: "",
- Insecure: false,
- Proxy: false,
- Scheme: "https",
- },
- },
- Services: {
- Composer: { ExternalURL: "" },
- Controller: { ExternalURL: "https://xxxxx.fakecluster.tld:34763/" },
- DispatchCloud: { ExternalURL: "" },
- DispatchLSF: { ExternalURL: "" },
- DispatchSLURM: { ExternalURL: "" },
- GitHTTP: { ExternalURL: "https://xxxxx.fakecluster.tld:39105/" },
- GitSSH: { ExternalURL: "" },
- Health: { ExternalURL: "https://xxxxx.fakecluster.tld:42915/" },
- Keepbalance: { ExternalURL: "" },
- Keepproxy: { ExternalURL: "https://xxxxx.fakecluster.tld:46773/" },
- Keepstore: { ExternalURL: "" },
- RailsAPI: { ExternalURL: "" },
- WebDAV: { ExternalURL: "https://xxxxx.fakecluster.tld:36041/" },
- WebDAVDownload: { ExternalURL: "https://xxxxx.fakecluster.tld:42957/" },
- WebShell: { ExternalURL: "" },
- Websocket: { ExternalURL: "wss://xxxxx.fakecluster.tld:37121/websocket" },
- Workbench1: { ExternalURL: "https://wb1.xxxxx.fakecluster.tld/" },
- Workbench2: { ExternalURL: "https://wb2.xxxxx.fakecluster.tld/" },
- },
- StorageClasses: {
- default: { Default: true, Priority: 0 },
- },
- Users: {},
- Volumes: {},
- Workbench: {},
+ {
+ method: "GET",
+ hostname: "xxxxx.fakecluster.tld",
+ url: "**/arvados/v1/config",
},
- }
+ {
+ statusCode: 200,
+ body: {
+ API: {},
+ ClusterID: "xxxxx",
+ Collections: {},
+ Containers: {},
+ InstanceTypes: {},
+ Login: {},
+ Mail: { SupportEmailAddress: "arvados@example.com" },
+ RemoteClusters: {
+ "*": {
+ ActivateUsers: false,
+ Host: "",
+ Insecure: false,
+ Proxy: false,
+ Scheme: "https",
+ },
+ },
+ Services: {
+ Composer: { ExternalURL: "" },
+ Controller: { ExternalURL: "https://xxxxx.fakecluster.tld:34763/" },
+ DispatchCloud: { ExternalURL: "" },
+ DispatchLSF: { ExternalURL: "" },
+ DispatchSLURM: { ExternalURL: "" },
+ GitHTTP: { ExternalURL: "https://xxxxx.fakecluster.tld:39105/" },
+ GitSSH: { ExternalURL: "" },
+ Health: { ExternalURL: "https://xxxxx.fakecluster.tld:42915/" },
+ Keepbalance: { ExternalURL: "" },
+ Keepproxy: { ExternalURL: "https://xxxxx.fakecluster.tld:46773/" },
+ Keepstore: { ExternalURL: "" },
+ RailsAPI: { ExternalURL: "" },
+ WebDAV: { ExternalURL: "https://xxxxx.fakecluster.tld:36041/" },
+ WebDAVDownload: { ExternalURL: "https://xxxxx.fakecluster.tld:42957/" },
+ WebShell: { ExternalURL: "" },
+ Websocket: { ExternalURL: "wss://xxxxx.fakecluster.tld:37121/websocket" },
+ Workbench1: { ExternalURL: "https://wb1.xxxxx.fakecluster.tld/" },
+ Workbench2: { ExternalURL: "https://wb2.xxxxx.fakecluster.tld/" },
+ },
+ StorageClasses: {
+ default: { Default: true, Priority: 0 },
+ },
+ Users: {},
+ Volumes: {},
+ Workbench: {},
+ },
+ }
);
cy.createCollection(adminUser.token, {
name: colName,
owner_uuid: activeUser.user.uuid,
preserve_version: true,
- manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
- }).then(function(testCollection) {
+ manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n",
+ }).then(function (testCollection) {
cy.loginAs(activeUser);
// Intercept search results to add federated result
- cy.intercept({method: 'GET', url: '**/arvados/v1/groups/contents?*'}, (req) => {
- req.reply((res) => {
+ cy.intercept({ method: "GET", url: "**/arvados/v1/groups/contents?*" }, req => {
+ req.reply(res => {
res.body.items = [
res.body.items[0],
{
portable_data_hash: "00000000000000000000000000000000+0",
name: federatedColName,
href: res.body.items[0].href.replace(testCollection.uuid, federatedColUuid),
- }
+ },
];
res.body.items_available += 1;
});
// Stub new window
cy.window().then(win => {
- cy.stub(win, 'open').as('Open')
+ cy.stub(win, "open").as("Open");
});
// 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}`);
});
-
});
});
});
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Virtual machine login manage tests', function() {
+describe("Virtual machine login manage tests", function () {
let activeUser;
let adminUser;
const vmHost = `vm-${Math.floor(999999 * Math.random())}.host`;
- before(function() {
+ before(function () {
// Only set up common users once. These aren't set up as aliases because
// aliases are cleaned up after every test. Also it doesn't make sense
// to set the same users on beforeEach() over and over again, so we
// separate a little from Cypress' 'Best Practices' here.
- cy.getUser('admin', 'VMAdmin', 'User', true, true)
- .as('adminUser').then(function() {
+ cy.getUser("admin", "VMAdmin", "User", true, true)
+ .as("adminUser")
+ .then(function () {
adminUser = this.adminUser;
- }
- );
- cy.getUser('user', 'VMActive', 'User', false, true)
- .as('activeUser').then(function() {
+ });
+ cy.getUser("user", "VMActive", "User", false, true)
+ .as("activeUser")
+ .then(function () {
activeUser = this.activeUser;
- }
- );
+ });
});
- it('adds and removes vm logins', function() {
+ it("adds and removes vm logins", function () {
cy.loginAs(adminUser);
- cy.createVirtualMachine(adminUser.token, {hostname: vmHost});
+ cy.createVirtualMachine(adminUser.token, { hostname: vmHost });
// Navigate to VM admin
cy.get('header button[title="Admin Panel"]').click();
- cy.get('#admin-menu').contains('Virtual Machines').click();
+ cy.get("#admin-menu").contains("Virtual Machines").click();
// Add login permission to admin
- 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();
});
- 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.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();
});
- 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('VMActive user');
- })
+ cy.get("label")
+ .contains("Search for user")
+ .parent()
+ .within(() => {
+ cy.get("input").type("VMActive user");
+ });
});
- cy.get('[role=tooltip]').click();
- cy.get('[data-cy=form-dialog]').within(() => {
- cy.get('[data-cy=form-submit-btn]').click();
+ cy.get("[role=tooltip]").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('user');
- });
+ cy.get("td").contains("user");
+ });
// Check admin's vm page for login
cy.get('header button[title="Account Management"]').click();
- cy.get('#account-menu').contains('Virtual Machines').click();
+ cy.get("#account-menu").contains("Virtual Machines").click();
- cy.get('[data-cy=vm-user-table]')
+ cy.get("[data-cy=vm-user-table]")
.contains(vmHost)
- .parents('tr')
+ .parents("tr")
.within(() => {
- cy.get('td').contains('admin');
- cy.get('td').contains('docker');
- cy.get('td').contains('sudo');
- cy.get('td').contains('ssh admin@' + vmHost);
- });
+ cy.get("td").contains("admin");
+ cy.get("td").contains("docker");
+ cy.get("td").contains("sudo");
+ cy.get("td").contains("ssh admin@" + vmHost);
+ });
// Check activeUser's vm page for login
cy.loginAs(activeUser);
cy.get('header button[title="Account Management"]').click();
- cy.get('#account-menu').contains('Virtual Machines').click();
+ cy.get("#account-menu").contains("Virtual Machines").click();
- cy.get('[data-cy=vm-user-table]')
+ cy.get("[data-cy=vm-user-table]")
.contains(vmHost)
- .parents('tr')
+ .parents("tr")
.within(() => {
- cy.get('td').contains('user');
- cy.get('td').should('not.contain', 'docker');
- cy.get('td').should('not.contain', 'sudo');
- cy.get('td').contains('ssh user@' + vmHost);
- });
+ cy.get("td").contains("user");
+ cy.get("td").should("not.contain", "docker");
+ cy.get("td").should("not.contain", "sudo");
+ cy.get("td").contains("ssh user@" + vmHost);
+ });
// Edit 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('admin'); // Wait for page to finish
+ cy.get("[data-cy=vm-admin-table]").contains("admin"); // Wait for page to finish
- cy.get('[data-cy=vm-admin-table]')
- .contains(vmHost)
- .parents('tr')
- .contains('admin')
- .click();
+ cy.get("[data-cy=vm-admin-table]").contains(vmHost).parents("tr").contains("admin").click();
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'Update login permission')
+ cy.get("[data-cy=form-dialog]")
+ .should("contain", "Update login permission")
.within(() => {
- cy.get('label')
- .contains('Add groups')
- .parent()
- .as('groupInput');
+ cy.get("label").contains("Add groups").parent().as("groupInput");
});
- cy.get('@groupInput').within(() => {
- cy.get('div[role=button]').contains('sudo').parent().find('svg').click();
- cy.get('div[role=button]').contains('docker').parent().find('svg').click();
+ cy.get("@groupInput").within(() => {
+ cy.get("div[role=button]").contains("sudo").parent().find("svg").click();
+ cy.get("div[role=button]").contains("docker").parent().find("svg").click();
});
- 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();
});
// Wait for page to finish loading
- cy.get('[data-cy=vm-admin-table]')
+ cy.get("[data-cy=vm-admin-table]")
.contains(vmHost)
- .parents('tr')
+ .parents("tr")
.within(() => {
- cy.get('div[role=button]')
- .parent()
- .first()
- .contains('admin')
+ cy.get("div[role=button]").parent().first().contains("admin");
});
- cy.get('[data-cy=vm-admin-table]')
- .contains(vmHost)
- .parents('tr')
- .contains('user')
- .click();
+ cy.get("[data-cy=vm-admin-table]").contains(vmHost).parents("tr").contains("user").click();
- cy.get('[data-cy=form-dialog]')
- .should('contain', 'Update login permission')
+ cy.get("[data-cy=form-dialog]")
+ .should("contain", "Update login permission")
.within(() => {
- cy.get('label')
- .contains('Add groups')
+ cy.get("label")
+ .contains("Add groups")
.parent()
.within(() => {
- cy.get('input').type('docker{enter}');
- })
+ cy.get("input").type("docker{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();
});
// Verify new login permissions
// Check admin's vm page for login
cy.get('header button[title="Account Management"]').click();
- cy.get('#account-menu').contains('Virtual Machines').click();
+ cy.get("#account-menu").contains("Virtual Machines").click();
- cy.get('[data-cy=vm-user-table]')
+ cy.get("[data-cy=vm-user-table]")
.contains(vmHost)
- .parents('tr')
+ .parents("tr")
.within(() => {
- cy.get('td').contains('admin');
- cy.get('td').should('not.contain', 'docker');
- cy.get('td').should('not.contain', 'sudo');
- cy.get('td').contains('ssh admin@' + vmHost);
- });
+ cy.get("td").contains("admin");
+ cy.get("td").should("not.contain", "docker");
+ cy.get("td").should("not.contain", "sudo");
+ cy.get("td").contains("ssh admin@" + vmHost);
+ });
// Verify new login permissions
// Check activeUser's vm page for login
cy.loginAs(activeUser);
cy.get('header button[title="Account Management"]').click();
- cy.get('#account-menu').contains('Virtual Machines').click();
+ cy.get("#account-menu").contains("Virtual Machines").click();
- cy.get('[data-cy=vm-user-table]')
+ cy.get("[data-cy=vm-user-table]")
.contains(vmHost)
- .parents('tr')
+ .parents("tr")
.within(() => {
- cy.get('td').contains('user');
- cy.get('td').contains('docker');
- cy.get('td').should('not.contain', 'sudo');
- cy.get('td').contains('ssh user@' + vmHost);
- });
+ cy.get("td").contains("user");
+ cy.get("td").contains("docker");
+ cy.get("td").should("not.contain", "sudo");
+ cy.get("td").contains("ssh user@" + vmHost);
+ });
// 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.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
cy.get('header button[title="Account Management"]').click();
- cy.get('#account-menu').contains('Virtual Machines').click();
+ cy.get("#account-menu").contains("Virtual Machines").click();
- cy.get('[data-cy=vm-user-panel]')
- .should('not.contain', vmHost);
+ cy.get("[data-cy=vm-user-panel]").should("not.contain", vmHost);
// Check activeUser's vm page for login
cy.loginAs(activeUser);
cy.get('header button[title="Account Management"]').click();
- cy.get('#account-menu').contains('Virtual Machines').click();
+ cy.get("#account-menu").contains("Virtual Machines").click();
- cy.get('[data-cy=vm-user-panel]')
- .should('not.contain', vmHost);
+ cy.get("[data-cy=vm-user-panel]").should("not.contain", vmHost);
});
});
import { extractFilesData } from "services/collection-service/collection-service-files-response";
-const controllerURL = Cypress.env('controller_url');
-const systemToken = Cypress.env('system_token');
+const controllerURL = Cypress.env("controller_url");
+const systemToken = Cypress.env("system_token");
let createdResources = [];
-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
return;
}
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);
});
Cypress.Commands.add(
- "doRequest", (method = 'GET', path = '', data = null, qs = null,
- token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
- return cy.request({
- method: method,
- url: `${controllerURL.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(
- "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}) => {
+ "doRequest",
+ (method = "GET", path = "", data = null, qs = null, token = systemToken, auth = false, followRedirect = true, failOnStatusCode = true) => {
return cy.request({
method: method,
- url: `${config.Services.WebDAVDownload.ExternalURL.replace(/\/+$/, '')}/${path.replace(/^\/+/, '')}`,
+ url: `${controllerURL.replace(/\/+$/, "")}/${path.replace(/^\/+/, "")}`,
body: data,
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')
+ "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("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(
- "createGroup", (token, data) => {
- return cy.createResource(token, 'groups', {
- group: JSON.stringify(data),
- ensure_unique_name: true
- })
- }
-)
+Cypress.Commands.add("createLink", (token, data) => {
+ return cy.createResource(token, "links", {
+ link: JSON.stringify(data),
+ });
+});
-Cypress.Commands.add(
- "trashGroup", (token, uuid) => {
- return cy.deleteResource(token, 'groups', uuid);
- }
-)
+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("createWorkflow", (token, data) => {
+ return cy.createResource(token, "workflows", {
+ workflow: JSON.stringify(data),
+ ensure_unique_name: true,
+ });
+});
-Cypress.Commands.add(
- "createCollection", (token, data) => {
- return cy.createResource(token, 'collections', {
- collection: JSON.stringify(data),
- ensure_unique_name: true
- })
- }
-)
+Cypress.Commands.add("createCollection", (token, data) => {
+ return cy.createResource(token, "collections", {
+ collection: JSON.stringify(data),
+ ensure_unique_name: true,
+ });
+});
-Cypress.Commands.add(
- "getCollection", (token, uuid) => {
- return cy.getResource(token, 'collections', uuid)
- }
-)
+Cypress.Commands.add("getCollection", (token, uuid) => {
+ return cy.getResource(token, "collections", uuid);
+});
-Cypress.Commands.add(
- "updateCollection", (token, uuid, data) => {
- return cy.updateResource(token, 'collections', uuid, {
- collection: JSON.stringify(data)
- })
- }
-)
+Cypress.Commands.add("updateCollection", (token, uuid, data) => {
+ return cy.updateResource(token, "collections", uuid, {
+ collection: JSON.stringify(data),
+ });
+});
-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("collectionReplaceFiles", (token, uuid, data) => {
+ return cy.updateResource(token, "collections", uuid, {
+ collection: {
+ preserve_version: true,
+ },
+ replace_files: JSON.stringify(data),
+ });
+});
-Cypress.Commands.add(
- "getContainer", (token, uuid) => {
- return cy.getResource(token, 'containers', uuid)
- }
-)
+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("updateContainer", (token, uuid, data) => {
+ return cy.updateResource(token, "containers", uuid, {
+ container: JSON.stringify(data),
+ });
+});
-Cypress.Commands.add(
- "getContainerRequest", (token, uuid) => {
- return cy.getResource(token, 'container_requests', uuid)
- }
-)
+Cypress.Commands.add("getContainerRequest", (token, uuid) => {
+ return cy.getResource(token, "container_requests", uuid);
+});
-Cypress.Commands.add(
- 'createContainerRequest', (token, data) => {
- return cy.createResource(token, 'container_requests', {
- container_request: JSON.stringify(data),
- ensure_unique_name: true
- })
- }
-)
+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("updateContainerRequest", (token, uuid, data) => {
+ return cy.updateResource(token, "container_requests", uuid, {
+ container_request: JSON.stringify(data),
+ });
+});
/**
* Requires an admin token for log_uuid modification to succeed
*/
-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, {
+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,
- manifest_text: ""
- }).then((collection) => {
+ manifest_text: "",
+ })
+ .then(collection => {
// Update CR log_uuid to fake log collection
cy.updateContainerRequest(token, containerRequest.uuid, {
log_uuid: collection.uuid,
- }).then(() => (
+ }).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
+ 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("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(
- "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("createVirtualMachine", (token, data) => {
+ return cy.createResource(token, "virtual_machines", {
+ virtual_machine: JSON.stringify(data),
+ ensure_unique_name: true,
+ });
+});
-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("getResource", (token, suffix, uuid) => {
+ return cy
+ .doRequest("GET", `/arvados/v1/${suffix}/${uuid}`, null, {}, token)
+ .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("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(
- "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("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(
- "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("updateResource", (token, suffix, uuid, data) => {
+ return cy
+ .doRequest("PATCH", "/arvados/v1/" + suffix + "/" + uuid, data, null, token, true)
+ .its("body")
+ .then(function (resource) {
+ return resource;
});
+});
- 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("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");
+});
- if (isProject) {
- cy.get('span[data-text=true]').contains(newDescription);
- } else {
- cy.get('input[name=description]').should('have.value', newDescription);
- }
+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('[data-cy=form-cancel-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(
- "doSearch", (searchTerm) => {
- cy.get('[data-cy=searchbar-input-field]').type(`{selectall}${searchTerm}{enter}`);
- }
-)
+ cy.get("[data-cy=form-cancel-btn]").click();
+ });
+});
-Cypress.Commands.add(
- "goToPath", (path) => {
- return cy.window().its('appHistory').invoke('push', path);
- }
-)
+Cypress.Commands.add("doSearch", searchTerm => {
+ cy.get("[data-cy=searchbar-input-field]").type(`{selectall}${searchTerm}{enter}`);
+});
-Cypress.Commands.add('getAll', (...elements) => {
- const promise = cy.wrap([], { log: false })
+Cypress.Commands.add("goToPath", path => {
+ return cy.window().its("appHistory").invoke("push", path);
+});
+
+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,
});
-})
+});
-Cypress.Commands.add('createProject', ({
- owningUser,
- targetUser,
- projectName,
- canWrite,
- addToFavorites
-}) => {
- const writePermission = canWrite ? 'can_write' : 'can_read';
+Cypress.Commands.add("createProject", ({ owningUser, targetUser, projectName, canWrite, addToFavorites }) => {
+ const writePermission = canWrite ? "can_write" : "can_read";
cy.createGroup(owningUser.token, {
name: `${projectName} ${Math.floor(Math.random() * 999999)}`,
- group_class: 'project',
- }).as(`${projectName}`).then((project) => {
- if (targetUser && targetUser !== owningUser) {
- cy.shareWith(owningUser.token, targetUser.user.uuid, project.uuid, writePermission);
- }
- if (addToFavorites) {
- const user = targetUser ? targetUser : owningUser;
- cy.addToFavorites(user.token, user.user.uuid, project.uuid);
- }
- });
+ group_class: "project",
+ })
+ .as(`${projectName}`)
+ .then(project => {
+ if (targetUser && targetUser !== owningUser) {
+ cy.shareWith(owningUser.token, targetUser.user.uuid, project.uuid, writePermission);
+ }
+ if (addToFavorites) {
+ const user = targetUser ? targetUser : owningUser;
+ cy.addToFavorites(user.token, user.user.uuid, project.uuid);
+ }
+ });
});
Cypress.Commands.add(
- 'upload',
+ "upload",
{
- prevSubject: 'element',
+ prevSubject: "element",
},
(subject, file, fileName, binaryMode = true) => {
cy.window().then(window => {
- const blob = binaryMode
- ? b64toBlob(file, '', 512)
- : new Blob([file], {type: 'text/plain'});
+ const blob = binaryMode ? b64toBlob(file, "", 512) : new Blob([file], { type: "text/plain" });
const testFile = new window.File([blob], fileName);
- cy.wrap(subject).trigger('drop', {
+ cy.wrap(subject).trigger("drop", {
dataTransfer: { files: [testFile] },
});
- })
+ });
}
-)
+);
-function b64toBlob(b64Data, contentType = '', sliceSize = 512) {
- const byteCharacters = atob(b64Data)
- const byteArrays = []
+function b64toBlob(b64Data, contentType = "", sliceSize = 512) {
+ const byteCharacters = atob(b64Data);
+ const byteArrays = [];
for (let offset = 0; offset < byteCharacters.length; offset += sliceSize) {
const slice = byteCharacters.slice(offset, offset + sliceSize);
}
const blob = new Blob(byteArrays, { type: contentType });
- return blob
+ return blob;
}
// From https://github.com/cypress-io/cypress/issues/7306#issuecomment-1076451070=
// This command requires the async package (https://www.npmjs.com/package/async)
-Cypress.Commands.add('waitForDom', () => {
- cy.window().then({
- // Don't timeout before waitForDom finishes
- timeout: 10000
- }, win => {
- let timeElapsed = 0;
-
- cy.log("Waiting for DOM mutations to complete");
-
- return new Cypress.Promise((resolve) => {
- // set the required variables
- let async = require("async");
- let observerConfig = { attributes: true, childList: true, subtree: true };
- let items = Array.apply(null, { length: 50 }).map(Number.call, Number);
- win.mutationCount = 0;
- win.previousMutationCount = null;
-
- // create an observer instance
- let observer = new win.MutationObserver((mutations) => {
- mutations.forEach((mutation) => {
- // Only record "attributes" type mutations that are not a "class" mutation.
- // If the mutation is not an "attributes" type, then we always record it.
- if (mutation.type === 'attributes' && mutation.attributeName !== 'class') {
- win.mutationCount += 1;
- } else if (mutation.type !== 'attributes') {
- win.mutationCount += 1;
- }
- });
+Cypress.Commands.add("waitForDom", () => {
+ cy.window().then(
+ {
+ // Don't timeout before waitForDom finishes
+ timeout: 10000,
+ },
+ win => {
+ let timeElapsed = 0;
+
+ cy.log("Waiting for DOM mutations to complete");
+
+ return new Cypress.Promise(resolve => {
+ // set the required variables
+ let async = require("async");
+ let observerConfig = { attributes: true, childList: true, subtree: true };
+ let items = Array.apply(null, { length: 50 }).map(Number.call, Number);
+ win.mutationCount = 0;
+ win.previousMutationCount = null;
+
+ // create an observer instance
+ let observer = new win.MutationObserver(mutations => {
+ mutations.forEach(mutation => {
+ // Only record "attributes" type mutations that are not a "class" mutation.
+ // If the mutation is not an "attributes" type, then we always record it.
+ if (mutation.type === "attributes" && mutation.attributeName !== "class") {
+ win.mutationCount += 1;
+ } else if (mutation.type !== "attributes") {
+ win.mutationCount += 1;
+ }
+ });
- // initialize the previousMutationCount
- if (win.previousMutationCount == null) win.previousMutationCount = 0;
- });
+ // initialize the previousMutationCount
+ if (win.previousMutationCount == null) win.previousMutationCount = 0;
+ });
- // watch the document body for the specified mutations
- observer.observe(win.document.body, observerConfig);
-
- // check the DOM for mutations up to 50 times for a maximum time of 5 seconds
- async.eachSeries(items, function iteratee(item, callback) {
- // keep track of the elapsed time so we can log it at the end of the command
- timeElapsed = timeElapsed + 100;
-
- // make each iteration of the loop 100ms apart
- setTimeout(() => {
- if (win.mutationCount === win.previousMutationCount) {
- // pass an argument to the async callback to exit the loop
- return callback('Resolved - DOM changes complete.');
- } else if (win.previousMutationCount != null) {
- // only set the previous count if the observer has checked the DOM at least once
- win.previousMutationCount = win.mutationCount;
- return callback();
- } else if (win.mutationCount === 0 && win.previousMutationCount == null && item === 4) {
- // this is an early exit in case nothing is changing in the DOM. That way we only
- // wait 500ms instead of the full 5 seconds when no DOM changes are occurring.
- return callback('Resolved - Exiting early since no DOM changes were detected.');
- } else {
- // proceed to the next iteration
- return callback();
- }
- }, 100);
- }, function done() {
- // Log the total wait time so users can see it
- cy.log(`DOM mutations ${timeElapsed >= 5000 ? "did not complete" : "completed"} in ${timeElapsed} ms`);
-
- // disconnect the observer and resolve the promise
- observer.disconnect();
- resolve();
- });
- });
- });
- });
+ // watch the document body for the specified mutations
+ observer.observe(win.document.body, observerConfig);
+
+ // check the DOM for mutations up to 50 times for a maximum time of 5 seconds
+ async.eachSeries(
+ items,
+ function iteratee(item, callback) {
+ // keep track of the elapsed time so we can log it at the end of the command
+ timeElapsed = timeElapsed + 100;
+
+ // make each iteration of the loop 100ms apart
+ setTimeout(() => {
+ if (win.mutationCount === win.previousMutationCount) {
+ // pass an argument to the async callback to exit the loop
+ return callback("Resolved - DOM changes complete.");
+ } else if (win.previousMutationCount != null) {
+ // only set the previous count if the observer has checked the DOM at least once
+ win.previousMutationCount = win.mutationCount;
+ return callback();
+ } else if (win.mutationCount === 0 && win.previousMutationCount == null && item === 4) {
+ // this is an early exit in case nothing is changing in the DOM. That way we only
+ // wait 500ms instead of the full 5 seconds when no DOM changes are occurring.
+ return callback("Resolved - Exiting early since no DOM changes were detected.");
+ } else {
+ // proceed to the next iteration
+ return callback();
+ }
+ }, 100);
+ },
+ function done() {
+ // Log the total wait time so users can see it
+ cy.log(`DOM mutations ${timeElapsed >= 5000 ? "did not complete" : "completed"} in ${timeElapsed} ms`);
+
+ // disconnect the observer and resolve the promise
+ observer.disconnect();
+ resolve();
+ }
+ );
+ });
+ }
+ );
+});
//
// 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 { 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;
collectionPanel: any;
}
-type CssRules = "backButton"
+type CssRules =
+ | "backButton"
| "backButtonHidden"
| "pathPanelPathWrapper"
| "uploadButton"
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
wrapper: {
- display: 'flex',
- minHeight: '600px',
- color: 'rgba(0,0,0,0.87)',
- fontSize: '0.875rem',
+ display: "flex",
+ minHeight: "600px",
+ color: "rgba(0,0,0,0.87)",
+ fontSize: "0.875rem",
fontFamily: '"Roboto", "Helvetica", "Arial", sans-serif',
fontWeight: 400,
- lineHeight: '1.5',
- letterSpacing: '0.01071em'
+ lineHeight: "1.5",
+ letterSpacing: "0.01071em",
},
backButton: {
- color: '#00bfa5',
- cursor: 'pointer',
- float: 'left',
+ color: "#00bfa5",
+ cursor: "pointer",
+ float: "left",
},
backButtonHidden: {
- display: 'none',
+ display: "none",
},
dataWrapper: {
- minHeight: '500px'
+ minHeight: "500px",
},
row: {
- display: 'flex',
- marginTop: '0.5rem',
- marginBottom: '0.5rem',
- cursor: 'pointer',
+ display: "flex",
+ marginTop: "0.5rem",
+ marginBottom: "0.5rem",
+ cursor: "pointer",
"&:hover": {
- backgroundColor: 'rgba(0, 0, 0, 0.08)',
- }
+ backgroundColor: "rgba(0, 0, 0, 0.08)",
+ },
},
rowEmpty: {
- top: '40%',
- width: '100%',
- textAlign: 'center',
- position: 'absolute'
+ top: "40%",
+ width: "100%",
+ textAlign: "center",
+ position: "absolute",
},
loader: {
- top: '50%',
- left: '50%',
- marginTop: '-15px',
- marginLeft: '-15px',
- position: 'absolute'
+ top: "50%",
+ left: "50%",
+ marginTop: "-15px",
+ marginLeft: "-15px",
+ position: "absolute",
},
rowName: {
- display: 'inline-flex',
- flexDirection: 'column',
- justifyContent: 'center'
+ display: "inline-flex",
+ flexDirection: "column",
+ justifyContent: "center",
},
searchWrapper: {
- display: 'inline-block',
- marginBottom: '1rem',
- marginLeft: '1rem',
+ display: "inline-block",
+ marginBottom: "1rem",
+ marginLeft: "1rem",
},
searchWrapperHidden: {
- width: '0px'
+ width: "0px",
},
rowSelection: {
- padding: '0px',
+ padding: "0px",
},
rowActive: {
color: `${theme.palette.primary.main} !important`,
},
listItemIcon: {
- display: 'inline-flex',
- flexDirection: 'column',
- justifyContent: 'center'
+ display: "inline-flex",
+ flexDirection: "column",
+ justifyContent: "center",
},
pathPanelMenu: {
- float: 'right',
- marginTop: '-15px',
+ float: "right",
+ marginTop: "-15px",
},
pathPanel: {
- padding: '0.5rem',
- marginBottom: '0.5rem',
- backgroundColor: '#fff',
- boxShadow: '0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)',
+ padding: "0.5rem",
+ marginBottom: "0.5rem",
+ backgroundColor: "#fff",
+ boxShadow: "0px 1px 3px 0px rgb(0 0 0 / 20%), 0px 1px 1px 0px rgb(0 0 0 / 14%), 0px 2px 1px -1px rgb(0 0 0 / 12%)",
},
pathPanelPathWrapper: {
- display: 'inline-block',
+ display: "inline-block",
},
leftPanel: {
flex: 0,
- padding: '0 1rem 1rem',
- marginRight: '1rem',
- whiteSpace: 'nowrap',
- position: 'relative',
- backgroundColor: '#fff',
- boxShadow: '0px 3px 3px 0px rgb(0 0 0 / 20%), 0px 3px 1px 0px rgb(0 0 0 / 14%), 0px 3px 1px -1px rgb(0 0 0 / 12%)',
+ padding: "0 1rem 1rem",
+ marginRight: "1rem",
+ whiteSpace: "nowrap",
+ position: "relative",
+ backgroundColor: "#fff",
+ boxShadow: "0px 3px 3px 0px rgb(0 0 0 / 20%), 0px 3px 1px 0px rgb(0 0 0 / 14%), 0px 3px 1px -1px rgb(0 0 0 / 12%)",
},
leftPanelVisible: {
opacity: 1,
- flex: '50%',
- animation: `animateVisible 1000ms ${theme.transitions.easing.easeOut}`
+ flex: "50%",
+ animation: `animateVisible 1000ms ${theme.transitions.easing.easeOut}`,
},
leftPanelHidden: {
opacity: 0,
- flex: 'initial',
- padding: '0',
- marginRight: '0',
+ flex: "initial",
+ padding: "0",
+ marginRight: "0",
},
"@keyframes animateVisible": {
"0%": {
opacity: 0,
- flex: 'initial',
+ flex: "initial",
},
"100%": {
opacity: 1,
- flex: '50%',
- }
+ flex: "50%",
+ },
},
rightPanel: {
- flex: '50%',
- padding: '1rem',
- paddingTop: '0.5rem',
- marginTop: '-0.5rem',
- position: 'relative',
- backgroundColor: '#fff',
- boxShadow: '0px 3px 3px 0px rgb(0 0 0 / 20%), 0px 3px 1px 0px rgb(0 0 0 / 14%), 0px 3px 1px -1px rgb(0 0 0 / 12%)',
+ flex: "50%",
+ padding: "1rem",
+ paddingTop: "0.5rem",
+ marginTop: "-0.5rem",
+ position: "relative",
+ backgroundColor: "#fff",
+ boxShadow: "0px 3px 3px 0px rgb(0 0 0 / 20%), 0px 3px 1px 0px rgb(0 0 0 / 14%), 0px 3px 1px -1px rgb(0 0 0 / 12%)",
},
pathPanelItem: {
- cursor: 'pointer',
+ cursor: "pointer",
},
uploadIcon: {
- transform: 'rotate(180deg)'
+ transform: "rotate(180deg)",
},
uploadButton: {
- float: 'right',
+ float: "right",
},
moreOptionsButton: {
width: theme.spacing.unit * 3,
height: theme.spacing.unit * 3,
marginRight: theme.spacing.unit,
- marginTop: 'auto',
- marginBottom: 'auto',
- justifyContent: 'center',
+ marginTop: "auto",
+ marginBottom: "auto",
+ justifyContent: "center",
},
moreOptions: {
- position: 'absolute'
+ position: "absolute",
},
});
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 && 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, 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>
- </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}
+ >
+ <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>
+ </span>
+ ))}
+ </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>
- {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}
- />
- {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"
+ >
+ <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}
+ />
+ </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}
+ />
+
+ {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>
+ </div>
</div>
</div>
- </div>
- </div>
-}));
+ );
+ })
+);
//
// SPDX-License-Identifier: AGPL-3.0
-import React from "react";
-import { connect, DispatchProp } from "react-redux";
-import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from "@material-ui/core";
+import React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { StyleRulesCallback, Tooltip, WithStyles, withStyles } from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
import CopyToClipboard from 'react-copy-to-clipboard';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
type CssRules = 'copyIcon';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
- copyIcon: {
- marginLeft: theme.spacing.unit,
- color: theme.palette.grey["500"],
- cursor: 'pointer',
- display: 'inline',
- '& svg': {
- fontSize: '1rem',
- verticalAlign: 'middle',
- }
- }
+ copyIcon: {
+ marginLeft: theme.spacing.unit,
+ color: theme.palette.grey['500'],
+ cursor: 'pointer',
+ display: 'inline',
+ '& svg': {
+ fontSize: '1rem',
+ verticalAlign: 'middle',
+ },
+ },
});
interface CopyToClipboardDataProps {
- children?: React.ReactNode;
- value: string;
+ children?: React.ReactNode;
+ value: string;
}
type CopyToClipboardProps = CopyToClipboardDataProps & WithStyles<CssRules> & DispatchProp;
-export const CopyToClipboardSnackbar = connect()(withStyles(styles)(
- class CopyToClipboardSnackbar extends React.Component<CopyToClipboardProps> {
- onCopy = () => {
- this.props.dispatch(snackbarActions.OPEN_SNACKBAR({
- message: 'Copied',
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
- };
+export const CopyToClipboardSnackbar = connect()(
+ withStyles(styles)(
+ class CopyToClipboardSnackbar extends React.Component<CopyToClipboardProps> {
+ onCopy = () => {
+ this.props.dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: 'Copied',
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ };
- render() {
- const { children, value, classes } = this.props;
- return (
- <Tooltip title="Copy to clipboard">
- <span className={classes.copyIcon}>
- <CopyToClipboard text={value} onCopy={this.onCopy}>
- {children || <CopyIcon />}
- </CopyToClipboard>
- </span>
- </Tooltip>
- );
- }
- }
-));
+ render() {
+ const { children, value, classes } = this.props;
+ return (
+ <Tooltip title='Copy to clipboard'>
+ <span className={classes.copyIcon}>
+ <CopyToClipboard text={value} onCopy={this.onCopy}>
+ {children || <CopyIcon />}
+ </CopyToClipboard>
+ </span>
+ </Tooltip>
+ );
+ }
+ }
+ )
+);
import React from "react";
import { configure, mount } from "enzyme";
-import Adapter from 'enzyme-adapter-react-16';
+import Adapter from "enzyme-adapter-react-16";
import { DataExplorer } from "./data-explorer";
import { ColumnSelector } from "../column-selector/column-selector";
import { DataTable, DataTableFetchMode } from "../data-table/data-table";
import { SearchInput } from "../search-input/search-input";
import { TablePagination } from "@material-ui/core";
-import { ProjectIcon } from '../icon/icon';
-import { SortDirection } from '../data-table/data-column';
+import { ProjectIcon } from "../icon/icon";
+import { SortDirection } from "../data-table/data-column";
+import { combineReducers, createStore } from "redux";
+import { Provider } from "react-redux";
configure({ adapter: new Adapter() });
describe("<DataExplorer />", () => {
+ let store;
+ beforeEach(() => {
+ const initialMSState = {
+ multiselect: {
+ checkedList: {},
+ isVisible: false,
+ },
+ resources: {},
+ };
+ store = createStore(
+ combineReducers({
+ multiselect: (state: any = initialMSState.multiselect, action: any) => state,
+ resources: (state: any = initialMSState.resources, action: any) => state,
+ })
+ );
+ });
it("communicates with <SearchInput/>", () => {
const onSearch = jest.fn();
const onSetColumns = jest.fn();
- const dataExplorer = mount(<DataExplorer
- {...mockDataExplorerProps()}
- items={[{ name: "item 1" }]}
- searchValue="search value"
- onSearch={onSearch}
- onSetColumns={onSetColumns} />);
+
+ const dataExplorer = mount(
+ <Provider store={store}>
+ <DataExplorer
+ {...mockDataExplorerProps()}
+ items={[{ name: "item 1" }]}
+ searchValue="search value"
+ onSearch={onSearch}
+ onSetColumns={onSetColumns}
+ />
+ </Provider>
+ );
expect(dataExplorer.find(SearchInput).prop("value")).toEqual("search value");
dataExplorer.find(SearchInput).prop("onSearch")("new value");
expect(onSearch).toHaveBeenCalledWith("new value");
const onColumnToggle = jest.fn();
const onSetColumns = jest.fn();
const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: {} }];
- const dataExplorer = mount(<DataExplorer
- {...mockDataExplorerProps()}
- columns={columns}
- onColumnToggle={onColumnToggle}
- items={[{ name: "item 1" }]}
- onSetColumns={onSetColumns} />);
+ const dataExplorer = mount(
+ <Provider store={store}>
+ <DataExplorer
+ {...mockDataExplorerProps()}
+ columns={columns}
+ onColumnToggle={onColumnToggle}
+ items={[{ name: "item 1" }]}
+ onSetColumns={onSetColumns}
+ />
+ </Provider>
+ );
expect(dataExplorer.find(ColumnSelector).prop("columns")).toBe(columns);
dataExplorer.find(ColumnSelector).prop("onColumnToggle")("columns");
expect(onColumnToggle).toHaveBeenCalledWith("columns");
const onSetColumns = jest.fn();
const columns = [{ name: "Column 1", render: jest.fn(), selected: true, configurable: true, sortDirection: SortDirection.ASC, filters: {} }];
const items = [{ name: "item 1" }];
- const dataExplorer = mount(<DataExplorer
- {...mockDataExplorerProps()}
- columns={columns}
- items={items}
- onFiltersChange={onFiltersChange}
- onSortToggle={onSortToggle}
- onRowClick={onRowClick}
- onSetColumns={onSetColumns} />);
- expect(dataExplorer.find(DataTable).prop("columns").slice(0, -1)).toEqual(columns);
+ const dataExplorer = mount(
+ <Provider store={store}>
+ <DataExplorer
+ {...mockDataExplorerProps()}
+ columns={columns}
+ items={items}
+ onFiltersChange={onFiltersChange}
+ onSortToggle={onSortToggle}
+ onRowClick={onRowClick}
+ onSetColumns={onSetColumns}
+ />
+ </Provider>
+ );
+ expect(dataExplorer.find(DataTable).prop("columns").slice(1, 2)).toEqual(columns);
expect(dataExplorer.find(DataTable).prop("items")).toBe(items);
dataExplorer.find(DataTable).prop("onRowClick")("event", "rowClick");
dataExplorer.find(DataTable).prop("onFiltersChange")("filtersChange");
const onChangePage = jest.fn();
const onChangeRowsPerPage = jest.fn();
const onSetColumns = jest.fn();
- const dataExplorer = mount(<DataExplorer
- {...mockDataExplorerProps()}
- items={[{ name: "item 1" }]}
- page={10}
- rowsPerPage={50}
- onChangePage={onChangePage}
- onChangeRowsPerPage={onChangeRowsPerPage}
- onSetColumns={onSetColumns} />);
+ const dataExplorer = mount(
+ <Provider store={store}>
+ <DataExplorer
+ {...mockDataExplorerProps()}
+ items={[{ name: "item 1" }]}
+ page={10}
+ rowsPerPage={50}
+ onChangePage={onChangePage}
+ onChangeRowsPerPage={onChangeRowsPerPage}
+ onSetColumns={onSetColumns}
+ />
+ </Provider>
+ );
expect(dataExplorer.find(TablePagination).prop("page")).toEqual(10);
expect(dataExplorer.find(TablePagination).prop("rowsPerPage")).toEqual(50);
dataExplorer.find(TablePagination).prop("onChangePage")(undefined, 6);
defaultIcon: ProjectIcon,
onSetColumns: jest.fn(),
onLoadMore: jest.fn(),
- defaultMessages: ['testing'],
- contextMenuColumn: true
+ defaultMessages: ["testing"],
+ contextMenuColumn: true,
+ setCheckedListOnStore: jest.fn(),
+ toggleMSToolbar: jest.fn(),
+ isMSToolbarVisible: false,
+ checkedList: {},
});
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from '@material-ui/core';
+import React from "react";
+import { Grid, Paper, Toolbar, StyleRulesCallback, withStyles, WithStyles, TablePagination, IconButton, Tooltip, Button } from "@material-ui/core";
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, MoreVerticalIcon } from "components/icon/icon";
+import { PaperProps } from "@material-ui/core/Paper";
+import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
-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: {
paddingRight: theme.spacing.unit,
},
footer: {
- overflow: 'auto'
+ overflow: "auto",
},
root: {
- height: '100%',
+ height: "100%",
},
moreOptionsButton: {
- padding: 0
+ padding: 0,
},
title: {
- display: 'inline-block',
+ display: "inline-block",
paddingLeft: theme.spacing.unit * 2,
paddingTop: theme.spacing.unit * 2,
- fontSize: '18px'
+ fontSize: "18px",
},
dataTable: {
- height: '100%',
- overflow: 'auto',
+ height: "100%",
+ overflow: "auto",
},
container: {
- height: '100%',
+ height: "100%",
},
headerMenu: {
- float: 'right',
- display: 'inline-block',
- }
+ width: "100%",
+ float: "right",
+ display: "flex",
+ flexDirection: "row-reverse",
+ justifyContent: "space-between",
+ },
});
interface DataExplorerDataProps<T> {
paperKey?: string;
currentItemUuid: string;
elementPath?: string;
+ isMSToolbarVisible: boolean;
+ checkedList: TCheckedList;
}
interface DataExplorerActionProps<T> {
onChangeRowsPerPage: (rowsPerPage: number) => void;
onLoadMore: (page: number) => void;
extractKey?: (item: T) => React.Key;
+ toggleMSToolbar: (isVisible: boolean) => void;
+ setCheckedListOnStore: (checkedList: TCheckedList) => void;
}
-type DataExplorerProps<T> = DataExplorerDataProps<T> &
- DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
+type DataExplorerProps<T> = DataExplorerDataProps<T> & DataExplorerActionProps<T> & WithStyles<CssRules> & MPVPanelProps;
export const DataExplorer = withStyles(styles)(
class DataExplorerGeneric<T> extends React.Component<DataExplorerProps<T>> {
state = {
showLoading: false,
- prevRefresh: '',
- prevRoute: '',
+ prevRefresh: "",
+ prevRoute: "",
};
componentDidUpdate(prevProps: DataExplorerProps<T>) {
- const currentRefresh = this.props.currentRefresh || '';
- const currentRoute = this.props.currentRoute || '';
+ const currentRefresh = this.props.currentRefresh || "";
+ const currentRoute = this.props.currentRoute || "";
if (currentRoute !== this.state.prevRoute) {
// Component already mounted, but the user comes from a route change,
// Component just mounted, so we need to show the loading indicator.
this.setState({
showLoading: this.props.working,
- prevRefresh: this.props.currentRefresh || '',
- prevRoute: this.props.currentRoute || '',
+ prevRefresh: this.props.currentRefresh || "",
+ prevRoute: this.props.currentRoute || "",
});
}
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"]}
+ >
+ <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>
+ <MultiselectToolbar />
+ </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}
+ toggleMSToolbar={toggleMSToolbar}
+ setCheckedListOnStore={setCheckedListOnStore}
+ checkedList={checkedList}
+ />
+ </Grid>
+ <Grid
+ item
+ xs
+ >
+ <Toolbar className={classes.footer}>
+ {elementPath && (
+ <Grid container>
+ <span data-cy="element-path">{elementPath}</span>
+ </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 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) => {
this.props.onChangePage(page);
- }
+ };
- changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = (event) => {
+ changeRowsPerPage: React.ChangeEventHandler<HTMLTextAreaElement | HTMLInputElement> = event => {
this.props.onChangeRowsPerPage(parseInt(event.target.value, 10));
- }
+ };
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"
+ >
+ <Tooltip
+ title="More options"
+ disableFocusListener
+ >
+ <IconButton
+ className={this.props.classes.moreOptionsButton}
+ onClick={event => this.props.onContextMenu(event, item)}
+ >
<MoreVerticalIcon />
</IconButton>
</Tooltip>
</Grid>
+ );
contextMenuColumn: DataColumn<any, any> = {
name: "Actions",
configurable: false,
filters: createTree(),
key: "context-actions",
- render: this.renderContextMenuTrigger
+ render: this.renderContextMenuTrigger,
};
}
);
//
// SPDX-License-Identifier: AGPL-3.0
-import React, { useEffect } from "react";
+import React, { useEffect } from 'react';
import {
WithStyles,
withStyles,
Typography,
CardContent,
Tooltip,
- IconButton
-} from "@material-ui/core";
-import classnames from "classnames";
-import { DefaultTransformOrigin } from "components/popover/helpers";
+ IconButton,
+} from '@material-ui/core';
+import classnames from 'classnames';
+import { DefaultTransformOrigin } from 'components/popover/helpers';
import { createTree } from 'models/tree';
-import { DataTableFilters, DataTableFiltersTree } from "./data-table-filters-tree";
+import { DataTableFilters, DataTableFiltersTree } from './data-table-filters-tree';
import { getNodeDescendants } from 'models/tree';
-import debounce from "lodash/debounce";
+import debounce from 'lodash/debounce';
-export type CssRules = "root" | "icon" | "iconButton" | "active" | "checkbox";
+export type CssRules = 'root' | 'icon' | 'iconButton' | 'active' | 'checkbox';
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
root: {
- cursor: "pointer",
- display: "inline-flex",
- justifyContent: "flex-start",
- flexDirection: "inherit",
- alignItems: "center",
- "&:hover": {
+ cursor: 'pointer',
+ display: 'inline-flex',
+ justifyContent: 'flex-start',
+ flexDirection: 'inherit',
+ alignItems: 'center',
+ '&:hover': {
color: theme.palette.text.primary,
},
- "&:focus": {
+ '&:focus': {
color: theme.palette.text.primary,
},
},
userSelect: 'none',
width: 16,
height: 15,
- marginTop: 1
+ marginTop: 1,
},
iconButton: {
color: theme.palette.text.primary,
},
checkbox: {
width: 24,
- height: 24
- }
+ height: 24,
+ },
});
enum SelectionMode {
ALL = 'all',
- NONE = 'none'
+ NONE = 'none',
}
export interface DataTableFilterProps {
render() {
const { name, classes, defaultSelection = SelectionMode.ALL, children } = this.props;
- const isActive = getNodeDescendants('')(this.state.filters)
- .some(f => defaultSelection === SelectionMode.ALL
- ? !f.selected
- : f.selected
- );
- return <>
- <Tooltip disableFocusListener title='Filters'>
- <ButtonBase
- className={classnames([classes.root, { [classes.active]: isActive }])}
- component="span"
- onClick={this.open}
- disableRipple>
- {children}
- <IconButton component='span' classes={{ root: classes.iconButton }} tabIndex={-1}>
- <i className={classnames(["fas fa-filter", classes.icon])}
- data-fa-transform="shrink-3"
- ref={this.icon} />
- </IconButton>
- </ButtonBase>
- </Tooltip>
- <Popover
- anchorEl={this.state.anchorEl}
- open={!!this.state.anchorEl}
- anchorOrigin={DefaultTransformOrigin}
- transformOrigin={DefaultTransformOrigin}
- onClose={this.close}>
- <Card>
- <CardContent>
- <Typography variant="caption">
- {name}
- </Typography>
- </CardContent>
- <DataTableFiltersTree
- filters={this.state.filters}
- mutuallyExclusive={this.props.mutuallyExclusive}
- onChange={this.onChange} />
- {this.props.mutuallyExclusive ||
- <CardActions>
- <Button
- color="primary"
- variant="outlined"
- size="small"
- onClick={this.close}>
- Close
- </Button>
- </CardActions >
- }
- </Card>
- </Popover>
- <this.MountHandler />
- </>;
+ const isActive = getNodeDescendants('')(this.state.filters).some((f) => (defaultSelection === SelectionMode.ALL ? !f.selected : f.selected));
+ return (
+ <>
+ <Tooltip disableFocusListener title='Filters'>
+ <ButtonBase className={classnames([classes.root, { [classes.active]: isActive }])} component='span' onClick={this.open} disableRipple>
+ {children}
+ <IconButton component='span' classes={{ root: classes.iconButton }} tabIndex={-1}>
+ <i className={classnames(['fas fa-filter', classes.icon])} data-fa-transform='shrink-3' ref={this.icon} />
+ </IconButton>
+ </ButtonBase>
+ </Tooltip>
+ <Popover
+ anchorEl={this.state.anchorEl}
+ open={!!this.state.anchorEl}
+ anchorOrigin={DefaultTransformOrigin}
+ transformOrigin={DefaultTransformOrigin}
+ onClose={this.close}
+ >
+ <Card>
+ <CardContent>
+ <Typography variant='caption'>{name}</Typography>
+ </CardContent>
+ <DataTableFiltersTree filters={this.state.filters} mutuallyExclusive={this.props.mutuallyExclusive} onChange={this.onChange} />
+ <>
+ {this.props.mutuallyExclusive || (
+ <CardActions>
+ <Button color='primary' variant='outlined' size='small' onClick={this.close}>
+ Close
+ </Button>
+ </CardActions>
+ )}
+ </>
+ </Card>
+ </Popover>
+ <this.MountHandler />
+ </>
+ );
}
static getDerivedStateFromProps(props: DataTableFilterProps, state: DataTableFilterState): DataTableFilterState {
- return props.filters !== state.prevFilters
- ? { ...state, filters: props.filters, prevFilters: props.filters }
- : state;
+ return props.filters !== state.prevFilters ? { ...state, filters: props.filters, prevFilters: props.filters } : state;
}
open = () => {
this.setState({ anchorEl: this.icon.current || undefined });
- }
+ };
onChange = (filters) => {
this.setState({ filters });
// Non-mutually exclusive filters are debounced
this.submit();
}
- }
+ };
- submit = debounce (() => {
+ submit = debounce(() => {
const { onChange } = this.props;
if (onChange) {
onChange(this.state.filters);
useEffect(() => {
return () => {
this.submit.cancel();
- }
- },[]);
+ };
+ }, []);
return null;
};
close = () => {
- this.setState(prev => ({
+ this.setState((prev) => ({
...prev,
- anchorEl: undefined
+ anchorEl: undefined,
}));
- }
-
+ };
}
);
if (item.selected) { return; }
// Otherwise select this node and deselect the others
- const filters = selectNode(item.id)(this.props.filters);
+ const filters = selectNode(item.id, true)(this.props.filters);
const toDeselect = Object.keys(this.props.filters).filter((id) => (id !== item.id));
- onChange(deselectNodes(toDeselect)(filters));
+ onChange(deselectNodes(toDeselect, true)(filters));
}
toggleFilter = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
const { onChange = noop } = this.props;
- onChange(toggleNodeSelection(item.id)(this.props.filters));
+ onChange(toggleNodeSelection(item.id, true)(this.props.filters));
}
toggleOpen = (_: React.MouseEvent, item: TreeItem<DataTableFilterItem>) => {
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { WithStyles, withStyles, ButtonBase, StyleRulesCallback, Theme, Popover, Card, Tooltip, IconButton } from "@material-ui/core";
+import classnames from "classnames";
+import { DefaultTransformOrigin } from "components/popover/helpers";
+import { grey } from "@material-ui/core/colors";
+import { TCheckedList } from "components/data-table/data-table";
+
+export type CssRules = "root" | "icon" | "iconButton" | "disabled" | "optionsContainer" | "option";
+
+const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
+ root: {
+ borderRadius: "7px",
+ "&:hover": {
+ backgroundColor: grey[200],
+ },
+ "&:focus": {
+ color: theme.palette.text.primary,
+ },
+ },
+ icon: {
+ cursor: "pointer",
+ fontSize: 20,
+ userSelect: "none",
+ "&:hover": {
+ color: theme.palette.text.primary,
+ },
+ paddingBottom: "5px",
+ },
+ iconButton: {
+ color: theme.palette.text.primary,
+ opacity: 0.6,
+ padding: 1,
+ paddingBottom: 5,
+ },
+ disabled: {
+ color: grey[500],
+ },
+ optionsContainer: {
+ padding: "1rem 0",
+ flex: 1,
+ },
+ option: {
+ cursor: "pointer",
+ display: "flex",
+ padding: "3px 2rem",
+ fontSize: "0.9rem",
+ alignItems: "center",
+ "&:hover": {
+ backgroundColor: "rgba(0, 0, 0, 0.08)",
+ },
+ },
+});
+
+export type DataTableMultiselectOption = {
+ name: string;
+ fn: (checkedList) => void;
+};
+
+export interface DataTableMultiselectProps {
+ name: string;
+ disabled: boolean;
+ options: DataTableMultiselectOption[];
+ checkedList: TCheckedList;
+}
+
+interface DataTableFMultiselectPopState {
+ anchorEl?: HTMLElement;
+}
+
+export const DataTableMultiselectPopover = withStyles(styles)(
+ class extends React.Component<DataTableMultiselectProps & WithStyles<CssRules>, DataTableFMultiselectPopState> {
+ state: DataTableFMultiselectPopState = {
+ anchorEl: undefined,
+ };
+ icon = React.createRef<HTMLElement>();
+
+ render() {
+ const { classes, children, options, checkedList, disabled } = this.props;
+ return (
+ <>
+ <Tooltip
+ disableFocusListener
+ title="Select Options"
+ >
+ <ButtonBase
+ className={classnames(classes.root)}
+ component="span"
+ onClick={disabled ? () => {} : this.open}
+ disableRipple
+ >
+ {children}
+ <IconButton
+ component="span"
+ classes={{ root: classes.iconButton }}
+ tabIndex={-1}
+ >
+ <i
+ className={`${classnames(["fas fa-sort-down", classes.icon])}${disabled ? ` ${classes.disabled}` : ""}`}
+ data-fa-transform="shrink-3"
+ ref={this.icon}
+ />
+ </IconButton>
+ </ButtonBase>
+ </Tooltip>
+ <Popover
+ anchorEl={this.state.anchorEl}
+ open={!!this.state.anchorEl}
+ anchorOrigin={DefaultTransformOrigin}
+ transformOrigin={DefaultTransformOrigin}
+ onClose={this.close}
+ >
+ <Card>
+ <div className={classes.optionsContainer}>
+ {options.length &&
+ options.map((option, i) => (
+ <div
+ key={i}
+ className={classes.option}
+ onClick={() => {
+ option.fn(checkedList);
+ this.close();
+ }}
+ >
+ {option.name}
+ </div>
+ ))}
+ </div>
+ </Card>
+ </Popover>
+ </>
+ );
+ }
+
+ open = () => {
+ this.setState({ anchorEl: this.icon.current || undefined });
+ };
+
+ close = () => {
+ this.setState(prev => ({
+ ...prev,
+ anchorEl: undefined,
+ }));
+ };
+ }
+);
import React from "react";
import { mount, configure } from "enzyme";
-import { pipe } from 'lodash/fp';
+import { pipe } from "lodash/fp";
import { TableHead, TableCell, Typography, TableBody, Button, TableSortLabel } from "@material-ui/core";
import Adapter from "enzyme-adapter-react-16";
import { DataTable, DataColumns } from "./data-table";
import { SortDirection, createDataColumn } from "./data-column";
-import { DataTableFiltersPopover } from 'components/data-table-filters/data-table-filters-popover';
-import { createTree, setNode, initTreeNode } from 'models/tree';
+import { DataTableFiltersPopover } from "components/data-table-filters/data-table-filters-popover";
+import { createTree, setNode, initTreeNode } from "models/tree";
import { DataTableFilterItem } from "components/data-table-filters/data-table-filters-tree";
configure({ adapter: new Adapter() });
name: "Column 1",
render: () => <span />,
selected: true,
- configurable: true
+ configurable: true,
}),
createDataColumn({
name: "Column 2",
render: () => <span />,
selected: true,
- configurable: true
+ configurable: true,
}),
createDataColumn({
name: "Column 3",
render: () => <span />,
selected: false,
- configurable: true
+ configurable: true,
}),
];
- const dataTable = mount(<DataTable
- columns={columns}
- items={[{ key: "1", name: "item 1" }]}
- onFiltersChange={jest.fn()}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onContextMenu={jest.fn()}
- onSortToggle={jest.fn()} />);
- expect(dataTable.find(TableHead).find(TableCell)).toHaveLength(2);
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ items={[{ key: "1", name: "item 1" }]}
+ onFiltersChange={jest.fn()}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSortToggle={jest.fn()}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
+ expect(dataTable.find(TableHead).find(TableCell)).toHaveLength(3);
});
it("renders column name", () => {
name: "Column 1",
render: () => <span />,
selected: true,
- configurable: true
+ configurable: true,
}),
];
- const dataTable = mount(<DataTable
- columns={columns}
- items={["item 1"]}
- onFiltersChange={jest.fn()}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onContextMenu={jest.fn()}
- onSortToggle={jest.fn()} />);
- expect(dataTable.find(TableHead).find(TableCell).text()).toBe("Column 1");
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ items={["item 1"]}
+ onFiltersChange={jest.fn()}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSortToggle={jest.fn()}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
+ expect(dataTable.find(TableHead).find(TableCell).last().text()).toBe("Column 1");
});
it("uses renderHeader instead of name prop", () => {
renderHeader: () => <span>Column Header</span>,
render: () => <span />,
selected: true,
- configurable: true
+ configurable: true,
}),
];
- const dataTable = mount(<DataTable
- columns={columns}
- items={[]}
- onFiltersChange={jest.fn()}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onContextMenu={jest.fn()}
- onSortToggle={jest.fn()} />);
- expect(dataTable.find(TableHead).find(TableCell).text()).toBe("Column Header");
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ items={[]}
+ onFiltersChange={jest.fn()}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSortToggle={jest.fn()}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
+ expect(dataTable.find(TableHead).find(TableCell).last().text()).toBe("Column Header");
});
it("passes column key prop to corresponding cells", () => {
key: "column-1-key",
render: () => <span />,
selected: true,
- configurable: true
- })
+ configurable: true,
+ }),
];
- const dataTable = mount(<DataTable
- columns={columns}
- working={false}
- items={["item 1"]}
- onFiltersChange={jest.fn()}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onContextMenu={jest.fn()}
- onSortToggle={jest.fn()} />);
- expect(dataTable.find(TableHead).find(TableCell).key()).toBe("column-1-key");
- expect(dataTable.find(TableBody).find(TableCell).key()).toBe("column-1-key");
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ working={false}
+ items={["item 1"]}
+ onFiltersChange={jest.fn()}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSortToggle={jest.fn()}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
+ expect(dataTable.find(TableBody).find(TableCell).last().key()).toBe("column-1-key");
});
it("renders items", () => {
const columns: DataColumns<string, string> = [
createDataColumn({
name: "Column 1",
- render: (item) => <Typography>{item}</Typography>,
+ render: item => <Typography>{item}</Typography>,
selected: true,
- configurable: true
+ configurable: true,
}),
createDataColumn({
name: "Column 2",
- render: (item) => <Button>{item}</Button>,
+ render: item => <Button>{item}</Button>,
selected: true,
- configurable: true
- })
+ configurable: true,
+ }),
];
- const dataTable = mount(<DataTable
- columns={columns}
- working={false}
- items={["item 1"]}
- onFiltersChange={jest.fn()}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onContextMenu={jest.fn()}
- onSortToggle={jest.fn()} />);
- expect(dataTable.find(TableBody).find(Typography).text()).toBe("item 1");
- expect(dataTable.find(TableBody).find(Button).text()).toBe("item 1");
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ working={false}
+ items={["item 1"]}
+ onFiltersChange={jest.fn()}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSortToggle={jest.fn()}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
+ expect(dataTable.find(TableBody).find(Typography).last().text()).toBe("item 1");
+ expect(dataTable.find(TableBody).find(Button).last().text()).toBe("item 1");
});
it("passes sorting props to <TableSortLabel />", () => {
const columns: DataColumns<string, string> = [
createDataColumn({
name: "Column 1",
- sort: {direction: SortDirection.ASC, field: "length"},
+ sort: { direction: SortDirection.ASC, field: "length" },
selected: true,
configurable: true,
- render: (item) => <Typography>{item}</Typography>
- })];
+ render: item => <Typography>{item}</Typography>,
+ }),
+ ];
const onSortToggle = jest.fn();
- const dataTable = mount(<DataTable
- columns={columns}
- items={["item 1"]}
- onFiltersChange={jest.fn()}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onContextMenu={jest.fn()}
- onSortToggle={onSortToggle} />);
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ items={["item 1"]}
+ onFiltersChange={jest.fn()}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onContextMenu={jest.fn()}
+ onSortToggle={onSortToggle}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
expect(dataTable.find(TableSortLabel).prop("active")).toBeTruthy();
dataTable.find(TableSortLabel).at(0).simulate("click");
- expect(onSortToggle).toHaveBeenCalledWith(columns[0]);
+ expect(onSortToggle).toHaveBeenCalledWith(columns[1]);
});
it("does not display <DataTableFiltersPopover /> if there is no filters provided", () => {
- const columns: DataColumns<string, string> = [{
- name: "Column 1",
- selected: true,
- configurable: true,
- filters: [],
- render: (item) => <Typography>{item}</Typography>
- }];
+ const columns: DataColumns<string, string> = [
+ {
+ name: "Column 1",
+ selected: true,
+ configurable: true,
+ filters: [],
+ render: item => <Typography>{item}</Typography>,
+ },
+ ];
const onFiltersChange = jest.fn();
- const dataTable = mount(<DataTable
- columns={columns}
- items={[]}
- onFiltersChange={onFiltersChange}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onSortToggle={jest.fn()}
- onContextMenu={jest.fn()} />);
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ items={[]}
+ onFiltersChange={onFiltersChange}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onSortToggle={jest.fn()}
+ onContextMenu={jest.fn()}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
expect(dataTable.find(DataTableFiltersPopover)).toHaveLength(0);
});
it("passes filter props to <DataTableFiltersPopover />", () => {
- const filters = pipe(
- () => createTree<DataTableFilterItem>(),
- setNode(initTreeNode({ id: 'filter', value: { name: 'filter' } }))
- );
- const columns: DataColumns<string, string> = [{
- name: "Column 1",
- selected: true,
- configurable: true,
- filters: filters(),
- render: (item) => <Typography>{item}</Typography>
- }];
+ const filters = pipe(() => createTree<DataTableFilterItem>(), setNode(initTreeNode({ id: "filter", value: { name: "filter" } })));
+ const columns: DataColumns<string, string> = [
+ {
+ name: "Column 1",
+ selected: true,
+ configurable: true,
+ filters: filters(),
+ render: item => <Typography>{item}</Typography>,
+ },
+ ];
const onFiltersChange = jest.fn();
- const dataTable = mount(<DataTable
- columns={columns}
- items={[]}
- onFiltersChange={onFiltersChange}
- onRowClick={jest.fn()}
- onRowDoubleClick={jest.fn()}
- onSortToggle={jest.fn()}
- onContextMenu={jest.fn()} />);
- expect(dataTable.find(DataTableFiltersPopover).prop("filters")).toBe(columns[0].filters);
+ const dataTable = mount(
+ <DataTable
+ columns={columns}
+ items={[]}
+ onFiltersChange={onFiltersChange}
+ onRowClick={jest.fn()}
+ onRowDoubleClick={jest.fn()}
+ onSortToggle={jest.fn()}
+ onContextMenu={jest.fn()}
+ setCheckedListOnStore={jest.fn()}
+ />
+ );
+ expect(dataTable.find(DataTableFiltersPopover).prop("filters")).toBe(columns[1].filters);
dataTable.find(DataTableFiltersPopover).prop("onChange")([]);
- expect(onFiltersChange).toHaveBeenCalledWith([], columns[0]);
+ expect(onFiltersChange).toHaveBeenCalledWith([], columns[1]);
});
});
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { Table, TableBody, TableRow, TableCell, TableHead, TableSortLabel, StyleRulesCallback, Theme, WithStyles, withStyles, IconButton } from '@material-ui/core';
-import classnames from 'classnames';
-import { DataColumn, SortDirection } from './data-column';
-import { DataTableDefaultView } from '../data-table-default-view/data-table-default-view';
-import { DataTableFilters } from '../data-table-filters/data-table-filters-tree';
-import { DataTableFiltersPopover } from '../data-table-filters/data-table-filters-popover';
-import { countNodes, getTreeDirty } from 'models/tree';
-import { IconType, PendingIcon } from 'components/icon/icon';
-import { SvgIconProps } from '@material-ui/core/SvgIcon';
-import ArrowDownwardIcon from '@material-ui/icons/ArrowDownward';
+import React from "react";
+import {
+ Table,
+ TableBody,
+ TableRow,
+ TableCell,
+ TableHead,
+ TableSortLabel,
+ StyleRulesCallback,
+ Theme,
+ WithStyles,
+ withStyles,
+ IconButton,
+ Tooltip,
+} from "@material-ui/core";
+import classnames from "classnames";
+import { DataColumn, SortDirection } from "./data-column";
+import { DataTableDefaultView } from "../data-table-default-view/data-table-default-view";
+import { DataTableFilters } from "../data-table-filters/data-table-filters-tree";
+import { DataTableMultiselectPopover } from "../data-table-multiselect-popover/data-table-multiselect-popover";
+import { DataTableFiltersPopover } from "../data-table-filters/data-table-filters-popover";
+import { countNodes, getTreeDirty } from "models/tree";
+import { IconType, PendingIcon } from "components/icon/icon";
+import { SvgIconProps } from "@material-ui/core/SvgIcon";
+import ArrowDownwardIcon from "@material-ui/icons/ArrowDownward";
+import { createTree } from "models/tree";
+import { DataTableMultiselectOption } from "../data-table-multiselect-popover/data-table-multiselect-popover";
export type DataColumns<I, R> = Array<DataColumn<I, R>>;
export enum DataTableFetchMode {
PAGINATED,
- INFINITE
+ INFINITE,
}
export interface DataTableDataProps<I> {
defaultViewMessages?: string[];
currentItemUuid?: string;
currentRoute?: string;
+ toggleMSToolbar: (isVisible: boolean) => void;
+ setCheckedListOnStore: (checkedList: TCheckedList) => void;
+ checkedList: TCheckedList;
}
-type CssRules = "tableBody" | "root" | "content" | "noItemsInfo" | 'tableCell' | 'arrow' | 'arrowButton' | 'tableCellWorkflows' | 'loader';
+type CssRules =
+ | "tableBody"
+ | "root"
+ | "content"
+ | "noItemsInfo"
+ | "checkBoxHead"
+ | "checkBoxCell"
+ | "checkBox"
+ | "firstTableCell"
+ | "tableCell"
+ | "arrow"
+ | "arrowButton"
+ | "tableCellWorkflows"
+ | "loader";
const styles: StyleRulesCallback<CssRules> = (theme: Theme) => ({
root: {
- width: '100%',
+ width: "100%",
},
content: {
- display: 'inline-block',
- width: '100%',
+ display: "inline-block",
+ width: "100%",
},
tableBody: {
- background: theme.palette.background.paper
+ background: theme.palette.background.paper,
},
loader: {
- left: '50%',
- marginLeft: '-84px',
- position: 'absolute'
+ left: "50%",
+ marginLeft: "-84px",
+ position: "absolute",
},
noItemsInfo: {
textAlign: "center",
- padding: theme.spacing.unit
+ padding: theme.spacing.unit,
+ },
+ checkBoxHead: {
+ padding: "0",
+ display: "flex",
+ },
+ checkBoxCell: {
+ padding: "0",
+ paddingLeft: "10px",
+ },
+ checkBox: {
+ cursor: "pointer",
},
tableCell: {
- wordWrap: 'break-word',
- paddingRight: '24px',
- color: '#737373'
-
+ wordWrap: "break-word",
+ paddingRight: "24px",
+ color: "#737373",
+ },
+ firstTableCell: {
+ paddingLeft: "5px",
},
tableCellWorkflows: {
- '&:nth-last-child(2)': {
- padding: '0px',
- maxWidth: '48px'
+ "&:nth-last-child(2)": {
+ padding: "0px",
+ maxWidth: "48px",
+ },
+ "&:last-child": {
+ padding: "0px",
+ paddingRight: "24px",
+ width: "48px",
},
- '&:last-child': {
- padding: '0px',
- paddingRight: '24px',
- width: '48px'
- }
},
arrow: {
- margin: 0
+ margin: 0,
},
arrowButton: {
- color: theme.palette.text.primary
- }
+ color: theme.palette.text.primary,
+ },
});
+export type TCheckedList = Record<string, boolean>;
+
+type DataTableState = {
+ isSelected: boolean;
+};
+
type DataTableProps<T> = DataTableDataProps<T> & WithStyles<CssRules>;
export const DataTable = withStyles(styles)(
class Component<T> extends React.Component<DataTableProps<T>> {
+ state: DataTableState = {
+ isSelected: false,
+ };
+
+ componentDidMount(): void {
+ this.initializeCheckedList(this.props.items);
+ }
+
+ componentDidUpdate(prevProps: Readonly<DataTableProps<T>>, prevState: DataTableState) {
+ const { items, setCheckedListOnStore } = this.props;
+ const { isSelected } = this.state;
+ if (prevProps.items !== items) {
+ if (isSelected === true) this.setState({ isSelected: false });
+ if (items.length) this.initializeCheckedList(items);
+ else setCheckedListOnStore({});
+ }
+ }
+
+ checkBoxColumn: DataColumn<any, any> = {
+ name: "checkBoxColumn",
+ selected: true,
+ configurable: false,
+ filters: createTree(),
+ render: uuid => {
+ const { classes, checkedList } = this.props;
+ return (
+ <input
+ type="checkbox"
+ name={uuid}
+ className={classes.checkBox}
+ checked={checkedList && checkedList[uuid] ? checkedList[uuid] : false}
+ onChange={() => this.handleSelectOne(uuid)}
+ onDoubleClick={ev => ev.stopPropagation()}></input>
+ );
+ },
+ };
+
+ multiselectOptions: DataTableMultiselectOption[] = [
+ { name: "All", fn: list => this.handleSelectAll(list) },
+ { name: "None", fn: list => this.handleSelectNone(list) },
+ { name: "Invert", fn: list => this.handleInvertSelect(list) },
+ ];
+
+ initializeCheckedList = (uuids: any[]): void => {
+ const newCheckedList = { ...this.props.checkedList };
+
+ uuids.forEach(uuid => {
+ if (!newCheckedList.hasOwnProperty(uuid)) {
+ newCheckedList[uuid] = false;
+ }
+ });
+ for (const key in newCheckedList) {
+ if (!uuids.includes(key)) {
+ delete newCheckedList[key];
+ }
+ }
+ this.props.setCheckedListOnStore(newCheckedList);
+ };
+
+ isAllSelected = (list: TCheckedList): boolean => {
+ for (const key in list) {
+ if (list[key] === false) return false;
+ }
+ return true;
+ };
+
+ isAnySelected = (): boolean => {
+ const { checkedList } = this.props;
+ if (!Object.keys(checkedList).length) return false;
+ for (const key in checkedList) {
+ if (checkedList[key] === true) return true;
+ }
+ return false;
+ };
+
+ handleSelectOne = (uuid: string): void => {
+ const { checkedList } = this.props;
+ const newCheckedList = { ...checkedList };
+ newCheckedList[uuid] = !checkedList[uuid];
+ this.setState({ isSelected: this.isAllSelected(newCheckedList) });
+ this.props.setCheckedListOnStore(newCheckedList);
+ };
+
+ handleSelectorSelect = (): void => {
+ const { checkedList } = this.props;
+ const { isSelected } = this.state;
+ isSelected ? this.handleSelectNone(checkedList) : this.handleSelectAll(checkedList);
+ };
+
+ handleSelectAll = (list: TCheckedList): void => {
+ if (Object.keys(list).length) {
+ const newCheckedList = { ...list };
+ for (const key in newCheckedList) {
+ newCheckedList[key] = true;
+ }
+ this.setState({ isSelected: true });
+ this.props.setCheckedListOnStore(newCheckedList);
+ }
+ };
+
+ handleSelectNone = (list: TCheckedList): void => {
+ const newCheckedList = { ...list };
+ for (const key in newCheckedList) {
+ newCheckedList[key] = false;
+ }
+ this.setState({ isSelected: false });
+ this.props.setCheckedListOnStore(newCheckedList);
+ };
+
+ handleInvertSelect = (list: TCheckedList): void => {
+ if (Object.keys(list).length) {
+ const newCheckedList = { ...list };
+ for (const key in newCheckedList) {
+ newCheckedList[key] = !list[key];
+ }
+ this.setState({ isSelected: this.isAllSelected(newCheckedList) });
+ this.props.setCheckedListOnStore(newCheckedList);
+ }
+ };
+
render() {
- const { items, classes, working } = this.props;
- return <div className={classes.root}>
- <div className={classes.content}>
- <Table>
- <TableHead>
- <TableRow>
- {this.mapVisibleColumns(this.renderHeadCell)}
- </TableRow>
- </TableHead>
- <TableBody className={classes.tableBody}>
- { !working && items.map(this.renderBodyRow) }
- </TableBody>
- </Table>
- { !!working &&
- <div className={classes.loader}>
- <DataTableDefaultView
- icon={PendingIcon}
- messages={['Loading data, please wait.']} />
- </div> }
- {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
+ const { items, classes, working, columns } = this.props;
+ if (columns[0].name === this.checkBoxColumn.name) columns.shift();
+ columns.unshift(this.checkBoxColumn);
+ return (
+ <div className={classes.root}>
+ <div className={classes.content}>
+ <Table>
+ <TableHead>
+ <TableRow>{this.mapVisibleColumns(this.renderHeadCell)}</TableRow>
+ </TableHead>
+ <TableBody className={classes.tableBody}>{!working && items.map(this.renderBodyRow)}</TableBody>
+ </Table>
+ {!!working && (
+ <div className={classes.loader}>
+ <DataTableDefaultView
+ icon={PendingIcon}
+ messages={["Loading data, please wait."]}
+ />
+ </div>
+ )}
+ {items.length === 0 && !working && this.renderNoItemsPlaceholder(this.props.columns)}
+ </div>
</div>
- </div>;
+ );
}
renderNoItemsPlaceholder = (columns: DataColumns<T, any>) => {
- const dirty = columns.some((column) => getTreeDirty('')(column.filters));
- return <DataTableDefaultView
- icon={this.props.defaultViewIcon}
- messages={this.props.defaultViewMessages}
- filtersApplied={dirty} />;
- }
+ const dirty = columns.some(column => getTreeDirty("")(column.filters));
+ return (
+ <DataTableDefaultView
+ icon={this.props.defaultViewIcon}
+ messages={this.props.defaultViewMessages}
+ filtersApplied={dirty}
+ />
+ );
+ };
renderHeadCell = (column: DataColumn<T, any>, index: number) => {
const { name, key, renderHeader, filters, sort } = column;
- const { onSortToggle, onFiltersChange, classes } = this.props;
- return <TableCell className={classes.tableCell} key={key || index}>
- {renderHeader ?
- renderHeader() :
- countNodes(filters) > 0
- ? <DataTableFiltersPopover
+ const { onSortToggle, onFiltersChange, classes, checkedList } = this.props;
+ const { isSelected } = this.state;
+ return column.name === "checkBoxColumn" ? (
+ <TableCell
+ key={key || index}
+ className={classes.checkBoxCell}>
+ <div className={classes.checkBoxHead}>
+ <Tooltip title={this.state.isSelected ? "Deselect All" : "Select All"}>
+ <input
+ type="checkbox"
+ className={classes.checkBox}
+ checked={isSelected}
+ disabled={!this.props.items.length}
+ onChange={this.handleSelectorSelect}></input>
+ </Tooltip>
+ <DataTableMultiselectPopover
+ name={`Options`}
+ disabled={!this.props.items.length}
+ options={this.multiselectOptions}
+ checkedList={checkedList}></DataTableMultiselectPopover>
+ </div>
+ </TableCell>
+ ) : (
+ <TableCell
+ className={index === 1 ? classes.firstTableCell : classes.tableCell}
+ key={key || index}>
+ {renderHeader ? (
+ renderHeader()
+ ) : countNodes(filters) > 0 ? (
+ <DataTableFiltersPopover
name={`${name} filters`}
mutuallyExclusive={column.mutuallyExclusiveFilters}
- onChange={filters =>
- onFiltersChange &&
- onFiltersChange(filters, column)}
+ onChange={filters => onFiltersChange && onFiltersChange(filters, column)}
filters={filters}>
{name}
</DataTableFiltersPopover>
- : sort
- ? <TableSortLabel
- active={sort.direction !== SortDirection.NONE}
- direction={sort.direction !== SortDirection.NONE ? sort.direction : undefined}
- IconComponent={this.ArrowIcon}
- hideSortIcon
- onClick={() =>
- onSortToggle &&
- onSortToggle(column)}>
- {name}
- </TableSortLabel>
- : <span>
- {name}
- </span>}
- </TableCell>;
- }
+ ) : sort ? (
+ <TableSortLabel
+ active={sort.direction !== SortDirection.NONE}
+ direction={sort.direction !== SortDirection.NONE ? sort.direction : undefined}
+ IconComponent={this.ArrowIcon}
+ hideSortIcon
+ onClick={() => onSortToggle && onSortToggle(column)}>
+ {name}
+ </TableSortLabel>
+ ) : (
+ <span>{name}</span>
+ )}
+ </TableCell>
+ );
+ };
ArrowIcon = ({ className, ...props }: SvgIconProps) => (
- <IconButton component='span' className={this.props.classes.arrowButton} tabIndex={-1}>
- <ArrowDownwardIcon {...props} className={classnames(className, this.props.classes.arrow)} />
+ <IconButton
+ component="span"
+ className={this.props.classes.arrowButton}
+ tabIndex={-1}>
+ <ArrowDownwardIcon
+ {...props}
+ className={classnames(className, this.props.classes.arrow)}
+ />
</IconButton>
- )
+ );
renderBodyRow = (item: any, index: number) => {
const { onRowClick, onRowDoubleClick, extractKey, classes, currentItemUuid, currentRoute } = this.props;
- return <TableRow
- hover
- key={extractKey ? extractKey(item) : index}
- onClick={event => onRowClick && onRowClick(event, item)}
- onContextMenu={this.handleRowContextMenu(item)}
- onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)}
- selected={item === currentItemUuid}>
- {this.mapVisibleColumns((column, index) => <TableCell key={column.key || index} className={currentRoute === '/workflows' ? classes.tableCellWorkflows : classes.tableCell}>
- {column.render(item)}
- </TableCell>
- )}
- </TableRow>;
- }
+ return (
+ <TableRow
+ hover
+ key={extractKey ? extractKey(item) : index}
+ onClick={event => onRowClick && onRowClick(event, item)}
+ onContextMenu={this.handleRowContextMenu(item)}
+ onDoubleClick={event => onRowDoubleClick && onRowDoubleClick(event, item)}
+ selected={item === currentItemUuid}>
+ {this.mapVisibleColumns((column, index) => (
+ <TableCell
+ key={column.key || index}
+ className={
+ currentRoute === "/workflows"
+ ? classes.tableCellWorkflows
+ : index === 0
+ ? classes.checkBoxCell
+ : `${classes.tableCell} ${index === 1 ? classes.firstTableCell : ""}`
+ }>
+ {column.render(item)}
+ </TableCell>
+ ))}
+ </TableRow>
+ );
+ };
mapVisibleColumns = (fn: (column: DataColumn<T, any>, index: number) => React.ReactElement<any>) => {
return this.props.columns.filter(column => column.selected).map(fn);
- }
-
- handleRowContextMenu = (item: T) =>
- (event: React.MouseEvent<HTMLElement>) =>
- this.props.onContextMenu(event, item)
+ };
+ handleRowContextMenu = (item: T) => (event: React.MouseEvent<HTMLElement>) => this.props.onContextMenu(event, item);
}
);
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { Badge, SvgIcon, Tooltip } from '@material-ui/core';
-import Add from '@material-ui/icons/Add';
-import ArrowBack from '@material-ui/icons/ArrowBack';
-import ArrowDropDown from '@material-ui/icons/ArrowDropDown';
-import Build from '@material-ui/icons/Build';
-import Cached from '@material-ui/icons/Cached';
-import DescriptionIcon from '@material-ui/icons/Description';
-import ChevronLeft from '@material-ui/icons/ChevronLeft';
-import CloudUpload from '@material-ui/icons/CloudUpload';
-import Code from '@material-ui/icons/Code';
-import Create from '@material-ui/icons/Create';
-import ImportContacts from '@material-ui/icons/ImportContacts';
-import ChevronRight from '@material-ui/icons/ChevronRight';
-import Close from '@material-ui/icons/Close';
-import ContentCopy from '@material-ui/icons/FileCopyOutlined';
-import FileCopy from '@material-ui/icons/FileCopy';
-import CreateNewFolder from '@material-ui/icons/CreateNewFolder';
-import Delete from '@material-ui/icons/Delete';
-import DeviceHub from '@material-ui/icons/DeviceHub';
-import Edit from '@material-ui/icons/Edit';
-import ErrorRoundedIcon from '@material-ui/icons/ErrorRounded';
-import ExpandMoreIcon from '@material-ui/icons/ExpandMore';
-import FlipToFront from '@material-ui/icons/FlipToFront';
-import Folder from '@material-ui/icons/Folder';
-import FolderShared from '@material-ui/icons/FolderShared';
-import Pageview from '@material-ui/icons/Pageview';
-import GetApp from '@material-ui/icons/GetApp';
-import Help from '@material-ui/icons/Help';
-import HelpOutline from '@material-ui/icons/HelpOutline';
-import History from '@material-ui/icons/History';
-import Inbox from '@material-ui/icons/Inbox';
-import Memory from '@material-ui/icons/Memory';
-import MoveToInbox from '@material-ui/icons/MoveToInbox';
-import Info from '@material-ui/icons/Info';
-import Input from '@material-ui/icons/Input';
-import InsertDriveFile from '@material-ui/icons/InsertDriveFile';
-import LastPage from '@material-ui/icons/LastPage';
-import LibraryBooks from '@material-ui/icons/LibraryBooks';
-import ListAlt from '@material-ui/icons/ListAlt';
-import Menu from '@material-ui/icons/Menu';
-import MoreVert from '@material-ui/icons/MoreVert';
-import MoreHoriz from '@material-ui/icons/MoreHoriz';
-import Mail from '@material-ui/icons/Mail';
-import Notifications from '@material-ui/icons/Notifications';
-import OpenInNew from '@material-ui/icons/OpenInNew';
-import People from '@material-ui/icons/People';
-import Person from '@material-ui/icons/Person';
-import PersonAdd from '@material-ui/icons/PersonAdd';
-import PlayArrow from '@material-ui/icons/PlayArrow';
-import Public from '@material-ui/icons/Public';
-import RateReview from '@material-ui/icons/RateReview';
-import RestoreFromTrash from '@material-ui/icons/History';
-import Search from '@material-ui/icons/Search';
-import SettingsApplications from '@material-ui/icons/SettingsApplications';
-import SettingsEthernet from '@material-ui/icons/SettingsEthernet';
-import Settings from '@material-ui/icons/Settings';
-import Star from '@material-ui/icons/Star';
-import StarBorder from '@material-ui/icons/StarBorder';
-import Warning from '@material-ui/icons/Warning';
-import VpnKey from '@material-ui/icons/VpnKey';
-import LinkOutlined from '@material-ui/icons/LinkOutlined';
-import RemoveRedEye from '@material-ui/icons/RemoveRedEye';
-import Computer from '@material-ui/icons/Computer';
-import WrapText from '@material-ui/icons/WrapText';
-import TextIncrease from '@material-ui/icons/ZoomIn';
-import TextDecrease from '@material-ui/icons/ZoomOut';
-import FullscreenSharp from '@material-ui/icons/FullscreenSharp';
-import FullscreenExitSharp from '@material-ui/icons/FullscreenExitSharp';
-import ExitToApp from '@material-ui/icons/ExitToApp';
-import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline';
-import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline';
-import NotInterested from '@material-ui/icons/NotInterested';
-import Image from '@material-ui/icons/Image';
-import Stop from '@material-ui/icons/Stop';
+import React from "react";
+import { Badge, SvgIcon, Tooltip } from "@material-ui/core";
+import Add from "@material-ui/icons/Add";
+import ArrowBack from "@material-ui/icons/ArrowBack";
+import ArrowDropDown from "@material-ui/icons/ArrowDropDown";
+import Build from "@material-ui/icons/Build";
+import Cached from "@material-ui/icons/Cached";
+import DescriptionIcon from "@material-ui/icons/Description";
+import ChevronLeft from "@material-ui/icons/ChevronLeft";
+import CloudUpload from "@material-ui/icons/CloudUpload";
+import Code from "@material-ui/icons/Code";
+import Create from "@material-ui/icons/Create";
+import ImportContacts from "@material-ui/icons/ImportContacts";
+import ChevronRight from "@material-ui/icons/ChevronRight";
+import Close from "@material-ui/icons/Close";
+import ContentCopy from "@material-ui/icons/FileCopyOutlined";
+import CreateNewFolder from "@material-ui/icons/CreateNewFolder";
+import Delete from "@material-ui/icons/Delete";
+import DeviceHub from "@material-ui/icons/DeviceHub";
+import Edit from "@material-ui/icons/Edit";
+import ErrorRoundedIcon from "@material-ui/icons/ErrorRounded";
+import ExpandMoreIcon from "@material-ui/icons/ExpandMore";
+import FlipToFront from "@material-ui/icons/FlipToFront";
+import Folder from "@material-ui/icons/Folder";
+import FolderShared from "@material-ui/icons/FolderShared";
+import Pageview from "@material-ui/icons/Pageview";
+import GetApp from "@material-ui/icons/GetApp";
+import Help from "@material-ui/icons/Help";
+import HelpOutline from "@material-ui/icons/HelpOutline";
+import History from "@material-ui/icons/History";
+import Inbox from "@material-ui/icons/Inbox";
+import Memory from "@material-ui/icons/Memory";
+import MoveToInbox from "@material-ui/icons/MoveToInbox";
+import Info from "@material-ui/icons/Info";
+import Input from "@material-ui/icons/Input";
+import InsertDriveFile from "@material-ui/icons/InsertDriveFile";
+import LastPage from "@material-ui/icons/LastPage";
+import LibraryBooks from "@material-ui/icons/LibraryBooks";
+import ListAlt from "@material-ui/icons/ListAlt";
+import Menu from "@material-ui/icons/Menu";
+import MoreVert from "@material-ui/icons/MoreVert";
+import MoreHoriz from "@material-ui/icons/MoreHoriz";
+import Mail from "@material-ui/icons/Mail";
+import Notifications from "@material-ui/icons/Notifications";
+import OpenInNew from "@material-ui/icons/OpenInNew";
+import People from "@material-ui/icons/People";
+import Person from "@material-ui/icons/Person";
+import PersonAdd from "@material-ui/icons/PersonAdd";
+import PlayArrow from "@material-ui/icons/PlayArrow";
+import Public from "@material-ui/icons/Public";
+import RateReview from "@material-ui/icons/RateReview";
+import RestoreFromTrash from "@material-ui/icons/History";
+import Search from "@material-ui/icons/Search";
+import SettingsApplications from "@material-ui/icons/SettingsApplications";
+import SettingsEthernet from "@material-ui/icons/SettingsEthernet";
+import Settings from "@material-ui/icons/Settings";
+import Star from "@material-ui/icons/Star";
+import StarBorder from "@material-ui/icons/StarBorder";
+import Warning from "@material-ui/icons/Warning";
+import VpnKey from "@material-ui/icons/VpnKey";
+import LinkOutlined from "@material-ui/icons/LinkOutlined";
+import RemoveRedEye from "@material-ui/icons/RemoveRedEye";
+import Computer from "@material-ui/icons/Computer";
+import WrapText from "@material-ui/icons/WrapText";
+import TextIncrease from "@material-ui/icons/ZoomIn";
+import TextDecrease from "@material-ui/icons/ZoomOut";
+import FullscreenSharp from "@material-ui/icons/FullscreenSharp";
+import FullscreenExitSharp from "@material-ui/icons/FullscreenExitSharp";
+import ExitToApp from "@material-ui/icons/ExitToApp";
+import CheckCircleOutline from "@material-ui/icons/CheckCircleOutline";
+import RemoveCircleOutline from "@material-ui/icons/RemoveCircleOutline";
+import NotInterested from "@material-ui/icons/NotInterested";
+import Image from "@material-ui/icons/Image";
+import Stop from "@material-ui/icons/Stop";
+import FileCopy from "@material-ui/icons/FileCopy";
// Import FontAwesome icons
-import { library } from '@fortawesome/fontawesome-svg-core';
-import { faPencilAlt, faSlash, faUsers, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
-import { FormatAlignLeft } from '@material-ui/icons';
-library.add(
- faPencilAlt,
- faSlash,
- faUsers,
- faEllipsisH,
-);
+import { library } from "@fortawesome/fontawesome-svg-core";
+import { faPencilAlt, faSlash, faUsers, faEllipsisH } from "@fortawesome/free-solid-svg-icons";
+import { FormatAlignLeft } from "@material-ui/icons";
+library.add(faPencilAlt, faSlash, faUsers, faEllipsisH);
-export const FreezeIcon = (props: any) =>
+export const FreezeIcon = (props: any) => (
<SvgIcon {...props}>
<path d="M20.79,13.95L18.46,14.57L16.46,13.44V10.56L18.46,9.43L20.79,10.05L21.31,8.12L19.54,7.65L20,5.88L18.07,5.36L17.45,7.69L15.45,8.82L13,7.38V5.12L14.71,3.41L13.29,2L12,3.29L10.71,2L9.29,3.41L11,5.12V7.38L8.5,8.82L6.5,7.69L5.92,5.36L4,5.88L4.47,7.65L2.7,8.12L3.22,10.05L5.55,9.43L7.55,10.56V13.45L5.55,14.58L3.22,13.96L2.7,15.89L4.47,16.36L4,18.12L5.93,18.64L6.55,16.31L8.55,15.18L11,16.62V18.88L9.29,20.59L10.71,22L12,20.71L13.29,22L14.7,20.59L13,18.88V16.62L15.5,15.17L17.5,16.3L18.12,18.63L20,18.12L19.53,16.35L21.3,15.88L20.79,13.95M9.5,10.56L12,9.11L14.5,10.56V13.44L12,14.89L9.5,13.44V10.56Z" />
</SvgIcon>
+);
-export const UnfreezeIcon = (props: any) =>
+export const UnfreezeIcon = (props: any) => (
<SvgIcon {...props}>
<path d="M11 5.12L9.29 3.41L10.71 2L12 3.29L13.29 2L14.71 3.41L13 5.12V7.38L15.45 8.82L17.45 7.69L18.07 5.36L20 5.88L19.54 7.65L21.31 8.12L20.79 10.05L18.46 9.43L16.46 10.56V13.26L14.5 11.3V10.56L12.74 9.54L10.73 7.53L11 7.38V5.12M18.46 14.57L16.87 13.67L19.55 16.35L21.3 15.88L20.79 13.95L18.46 14.57M13 16.62V18.88L14.7 20.59L13.29 22L12 20.71L10.71 22L9.29 20.59L11 18.88V16.62L8.55 15.18L6.55 16.31L5.93 18.64L4 18.12L4.47 16.36L2.7 15.89L3.22 13.96L5.55 14.58L7.55 13.45V10.56L5.55 9.43L3.22 10.05L2.7 8.12L4.47 7.65L4 5.89L1.11 3L2.39 1.73L22.11 21.46L20.84 22.73L14.1 16L13 16.62M12 14.89L12.63 14.5L9.5 11.39V13.44L12 14.89Z" />
</SvgIcon>
+);
-export const PendingIcon = (props: any) =>
+export const PendingIcon = (props: any) => (
<span {...props}>
- <span className='fas fa-ellipsis-h' />
+ <span className="fas fa-ellipsis-h" />
</span>
+);
-export const ReadOnlyIcon = (props: any) =>
+export const ReadOnlyIcon = (props: any) => (
<span {...props}>
<div className="fa-layers fa-1x fa-fw">
- <span className="fas fa-slash"
- data-fa-mask="fas fa-pencil-alt" data-fa-transform="down-1.5" />
+ <span
+ className="fas fa-slash"
+ data-fa-mask="fas fa-pencil-alt"
+ data-fa-transform="down-1.5"
+ />
<span className="fas fa-slash" />
</div>
- </span>;
+ </span>
+);
-export const GroupsIcon = (props: any) =>
+export const GroupsIcon = (props: any) => (
<span {...props}>
<span className="fas fa-users" />
- </span>;
+ </span>
+);
-export const CollectionOldVersionIcon = (props: any) =>
- <Tooltip title='Old version'>
- <Badge badgeContent={<History fontSize='small' />}>
+export const CollectionOldVersionIcon = (props: any) => (
+ <Tooltip title="Old version">
+ <Badge badgeContent={<History fontSize="small" />}>
<CollectionIcon {...props} />
</Badge>
- </Tooltip>;
+ </Tooltip>
+);
// https://materialdesignicons.com/icon/image-off
-export const ImageOffIcon = (props: any) =>
+export const ImageOffIcon = (props: any) => (
<SvgIcon {...props}>
<path d="M21 17.2L6.8 3H19C20.1 3 21 3.9 21 5V17.2M20.7 22L19.7 21H5C3.9 21 3 20.1 3 19V4.3L2 3.3L3.3 2L22 20.7L20.7 22M16.8 18L12.9 14.1L11 16.5L8.5 13.5L5 18H16.8Z" />
- </SvgIcon>;
+ </SvgIcon>
+);
// https://materialdesignicons.com/icon/inbox-arrow-up
-export const OutputIcon: IconType = (props: any) =>
+export const OutputIcon: IconType = (props: any) => (
<SvgIcon {...props}>
<path d="M14,14H10V11H8L12,7L16,11H14V14M16,11M5,15V5H19V15H15A3,3 0 0,1 12,18A3,3 0 0,1 9,15H5M19,3H5C3.89,3 3,3.9 3,5V19A2,2 0 0,0 5,21H19A2,2 0 0,0 21,19V5A2,2 0 0,0 19,3" />
- </SvgIcon>;
+ </SvgIcon>
+);
// https://pictogrammers.com/library/mdi/icon/file-move/
-export const FileMoveIcon: IconType = (props: any) =>
+export const FileMoveIcon: IconType = (props: any) => (
<SvgIcon {...props}>
<path d="M14,17H18V14L23,18.5L18,23V20H14V17M13,9H18.5L13,3.5V9M6,2H14L20,8V12.34C19.37,12.12 18.7,12 18,12A6,6 0 0,0 12,18C12,19.54 12.58,20.94 13.53,22H6C4.89,22 4,21.1 4,20V4A2,2 0 0,1 6,2Z" />
- </SvgIcon>;
+ </SvgIcon>
+);
// https://pictogrammers.com/library/mdi/icon/checkbox-multiple-outline/
-export const CheckboxMultipleOutline: IconType = (props: any) =>
+export const CheckboxMultipleOutline: IconType = (props: any) => (
<SvgIcon {...props}>
<path d="M20,2H8A2,2 0 0,0 6,4V16A2,2 0 0,0 8,18H20A2,2 0 0,0 22,16V4A2,2 0 0,0 20,2M20,16H8V4H20V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16M18.53,8.06L17.47,7L12.59,11.88L10.47,9.76L9.41,10.82L12.59,14L18.53,8.06Z" />
- </SvgIcon>;
+ </SvgIcon>
+);
// https://pictogrammers.com/library/mdi/icon/checkbox-multiple-blank-outline/
-export const CheckboxMultipleBlankOutline: IconType = (props: any) =>
+export const CheckboxMultipleBlankOutline: IconType = (props: any) => (
<SvgIcon {...props}>
<path d="M20,16V4H8V16H20M22,16A2,2 0 0,1 20,18H8C6.89,18 6,17.1 6,16V4C6,2.89 6.89,2 8,2H20A2,2 0 0,1 22,4V16M16,20V22H4A2,2 0 0,1 2,20V7H4V20H16Z" />
- </SvgIcon>;
+ </SvgIcon>
+);
-export type IconType = React.SFC<{ className?: string, style?: object }>;
+export type IconType = React.SFC<{ className?: string; style?: object }>;
-// https://v4.mui.com/components/material-icons/
-export const AddIcon: IconType = (props) => <Add {...props} />;
-export const AddFavoriteIcon: IconType = (props) => <StarBorder {...props} />;
-export const AdminMenuIcon: IconType = (props) => <Build {...props} />;
-export const AdvancedIcon: IconType = (props) => <SettingsApplications {...props} />;
-export const AttributesIcon: IconType = (props) => <ListAlt {...props} />;
-export const BackIcon: IconType = (props) => <ArrowBack {...props} />;
-export const CustomizeTableIcon: IconType = (props) => <Menu {...props} />;
-export const CommandIcon: IconType = (props) => <LastPage {...props} />;
-export const CopyIcon: IconType = (props) => <ContentCopy {...props} />;
-export const FileCopyIcon: IconType = (props) => <FileCopy {...props} />;
-export const CollectionIcon: IconType = (props) => <LibraryBooks {...props} />;
-export const CloseIcon: IconType = (props) => <Close {...props} />;
-export const CloudUploadIcon: IconType = (props) => <CloudUpload {...props} />;
-export const DefaultIcon: IconType = (props) => <RateReview {...props} />;
-export const DetailsIcon: IconType = (props) => <Info {...props} />;
-export const DirectoryIcon: IconType = (props) => <Folder {...props} />;
-export const DownloadIcon: IconType = (props) => <GetApp {...props} />;
-export const EditSavedQueryIcon: IconType = (props) => <Create {...props} />;
-export const ExpandIcon: IconType = (props) => <ExpandMoreIcon {...props} />;
-export const ErrorIcon: IconType = (props) => <ErrorRoundedIcon style={{ color: '#ff0000' }} {...props} />;
-export const FavoriteIcon: IconType = (props) => <Star {...props} />;
-export const FileIcon: IconType = (props) => <DescriptionIcon {...props} />;
-export const HelpIcon: IconType = (props) => <Help {...props} />;
-export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
-export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
-export const InfoIcon: IconType = (props) => <Info {...props} />;
-export const FileInputIcon: IconType = (props) => <InsertDriveFile {...props} />;
-export const KeyIcon: IconType = (props) => <VpnKey {...props} />;
-export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
-export const MailIcon: IconType = (props) => <Mail {...props} />;
-export const MaximizeIcon: IconType = (props) => <FullscreenSharp {...props} />;
-export const MemoryIcon: IconType = (props) => <Memory {...props} />;
-export const UnMaximizeIcon: IconType = (props) => <FullscreenExitSharp {...props} />;
-export const MoreVerticalIcon: IconType = (props) => <MoreVert {...props} />;
-export const MoreHorizontalIcon: IconType = (props) => <MoreHoriz {...props} />;
-export const MoveToIcon: IconType = (props) => <Input {...props} />;
-export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
-export const NotificationIcon: IconType = (props) => <Notifications {...props} />;
-export const OpenIcon: IconType = (props) => <OpenInNew {...props} />;
-export const InputIcon: IconType = (props) => <MoveToInbox {...props} />;
-export const PaginationDownIcon: IconType = (props) => <ArrowDropDown {...props} />;
-export const PaginationLeftArrowIcon: IconType = (props) => <ChevronLeft {...props} />;
-export const PaginationRightArrowIcon: IconType = (props) => <ChevronRight {...props} />;
-export const ProcessIcon: IconType = (props) => <Settings {...props} />;
-export const ProjectIcon: IconType = (props) => <Folder {...props} />;
-export const FilterGroupIcon: IconType = (props) => <Pageview {...props} />;
-export const ProjectsIcon: IconType = (props) => <Inbox {...props} />;
-export const ProvenanceGraphIcon: IconType = (props) => <DeviceHub {...props} />;
-export const RemoveIcon: IconType = (props) => <Delete {...props} />;
-export const RemoveFavoriteIcon: IconType = (props) => <Star {...props} />;
-export const PublicFavoriteIcon: IconType = (props) => <Public {...props} />;
-export const RenameIcon: IconType = (props) => <Edit {...props} />;
-export const RestoreVersionIcon: IconType = (props) => <FlipToFront {...props} />;
-export const RestoreFromTrashIcon: IconType = (props) => <RestoreFromTrash {...props} />;
-export const ReRunProcessIcon: IconType = (props) => <Cached {...props} />;
-export const SearchIcon: IconType = (props) => <Search {...props} />;
-export const ShareIcon: IconType = (props) => <PersonAdd {...props} />;
-export const ShareMeIcon: IconType = (props) => <People {...props} />;
-export const SidePanelRightArrowIcon: IconType = (props) => <PlayArrow {...props} />;
-export const TrashIcon: IconType = (props) => <Delete {...props} />;
-export const UserPanelIcon: IconType = (props) => <Person {...props} />;
-export const UsedByIcon: IconType = (props) => <Folder {...props} />;
-export const WorkflowIcon: IconType = (props) => <Code {...props} />;
-export const WarningIcon: IconType = (props) => <Warning style={{ color: '#fbc02d', height: '30px', width: '30px' }} {...props} />;
-export const Link: IconType = (props) => <LinkOutlined {...props} />;
-export const FolderSharedIcon: IconType = (props) => <FolderShared {...props} />;
-export const CanReadIcon: IconType = (props) => <RemoveRedEye {...props} />;
-export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
-export const CanManageIcon: IconType = (props) => <Computer {...props} />;
-export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
-export const WordWrapOnIcon: IconType = (props) => <WrapText {...props} />;
-export const WordWrapOffIcon: IconType = (props) => <FormatAlignLeft {...props} />;
-export const TextIncreaseIcon: IconType = (props) => <TextIncrease {...props} />;
-export const TextDecreaseIcon: IconType = (props) => <TextDecrease {...props} />;
-export const DeactivateUserIcon: IconType = (props) => <NotInterested {...props} />;
-export const LoginAsIcon: IconType = (props) => <ExitToApp {...props} />;
-export const ActiveIcon: IconType = (props) => <CheckCircleOutline {...props} />;
-export const SetupIcon: IconType = (props) => <RemoveCircleOutline {...props} />;
-export const InactiveIcon: IconType = (props) => <NotInterested {...props} />;
-export const ImageIcon: IconType = (props) => <Image {...props} />;
-export const StartIcon: IconType = (props) => <PlayArrow {...props} />;
-export const StopIcon: IconType = (props) => <Stop {...props} />;
-export const SelectAllIcon: IconType = (props) => <CheckboxMultipleOutline {...props} />;
-export const SelectNoneIcon: IconType = (props) => <CheckboxMultipleBlankOutline {...props} />;
+export const AddIcon: IconType = props => <Add {...props} />;
+export const AddFavoriteIcon: IconType = props => <StarBorder {...props} />;
+export const AdminMenuIcon: IconType = props => <Build {...props} />;
+export const AdvancedIcon: IconType = props => <SettingsApplications {...props} />;
+export const AttributesIcon: IconType = props => <ListAlt {...props} />;
+export const BackIcon: IconType = props => <ArrowBack {...props} />;
+export const CustomizeTableIcon: IconType = props => <Menu {...props} />;
+export const CommandIcon: IconType = props => <LastPage {...props} />;
+export const CopyIcon: IconType = props => <ContentCopy {...props} />;
+export const FileCopyIcon: IconType = props => <FileCopy {...props} />;
+export const CollectionIcon: IconType = props => <LibraryBooks {...props} />;
+export const CloseIcon: IconType = props => <Close {...props} />;
+export const CloudUploadIcon: IconType = props => <CloudUpload {...props} />;
+export const DefaultIcon: IconType = props => <RateReview {...props} />;
+export const DetailsIcon: IconType = props => <Info {...props} />;
+export const DirectoryIcon: IconType = props => <Folder {...props} />;
+export const DownloadIcon: IconType = props => <GetApp {...props} />;
+export const EditSavedQueryIcon: IconType = props => <Create {...props} />;
+export const ExpandIcon: IconType = props => <ExpandMoreIcon {...props} />;
+export const ErrorIcon: IconType = props => (
+ <ErrorRoundedIcon
+ style={{ color: "#ff0000" }}
+ {...props}
+ />
+);
+export const FavoriteIcon: IconType = props => <Star {...props} />;
+export const FileIcon: IconType = props => <DescriptionIcon {...props} />;
+export const HelpIcon: IconType = props => <Help {...props} />;
+export const HelpOutlineIcon: IconType = props => <HelpOutline {...props} />;
+export const ImportContactsIcon: IconType = props => <ImportContacts {...props} />;
+export const InfoIcon: IconType = props => <Info {...props} />;
+export const FileInputIcon: IconType = props => <InsertDriveFile {...props} />;
+export const KeyIcon: IconType = props => <VpnKey {...props} />;
+export const LogIcon: IconType = props => <SettingsEthernet {...props} />;
+export const MailIcon: IconType = props => <Mail {...props} />;
+export const MaximizeIcon: IconType = props => <FullscreenSharp {...props} />;
+export const MemoryIcon: IconType = props => <Memory {...props} />;
+export const UnMaximizeIcon: IconType = props => <FullscreenExitSharp {...props} />;
+export const MoreVerticalIcon: IconType = props => <MoreVert {...props} />;
+export const MoreHorizontalIcon: IconType = props => <MoreHoriz {...props} />;
+export const MoveToIcon: IconType = props => <Input {...props} />;
+export const NewProjectIcon: IconType = props => <CreateNewFolder {...props} />;
+export const NotificationIcon: IconType = props => <Notifications {...props} />;
+export const OpenIcon: IconType = props => <OpenInNew {...props} />;
+export const InputIcon: IconType = props => <MoveToInbox {...props} />;
+export const PaginationDownIcon: IconType = props => <ArrowDropDown {...props} />;
+export const PaginationLeftArrowIcon: IconType = props => <ChevronLeft {...props} />;
+export const PaginationRightArrowIcon: IconType = props => <ChevronRight {...props} />;
+export const ProcessIcon: IconType = props => <Settings {...props} />;
+export const ProjectIcon: IconType = props => <Folder {...props} />;
+export const FilterGroupIcon: IconType = props => <Pageview {...props} />;
+export const ProjectsIcon: IconType = props => <Inbox {...props} />;
+export const ProvenanceGraphIcon: IconType = props => <DeviceHub {...props} />;
+export const RemoveIcon: IconType = props => <Delete {...props} />;
+export const RemoveFavoriteIcon: IconType = props => <Star {...props} />;
+export const PublicFavoriteIcon: IconType = props => <Public {...props} />;
+export const RenameIcon: IconType = props => <Edit {...props} />;
+export const RestoreVersionIcon: IconType = props => <FlipToFront {...props} />;
+export const RestoreFromTrashIcon: IconType = props => <RestoreFromTrash {...props} />;
+export const ReRunProcessIcon: IconType = props => <Cached {...props} />;
+export const SearchIcon: IconType = props => <Search {...props} />;
+export const ShareIcon: IconType = props => <PersonAdd {...props} />;
+export const ShareMeIcon: IconType = props => <People {...props} />;
+export const SidePanelRightArrowIcon: IconType = props => <PlayArrow {...props} />;
+export const TrashIcon: IconType = props => <Delete {...props} />;
+export const UserPanelIcon: IconType = props => <Person {...props} />;
+export const UsedByIcon: IconType = props => <Folder {...props} />;
+export const WorkflowIcon: IconType = props => <Code {...props} />;
+export const WarningIcon: IconType = props => (
+ <Warning
+ style={{ color: "#fbc02d", height: "30px", width: "30px" }}
+ {...props}
+ />
+);
+export const Link: IconType = props => <LinkOutlined {...props} />;
+export const FolderSharedIcon: IconType = props => <FolderShared {...props} />;
+export const CanReadIcon: IconType = props => <RemoveRedEye {...props} />;
+export const CanWriteIcon: IconType = props => <Edit {...props} />;
+export const CanManageIcon: IconType = props => <Computer {...props} />;
+export const AddUserIcon: IconType = props => <PersonAdd {...props} />;
+export const WordWrapOnIcon: IconType = props => <WrapText {...props} />;
+export const WordWrapOffIcon: IconType = props => <FormatAlignLeft {...props} />;
+export const TextIncreaseIcon: IconType = props => <TextIncrease {...props} />;
+export const TextDecreaseIcon: IconType = props => <TextDecrease {...props} />;
+export const DeactivateUserIcon: IconType = props => <NotInterested {...props} />;
+export const LoginAsIcon: IconType = props => <ExitToApp {...props} />;
+export const ActiveIcon: IconType = props => <CheckCircleOutline {...props} />;
+export const SetupIcon: IconType = props => <RemoveCircleOutline {...props} />;
+export const InactiveIcon: IconType = props => <NotInterested {...props} />;
+export const ImageIcon: IconType = props => <Image {...props} />;
+export const StartIcon: IconType = props => <PlayArrow {...props} />;
+export const StopIcon: IconType = props => <Stop {...props} />;
+export const SelectAllIcon: IconType = props => <CheckboxMultipleOutline {...props} />;
+export const SelectNoneIcon: IconType = props => <CheckboxMultipleBlankOutline {...props} />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { connect } from "react-redux";
+import { StyleRulesCallback, withStyles, WithStyles, Toolbar, Tooltip, IconButton } from "@material-ui/core";
+import { ArvadosTheme } from "common/custom-theme";
+import { RootState } from "store/store";
+import { Dispatch } from "redux";
+import { TCheckedList } from "components/data-table/data-table";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+import { Resource, extractUuidKind } from "models/resource";
+import { getResource } from "store/resources/resources";
+import { ResourcesState } from "store/resources/resources";
+import { ContextMenuAction, ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { RestoreFromTrashIcon, TrashIcon } from "components/icon/icon";
+import { multiselectActionsFilters, TMultiselectActionsFilters, contextMenuActionConsts } from "./ms-toolbar-action-filters";
+import { kindToActionSet, findActionByName } from "./ms-kind-action-differentiator";
+import { msToggleTrashAction } from "views-components/multiselect-toolbar/ms-project-action-set";
+import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+import { ContainerRequestResource } from "models/container-request";
+
+type CssRules = "root" | "button";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ root: {
+ display: "flex",
+ flexDirection: "row",
+ width: 0,
+ padding: 0,
+ margin: "1rem auto auto 0.5rem",
+ overflow: "hidden",
+ transition: "width 150ms",
+ },
+ button: {
+ width: "2.5rem",
+ height: "2.5rem ",
+ },
+});
+
+export type MultiselectToolbarProps = {
+ checkedList: TCheckedList;
+ resources: ResourcesState;
+ executeMulti: (action: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState) => void;
+};
+
+export const MultiselectToolbar = connect(
+ mapStateToProps,
+ mapDispatchToProps
+)(
+ withStyles(styles)((props: MultiselectToolbarProps & WithStyles<CssRules>) => {
+ const { classes, checkedList } = props;
+ const currentResourceKinds = Array.from(selectedToKindSet(checkedList));
+
+ const currentPathIsTrash = window.location.pathname === "/trash";
+ const buttons =
+ currentPathIsTrash && selectedToKindSet(checkedList).size
+ ? [msToggleTrashAction]
+ : selectActionsByKind(currentResourceKinds, multiselectActionsFilters);
+
+ return (
+ <React.Fragment>
+ <Toolbar
+ className={classes.root}
+ style={{ width: `${buttons.length * 2.5}rem` }}
+ >
+ {buttons.length ? (
+ buttons.map((btn, i) =>
+ btn.name === "ToggleTrashAction" ? (
+ <Tooltip
+ className={classes.button}
+ title={currentPathIsTrash ? "Restore selected" : "Move to trash"}
+ key={i}
+ disableFocusListener
+ >
+ <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
+ {currentPathIsTrash ? <RestoreFromTrashIcon /> : <TrashIcon />}
+ </IconButton>
+ </Tooltip>
+ ) : (
+ <Tooltip
+ className={classes.button}
+ title={btn.name}
+ key={i}
+ disableFocusListener
+ >
+ <IconButton onClick={() => props.executeMulti(btn, checkedList, props.resources)}>
+ {btn.icon ? btn.icon({}) : <></>}
+ </IconButton>
+ </Tooltip>
+ )
+ )
+ ) : (
+ <></>
+ )}
+ </Toolbar>
+ </React.Fragment>
+ );
+ })
+);
+
+export function selectedToArray(checkedList: TCheckedList): Array<string> {
+ const arrayifiedSelectedList: Array<string> = [];
+ for (const [key, value] of Object.entries(checkedList)) {
+ if (value === true) {
+ arrayifiedSelectedList.push(key);
+ }
+ }
+ return arrayifiedSelectedList;
+}
+
+export function selectedToKindSet(checkedList: TCheckedList): Set<string> {
+ const setifiedList = new Set<string>();
+ for (const [key, value] of Object.entries(checkedList)) {
+ if (value === true) {
+ setifiedList.add(extractUuidKind(key) as string);
+ }
+ }
+ return setifiedList;
+}
+
+function groupByKind(checkedList: TCheckedList, resources: ResourcesState): Record<string, ContextMenuResource[]> {
+ const result = {};
+ selectedToArray(checkedList).forEach(uuid => {
+ const resource = getResource(uuid)(resources) as ContainerRequestResource | Resource;
+ if (!result[resource.kind]) result[resource.kind] = [];
+ result[resource.kind].push(resource);
+ });
+ return result;
+}
+
+function filterActions(actionArray: ContextMenuActionSet, filters: Set<string>): Array<ContextMenuAction> {
+ return actionArray[0].filter(action => filters.has(action.name as string));
+}
+
+function selectActionsByKind(currentResourceKinds: Array<string>, filterSet: TMultiselectActionsFilters) {
+ const rawResult: Set<ContextMenuAction> = new Set();
+ const resultNames = new Set();
+ const allFiltersArray: ContextMenuAction[][] = [];
+ currentResourceKinds.forEach(kind => {
+ if (filterSet[kind]) {
+ const actions = filterActions(...filterSet[kind]);
+ allFiltersArray.push(actions);
+ actions.forEach(action => {
+ if (!resultNames.has(action.name)) {
+ rawResult.add(action);
+ resultNames.add(action.name);
+ }
+ });
+ }
+ });
+
+ const filteredNameSet = allFiltersArray.map(filterArray => {
+ const resultSet = new Set();
+ filterArray.forEach(action => resultSet.add(action.name || ""));
+ return resultSet;
+ });
+
+ const filteredResult = Array.from(rawResult).filter(action => {
+ for (let i = 0; i < filteredNameSet.length; i++) {
+ if (!filteredNameSet[i].has(action.name)) return false;
+ }
+ return true;
+ });
+
+ return filteredResult.sort((a, b) => {
+ const nameA = a.name || "";
+ const nameB = b.name || "";
+ if (nameA < nameB) {
+ return -1;
+ }
+ if (nameA > nameB) {
+ return 1;
+ }
+ return 0;
+ });
+}
+
+//--------------------------------------------------//
+
+function mapStateToProps(state: RootState) {
+ return {
+ checkedList: state.multiselect.checkedList as TCheckedList,
+ resources: state.resources,
+ };
+}
+
+function mapDispatchToProps(dispatch: Dispatch) {
+ return {
+ executeMulti: (selectedAction: ContextMenuAction, checkedList: TCheckedList, resources: ResourcesState): void => {
+ const kindGroups = groupByKind(checkedList, resources);
+ switch (selectedAction.name) {
+ case contextMenuActionConsts.MOVE_TO:
+ case contextMenuActionConsts.REMOVE:
+ const firstResource = getResource(selectedToArray(checkedList)[0])(resources) as ContainerRequestResource | Resource;
+ const action = findActionByName(selectedAction.name as string, kindToActionSet[firstResource.kind]);
+ if (action) action.execute(dispatch, kindGroups[firstResource.kind]);
+ break;
+ case contextMenuActionConsts.COPY_TO_CLIPBOARD:
+ const selectedResources = selectedToArray(checkedList).map(uuid => getResource(uuid)(resources));
+ dispatch<any>(copyToClipboardAction(selectedResources));
+ break;
+ default:
+ for (const kind in kindGroups) {
+ const action = findActionByName(selectedAction.name as string, kindToActionSet[kind]);
+ if (action) action.execute(dispatch, kindGroups[kind]);
+ }
+ break;
+ }
+ },
+ };
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ResourceKind } from "models/resource";
+import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { msCollectionActionSet } from "views-components/multiselect-toolbar/ms-collection-action-set";
+import { msProjectActionSet } from "views-components/multiselect-toolbar/ms-project-action-set";
+import { msProcessActionSet } from "views-components/multiselect-toolbar/ms-process-action-set";
+
+export function findActionByName(name: string, actionSet: ContextMenuActionSet) {
+ return actionSet[0].find(action => action.name === name);
+}
+
+const { COLLECTION, PROJECT, PROCESS } = ResourceKind;
+
+export const kindToActionSet: Record<string, ContextMenuActionSet> = {
+ [COLLECTION]: msCollectionActionSet,
+ [PROJECT]: msProjectActionSet,
+ [PROCESS]: msProcessActionSet,
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ResourceKind } from "models/resource";
+import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { msCollectionActionSet } from "views-components/multiselect-toolbar/ms-collection-action-set";
+import { msProjectActionSet } from "views-components/multiselect-toolbar/ms-project-action-set";
+import { msProcessActionSet } from "views-components/multiselect-toolbar/ms-process-action-set";
+
+export type TMultiselectActionsFilters = Record<string, [ContextMenuActionSet, Set<string>]>;
+
+export const contextMenuActionConsts = {
+ MAKE_A_COPY: "Make a copy",
+ MOVE_TO: "Move to",
+ TOGGLE_TRASH_ACTION: "ToggleTrashAction",
+ COPY_TO_CLIPBOARD: "Copy to clipboard",
+ COPY_AND_RERUN_PROCESS: "Copy and re-run process",
+ REMOVE: "Remove",
+};
+
+const { MOVE_TO, TOGGLE_TRASH_ACTION, REMOVE, MAKE_A_COPY } = contextMenuActionConsts;
+
+//these sets govern what actions are on the ms toolbar for each resource kind
+const projectMSActionsFilter = new Set([MOVE_TO, TOGGLE_TRASH_ACTION]);
+const processResourceMSActionsFilter = new Set([MOVE_TO, REMOVE]);
+const collectionMSActionsFilter = new Set([MAKE_A_COPY, MOVE_TO, TOGGLE_TRASH_ACTION]);
+
+const { COLLECTION, PROJECT, PROCESS } = ResourceKind;
+
+export const multiselectActionsFilters: TMultiselectActionsFilters = {
+ [PROJECT]: [msProjectActionSet, projectMSActionsFilter],
+ [PROCESS]: [msProcessActionSet, processResourceMSActionsFilter],
+ [COLLECTION]: [msCollectionActionSet, collectionMSActionsFilter],
+};
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
formControl: {
- width: '100%'
+ width: '100%',
},
selectWrapper: {
backgroundColor: theme.palette.common.white,
'&:before': {
- borderBottomColor: 'rgba(0, 0, 0, 0.42)'
+ borderBottomColor: 'rgba(0, 0, 0, 0.42)',
},
'&:focus': {
- outline: 'none'
- }
+ outline: 'none',
+ },
},
select: {
fontSize: '0.875rem',
'&:focus': {
- backgroundColor: 'rgba(0, 0, 0, 0.0)'
- }
+ backgroundColor: 'rgba(0, 0, 0, 0.0)',
+ },
},
option: {
fontSize: '0.875rem',
backgroundColor: theme.palette.common.white,
- height: '30px'
- }
+ height: '30px',
+ },
});
interface NativeSelectFieldProps {
disabled?: boolean;
}
-export const NativeSelectField = withStyles(styles)
- ((props: WrappedFieldProps & NativeSelectFieldProps & WithStyles<CssRules> & { items: any[] }) =>
- <FormControl className={props.classes.formControl}>
- <Select className={props.classes.selectWrapper}
- native
- value={props.input.value}
- onChange={props.input.onChange}
- disabled={props.meta.submitting || props.disabled}
- name={props.input.name}
- inputProps={{
- id: `id-${props.input.name}`,
- className: props.classes.select
- }}>
- {props.items.map(item => (
- <option key={item.key} value={item.key} className={props.classes.option}>
- {item.value}
- </option>
- ))}
- </Select>
- </FormControl>
- );
+export const NativeSelectField = withStyles(styles)((props: WrappedFieldProps & NativeSelectFieldProps & WithStyles<CssRules> & { items: any[] }) => (
+ <FormControl className={props.classes.formControl}>
+ <Select
+ className={props.classes.selectWrapper}
+ native
+ value={props.input.value}
+ onChange={props.input.onChange}
+ disabled={props.meta.submitting || props.disabled}
+ name={props.input.name}
+ inputProps={{
+ id: `id-${props.input.name}`,
+ className: props.classes.select,
+ }}>
+ {props.items.map(item => (
+ <option
+ key={item.key}
+ value={item.key}
+ className={props.classes.option}>
+ {item.value}
+ </option>
+ ))}
+ </Select>
+ </FormControl>
+));
interface SelectFieldProps {
children: React.ReactNode;
const selectFieldStyles: StyleRulesCallback<SelectFieldCssRules> = (theme: ArvadosTheme) => ({
formControl: {
- marginBottom: theme.spacing.unit * 3
+ marginBottom: theme.spacing.unit * 3,
},
});
-export const SelectField = withStyles(selectFieldStyles)(
- (props: WrappedFieldProps & SelectFieldProps & WithStyles<SelectFieldCssRules>) =>
- <FormControl error={props.meta.invalid} className={props.classes.formControl}>
- <InputLabel>
- {props.label}
- </InputLabel>
- <Select
- {...props.input}>
- {props.children}
- </Select>
- <FormHelperText>{props.meta.error}</FormHelperText>
- </FormControl>
-);
+export const SelectField = withStyles(selectFieldStyles)((props: WrappedFieldProps & SelectFieldProps & WithStyles<SelectFieldCssRules>) => (
+ <FormControl
+ error={props.meta.invalid}
+ className={props.classes.formControl}>
+ <InputLabel>{props.label}</InputLabel>
+ <Select {...props.input}>{props.children}</Select>
+ <FormHelperText>{props.meta.error}</FormHelperText>
+ </FormControl>
+));
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import ReactDOM from 'react-dom';
+import React from "react";
+import ReactDOM from "react-dom";
import { Provider } from "react-redux";
-import { MainPanel } from 'views/main-panel/main-panel';
-import 'index.css';
-import { Route, Switch } from 'react-router';
+import { MainPanel } from "views/main-panel/main-panel";
+import "index.css";
+import { Route, Switch } from "react-router";
import { createBrowserHistory } from "history";
import { History } from "history";
-import { configureStore, RootStore } from 'store/store';
+import { configureStore, RootStore } from "store/store";
import { ConnectedRouter } from "react-router-redux";
import { ApiToken } from "views-components/api-token/api-token";
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, 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,
+ runningProcessResourceActionSet,
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';
+ runningProcessResourceAdminActionSet,
+ 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";
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()}]`);
addMenuActionSet(ContextMenuKind.OLD_VERSION_COLLECTION, oldCollectionVersionActionSet);
addMenuActionSet(ContextMenuKind.TRASHED_COLLECTION, trashedCollectionActionSet);
addMenuActionSet(ContextMenuKind.PROCESS_RESOURCE, processResourceActionSet);
+addMenuActionSet(ContextMenuKind.RUNNING_PROCESS_RESOURCE, runningProcessResourceActionSet);
addMenuActionSet(ContextMenuKind.READONLY_PROCESS_RESOURCE, readOnlyProcessResourceActionSet);
addMenuActionSet(ContextMenuKind.TRASH, trashActionSet);
addMenuActionSet(ContextMenuKind.REPOSITORY, repositoryActionSet);
addMenuActionSet(ContextMenuKind.GROUP_MEMBER, groupMemberActionSet);
addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet);
addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
+addMenuActionSet(ContextMenuKind.RUNNING_PROCESS_ADMIN, runningProcessResourceAdminActionSet);
addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
addMenuActionSet(ContextMenuKind.FROZEN_PROJECT, frozenActionSet);
addMenuActionSet(ContextMenuKind.FROZEN_PROJECT_ADMIN, frozenAdminActionSet);
storeRedirects();
-fetchConfig()
- .then(({ config, apiHost }) => {
- const history = createBrowserHistory();
+fetchConfig().then(({ config, apiHost }) => {
+ const history = createBrowserHistory();
- // Provide browser's history access to Cypress to allow programmatic
- // navigation.
- if ((window as any).Cypress) {
- (window as any).appHistory = history;
- }
+ // Provide browser's history access to Cypress to allow programmatic
+ // navigation.
+ if ((window as any).Cypress) {
+ (window as any).appHistory = history;
+ }
+
+ const services = createServices(config, {
+ progressFn: (id, working) => {
+ //store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
+ },
+ errorFn: (id, error, showSnackBar: boolean) => {
+ if (showSnackBar) {
+ console.error("Backend error:", error);
- const services = createServices(config, {
- progressFn: (id, working) => {
- store.dispatch(progressIndicatorActions.TOGGLE_WORKING({ id, working }));
- },
- errorFn: (id, error, showSnackBar: boolean) => {
- if (showSnackBar) {
- console.error("Backend error:", error);
-
- if (error.status === 404) {
- store.dispatch(openNotFoundDialog());
- } else if (error.status === 401 && error.errors[0].indexOf("Not logged in") > -1) {
- // Catch auth errors when navigating and redirect to login preserving url location
- store.dispatch(logout(false, true));
- } else {
- store.dispatch(snackbarActions.OPEN_SNACKBAR({
- message: `${error.errors
- ? error.errors[0]
- : error.message}`,
+ if (error.status === 404) {
+ store.dispatch(openNotFoundDialog());
+ } else if (error.status === 401 && error.errors[0].indexOf("Not logged in") > -1) {
+ // Catch auth errors when navigating and redirect to login preserving url location
+ store.dispatch(logout(false, true));
+ } else {
+ store.dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: `${error.errors ? error.errors[0] : error.message}`,
kind: SnackbarKind.ERROR,
- hideDuration: 8000
+ hideDuration: 8000,
})
- );
- }
+ );
}
}
- });
-
- // be sure this is initiated before the app starts
- servicesProvider.setServices(services);
-
- const store = configureStore(history, services, config);
-
- servicesProvider.setStore(store);
-
- store.subscribe(initListener(history, store, services, config));
- store.dispatch(initAuth(config));
- store.dispatch(setBuildInfo());
- store.dispatch(setTokenDialogApiHost(apiHost));
- store.dispatch(loadVocabulary);
- store.dispatch(loadFileViewersConfig);
-
- const TokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={true} {...props} />;
- const AddSessionComponent = (props: any) => <AddSession {...props} />;
- const FedTokenComponent = (props: any) => <ApiToken authService={services.authService} config={config} loadMainApp={false} {...props} />;
- const MainPanelComponent = (props: any) => <MainPanel {...props} />;
-
- const App = () =>
- <MuiThemeProvider theme={CustomTheme}>
- <DragDropContextProvider backend={HTML5Backend}>
- <Provider store={store}>
- <ConnectedRouter history={history}>
- <Switch>
- <Route path={Routes.TOKEN} component={TokenComponent} />
- <Route path={Routes.FED_LOGIN} component={FedTokenComponent} />
- <Route path={Routes.ADD_SESSION} component={AddSessionComponent} />
- <Route path={Routes.ROOT} component={MainPanelComponent} />
- </Switch>
- </ConnectedRouter>
- </Provider>
- </DragDropContextProvider>
- </MuiThemeProvider>;
-
- ReactDOM.render(
- <App />,
- document.getElementById('root') as HTMLElement
- );
+ },
});
+ // be sure this is initiated before the app starts
+ servicesProvider.setServices(services);
+
+ const store = configureStore(history, services, config);
+
+ servicesProvider.setStore(store);
+
+ store.subscribe(initListener(history, store, services, config));
+ store.dispatch(initAuth(config));
+ store.dispatch(setBuildInfo());
+ store.dispatch(setTokenDialogApiHost(apiHost));
+ store.dispatch(loadVocabulary);
+ store.dispatch(loadFileViewersConfig);
+
+ const TokenComponent = (props: any) => (
+ <ApiToken
+ authService={services.authService}
+ config={config}
+ loadMainApp={true}
+ {...props}
+ />
+ );
+ const AddSessionComponent = (props: any) => <AddSession {...props} />;
+ const FedTokenComponent = (props: any) => (
+ <ApiToken
+ authService={services.authService}
+ config={config}
+ loadMainApp={false}
+ {...props}
+ />
+ );
+ const MainPanelComponent = (props: any) => <MainPanel {...props} />;
+
+ const App = () => (
+ <MuiThemeProvider theme={CustomTheme}>
+ <DragDropContextProvider backend={HTML5Backend}>
+ <Provider store={store}>
+ <ConnectedRouter history={history}>
+ <Switch>
+ <Route
+ path={Routes.TOKEN}
+ component={TokenComponent}
+ />
+ <Route
+ path={Routes.FED_LOGIN}
+ component={FedTokenComponent}
+ />
+ <Route
+ path={Routes.ADD_SESSION}
+ component={AddSessionComponent}
+ />
+ <Route
+ path={Routes.ROOT}
+ component={MainPanelComponent}
+ />
+ </Switch>
+ </ConnectedRouter>
+ </Provider>
+ </DragDropContextProvider>
+ </MuiThemeProvider>
+ );
+
+ ReactDOM.render(<App />, document.getElementById("root") as HTMLElement);
+});
+
const initListener = (history: History, store: RootStore, services: ServiceRepository, config: Config) => {
let initialized = false;
return async () => {
const mappedTree = Tree.mapTreeValues<string, number>(value => parseInt(value.split(' ')[1], 10))(newTree);
expect(Tree.getNode('Node 2')(mappedTree)).toEqual(initTreeNode({ id: 'Node 2', parent: 'Node 1', value: 2 }));
});
+
+ it('expands node ancestor chains', () => {
+ const newTree = [
+ initTreeNode({ id: 'Root Node 1', parent: '', value: 'Value 1' }),
+ initTreeNode({ id: 'Node 1.1', parent: 'Root Node 1', value: 'Value 1' }),
+ initTreeNode({ id: 'Node 1.1.1', parent: 'Node 1.1', value: 'Value 1' }),
+ initTreeNode({ id: 'Node 1.2', parent: 'Root Node 1', value: 'Value 1' }),
+
+ initTreeNode({ id: 'Root Node 2', parent: '', value: 'Value 1' }),
+ initTreeNode({ id: 'Node 2.1', parent: 'Root Node 2', value: 'Value 1' }),
+ initTreeNode({ id: 'Node 2.1.1', parent: 'Node 2.1', value: 'Value 1' }),
+
+ initTreeNode({ id: 'Root Node 3', parent: '', value: 'Value 1' }),
+ initTreeNode({ id: 'Node 3.1', parent: 'Root Node 3', value: 'Value 1' }),
+ ].reduce((tree, node) => Tree.setNode(node)(tree), tree);
+
+ const expandedTree = Tree.expandNodeAncestors(
+ 'Node 1.1.1', // Expands 1.1 and 1
+ 'Node 2.1', // Expands 2
+ )(newTree);
+
+ expect(Tree.getNode('Root Node 1')(expandedTree)?.expanded).toEqual(true);
+ expect(Tree.getNode('Node 1.1')(expandedTree)?.expanded).toEqual(true);
+ expect(Tree.getNode('Node 1.1.1')(expandedTree)?.expanded).toEqual(false);
+ expect(Tree.getNode('Node 1.2')(expandedTree)?.expanded).toEqual(false);
+ expect(Tree.getNode('Root Node 2')(expandedTree)?.expanded).toEqual(true);
+ expect(Tree.getNode('Node 2.1')(expandedTree)?.expanded).toEqual(false);
+ expect(Tree.getNode('Node 2.1.1')(expandedTree)?.expanded).toEqual(false);
+ expect(Tree.getNode('Root Node 3')(expandedTree)?.expanded).toEqual(false);
+ expect(Tree.getNode('Node 3.1')(expandedTree)?.expanded).toEqual(false);
+ });
});
export const expandNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+export const expandNodeAncestors = (...ids: string[]) => <T>(tree: Tree<T>) => {
+ const ancestors = ids.reduce((acc, id): string[] => ([...acc, ...getNodeAncestorsIds(id)(tree)]), [] as string[]);
+ return mapTree((node: TreeNode<T>) => ancestors.some(id => id === node.id) ? { ...node, expanded: true } : node)(tree);
+}
+
export const collapseNode = (...ids: string[]) => <T>(tree: Tree<T>) =>
mapTree((node: TreeNode<T>) => ids.some(id => id === node.id) ? { ...node, expanded: false } : node)(tree);
: tree;
};
-export const toggleNodeSelection = (id: string) => <T>(tree: Tree<T>) => {
+export const toggleNodeSelection = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
const node = getNode(id)(tree);
+
return node
- ? pipe(
- setNode({ ...node, selected: !node.selected }),
- toggleAncestorsSelection(id),
- toggleDescendantsSelection(id))(tree)
+ ? cascade
+ ? pipe(
+ setNode({ ...node, selected: !node.selected }),
+ toggleAncestorsSelection(id),
+ toggleDescendantsSelection(id))(tree)
+ : setNode({ ...node, selected: !node.selected })(tree)
: tree;
};
-export const selectNode = (id: string) => <T>(tree: Tree<T>) => {
+export const selectNode = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
const node = getNode(id)(tree);
return node && node.selected
? tree
- : toggleNodeSelection(id)(tree);
+ : toggleNodeSelection(id, cascade)(tree);
};
-export const selectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+export const selectNodes = (id: string | string[], cascade: boolean) => <T>(tree: Tree<T>) => {
const ids = typeof id === 'string' ? [id] : id;
- return ids.reduce((tree, id) => selectNode(id)(tree), tree);
+ return ids.reduce((tree, id) => selectNode(id, cascade)(tree), tree);
};
-export const deselectNode = (id: string) => <T>(tree: Tree<T>) => {
+export const deselectNode = (id: string, cascade: boolean) => <T>(tree: Tree<T>) => {
const node = getNode(id)(tree);
return node && node.selected
- ? toggleNodeSelection(id)(tree)
+ ? toggleNodeSelection(id, cascade)(tree)
: tree;
};
-export const deselectNodes = (id: string | string[]) => <T>(tree: Tree<T>) => {
+export const deselectNodes = (id: string | string[], cascade: boolean) => <T>(tree: Tree<T>) => {
const ids = typeof id === 'string' ? [id] : id;
- return ids.reduce((tree, id) => deselectNode(id)(tree), tree);
+ return ids.reduce((tree, id) => deselectNode(id, cascade)(tree), tree);
};
export const getSelectedNodes = <T>(tree: Tree<T>) =>
export const getFileFullPath = ({ name, path }: CollectionFile | CollectionDirectory) => {
return `${path}/${name}`;
-};
\ No newline at end of file
+};
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 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",
]);
}
}
update(uuid: string, data: Partial<CollectionResource>, showErrors?: boolean) {
- const select = [...Object.keys(data), 'version', 'modifiedAt'];
+ const select = [...Object.keys(data), "version", "modifiedAt"];
return super.update(uuid, { ...data, preserveVersion: true }, showErrors, select);
}
if (request.responseXML != null) {
return extractFilesData(request.responseXML);
}
+
return Promise.reject();
}
private combineFilePath(parts: string[]) {
return parts.reduce((path, part) => {
// Trim leading and trailing slashes
- const trimmedPart = part.split('/').filter(Boolean).join('/');
+ const trimmedPart = part.split("/").filter(Boolean).join("/");
if (trimmedPart.length) {
- const separator = path.endsWith('/') ? '' : '/';
+ const separator = path.endsWith("/") ? "" : "/";
return `${path}${separator}${trimmedPart}`;
} else {
return path;
// Don't send uuid in payload when creating
uuid: undefined,
},
- replace_files: fileMap
+ replace_files: fileMap,
};
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({uuid: collectionUuid}, {
- [this.combineFilePath([newPath])]: `${collectionPdh}${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.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.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 optimizedFiles = files
.sort((a, b) => a.length - b.length)
.reduce((acc, currentPath) => {
- const parentPathFound = acc.find((parentPath) => currentPath.indexOf(`${parentPath}/`) > -1);
+ const parentPathFound = acc.find(parentPath => currentPath.indexOf(`${parentPath}/`) > -1);
if (!parentPathFound) {
return [...acc, currentPath];
const fileMap = optimizedFiles.reduce((obj, filePath) => {
return {
...obj,
- [this.combineFilePath([filePath])]: ''
- }
- }, {})
+ [this.combineFilePath([filePath])]: "",
+ };
+ }, {});
- return this.replaceFiles({uuid: collectionUuid}, fileMap, showErrors);
+ return this.replaceFiles({ uuid: collectionUuid }, fileMap, showErrors);
}
- 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 fileBasename = sourceFile.split('/').filter(Boolean).slice(-1).join("");
+ const fileBasename = sourceFile.split("/").filter(Boolean).slice(-1).join("");
return {
...obj,
- [this.combineFilePath([destinationPath, fileBasename])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`
+ [this.combineFilePath([destinationPath, fileBasename])]: `${sourcePdh}${this.combineFilePath([sourceFile])}`,
};
}, {});
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[],
+ destinationCollection: CollectionPartialUpdateOrCreate,
+ destinationPath: string,
+ showErrors?: boolean
+ ) {
if (sourceUuid === destinationCollection.uuid) {
let errors: CommonResourceServiceError[] = [];
const fileMap = files.reduce((obj, 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);
}, {});
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, 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({uuid: collectionUuid}, fileMap, showErrors);
+ return this.replaceFiles({ uuid: collectionUuid }, fileMap, showErrors);
}
-
}
import { ApiActions } from "services/api/api-actions";
export class TrashableResourceService<T extends TrashableResource> extends CommonResourceService<T> {
-
constructor(serverApi: AxiosInstance, resourceType: string, actions: ApiActions, readOnlyFields: string[] = []) {
super(serverApi, resourceType, actions, readOnlyFields);
}
trash(uuid: string): Promise<T> {
- return CommonResourceService.defaultResponse(
- this.serverApi
- .post(this.resourceType + `/${uuid}/trash`),
- this.actions
- );
+ return CommonResourceService.defaultResponse(this.serverApi.post(this.resourceType + `/${uuid}/trash`), this.actions);
}
untrash(uuid: string): Promise<T> {
const params = {
- ensure_unique_name: true
+ ensure_unique_name: true,
};
return CommonResourceService.defaultResponse(
- this.serverApi
- .post(this.resourceType + `/${uuid}/untrash`, {
- params: CommonResourceService.mapKeys(snakeCase)(params)
- }),
- this.actions
+ this.serverApi.post(this.resourceType + `/${uuid}/untrash`, {
+ params: CommonResourceService.mapKeys(snakeCase)(params),
+ }),
+ this.actions,
+ undefined,
+ false
);
}
}
super(id);
}
- async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean, background?: boolean) {
const dataExplorer = getDataExplorer(api.getState().dataExplorer, this.getId());
if (!dataExplorer) {
api.dispatch(allProcessesPanelDataExplorerIsNotSet());
} else {
try {
- api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); }
const processItems = await this.services.containerRequestService.list(
{
...getParams(dataExplorer),
select: containerRequestFieldsNoMounts,
});
- api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
api.dispatch(resourcesActions.SET_RESOURCES(processItems.items));
await api.dispatch<any>(loadMissingProcessesInformation(processItems.items));
api.dispatch(allProcessesPanelActions.SET_ITEMS({
rowsPerPage: processItems.limit
}));
} catch {
- api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
api.dispatch(allProcessesPanelActions.SET_ITEMS({
items: [],
itemsAvailable: 0,
}
}
-const getParams = ( dataExplorer: DataExplorer ) => ({
+const getParams = (dataExplorer: DataExplorer) => ({
...dataExplorerToListParams(dataExplorer),
order: getOrder<ContainerRequestResource>(dataExplorer),
filters: getFilters(dataExplorer)
});
-const getFilters = ( dataExplorer: DataExplorer ) => {
+const getFilters = (dataExplorer: DataExplorer) => {
const columns = dataExplorer.columns as DataColumns<string, ContainerRequestResource>;
const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
const activeStatusFilter = Object.keys(statusColumnFilters).find(
import { CollectionResource } from 'models/collection';
import { getSidePanelIcon } from 'views-components/side-panel-tree/side-panel-tree';
import { WorkflowResource } from 'models/workflow';
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
export const BREADCRUMBS = 'breadcrumbs';
export const setSidePanelBreadcrumbs = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const ancestors = await services.ancestorsService.ancestors(uuid, '');
- dispatch(updateResources(ancestors));
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid + "-breadcrumbs"));
+ const ancestors = await services.ancestorsService.ancestors(uuid, '');
+ dispatch(updateResources(ancestors));
- let breadcrumbs: Breadcrumb[] = [];
- const { collectionPanel: { item } } = getState();
+ let breadcrumbs: Breadcrumb[] = [];
+ const { collectionPanel: { item } } = getState();
- const path = getState().router.location!.pathname;
- const currentUuid = path.split('/')[2];
- const uuidKind = extractUuidKind(currentUuid);
- const rootUuid = getUserUuid(getState());
+ const path = getState().router.location!.pathname;
+ const currentUuid = path.split('/')[2];
+ const uuidKind = extractUuidKind(currentUuid);
+ const rootUuid = getUserUuid(getState());
- if (ancestors.find(ancestor => ancestor.uuid === rootUuid)) {
- // Handle home project uuid root
- breadcrumbs.push({
- label: SidePanelTreeCategory.PROJECTS,
- uuid: SidePanelTreeCategory.PROJECTS,
- icon: getSidePanelIcon(SidePanelTreeCategory.PROJECTS)
- });
- } else if (Object.values(SidePanelTreeCategory).includes(uuid as SidePanelTreeCategory)) {
- // Handle SidePanelTreeCategory root
- breadcrumbs.push({
- label: uuid,
- uuid: uuid,
- icon: getSidePanelIcon(uuid)
- });
- }
+ if (ancestors.find(ancestor => ancestor.uuid === rootUuid)) {
+ // Handle home project uuid root
+ breadcrumbs.push({
+ label: SidePanelTreeCategory.PROJECTS,
+ uuid: SidePanelTreeCategory.PROJECTS,
+ icon: getSidePanelIcon(SidePanelTreeCategory.PROJECTS)
+ });
+ } else if (Object.values(SidePanelTreeCategory).includes(uuid as SidePanelTreeCategory)) {
+ // Handle SidePanelTreeCategory root
+ breadcrumbs.push({
+ label: uuid,
+ uuid: uuid,
+ icon: getSidePanelIcon(uuid)
+ });
+ }
- breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
- ancestor.kind === ResourceKind.GROUP
- ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
- : breadcrumbs,
- breadcrumbs);
+ breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
+ ancestor.kind === ResourceKind.GROUP
+ ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
+ : breadcrumbs,
+ breadcrumbs);
- if (uuidKind === ResourceKind.COLLECTION) {
- const collectionItem = item ? item : await services.collectionService.get(currentUuid);
- const parentProcessItem = await getCollectionParent(collectionItem)(services);
- if (parentProcessItem) {
- const mainProcessItem = await getProcessParent(parentProcessItem)(services);
- mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
- breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
- }
- dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
- } else if (uuidKind === ResourceKind.PROCESS) {
- const processItem = await services.containerRequestService.get(currentUuid);
- const parentProcessItem = await getProcessParent(processItem)(services);
- if (parentProcessItem) {
- breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ if (uuidKind === ResourceKind.COLLECTION) {
+ const collectionItem = item ? item : await services.collectionService.get(currentUuid);
+ const parentProcessItem = await getCollectionParent(collectionItem)(services);
+ if (parentProcessItem) {
+ const mainProcessItem = await getProcessParent(parentProcessItem)(services);
+ mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
+ dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
+ } else if (uuidKind === ResourceKind.PROCESS) {
+ const processItem = await services.containerRequestService.get(currentUuid);
+ const parentProcessItem = await getProcessParent(processItem)(services);
+ if (parentProcessItem) {
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
+ dispatch(setBreadcrumbs(breadcrumbs, processItem));
+ } else if (uuidKind === ResourceKind.WORKFLOW) {
+ const workflowItem = await services.workflowService.get(currentUuid);
+ dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
}
- dispatch(setBreadcrumbs(breadcrumbs, processItem));
- } else if (uuidKind === ResourceKind.WORKFLOW) {
- const workflowItem = await services.workflowService.get(currentUuid);
- dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
+ dispatch(setBreadcrumbs(breadcrumbs));
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid + "-breadcrumbs"));
}
- dispatch(setBreadcrumbs(breadcrumbs));
};
export const setSharedWithMeBreadcrumbs = (uuid: string) =>
export const setCategoryBreadcrumbs = (uuid: string, category: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const ancestors = await services.ancestorsService.ancestors(uuid, '');
- dispatch(updateResources(ancestors));
- const initialBreadcrumbs: Breadcrumb[] = [
- {
- label: category,
- uuid: category,
- icon: getSidePanelIcon(category)
- }
- ];
- const { collectionPanel: { item } } = getState();
- const path = getState().router.location!.pathname;
- const currentUuid = path.split('/')[2];
- const uuidKind = extractUuidKind(currentUuid);
- let breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
- ancestor.kind === ResourceKind.GROUP
- ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
- : breadcrumbs,
- initialBreadcrumbs);
- if (uuidKind === ResourceKind.COLLECTION) {
- const collectionItem = item ? item : await services.collectionService.get(currentUuid);
- const parentProcessItem = await getCollectionParent(collectionItem)(services);
- if (parentProcessItem) {
- const mainProcessItem = await getProcessParent(parentProcessItem)(services);
- mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
- breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
- }
- dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
- } else if (uuidKind === ResourceKind.PROCESS) {
- const processItem = await services.containerRequestService.get(currentUuid);
- const parentProcessItem = await getProcessParent(processItem)(services);
- if (parentProcessItem) {
- breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid + "-breadcrumbs"));
+ const ancestors = await services.ancestorsService.ancestors(uuid, '');
+ dispatch(updateResources(ancestors));
+ const initialBreadcrumbs: Breadcrumb[] = [
+ {
+ label: category,
+ uuid: category,
+ icon: getSidePanelIcon(category)
+ }
+ ];
+ const { collectionPanel: { item } } = getState();
+ const path = getState().router.location!.pathname;
+ const currentUuid = path.split('/')[2];
+ const uuidKind = extractUuidKind(currentUuid);
+ let breadcrumbs = ancestors.reduce((breadcrumbs, ancestor) =>
+ ancestor.kind === ResourceKind.GROUP
+ ? [...breadcrumbs, resourceToBreadcrumb(ancestor)]
+ : breadcrumbs,
+ initialBreadcrumbs);
+ if (uuidKind === ResourceKind.COLLECTION) {
+ const collectionItem = item ? item : await services.collectionService.get(currentUuid);
+ const parentProcessItem = await getCollectionParent(collectionItem)(services);
+ if (parentProcessItem) {
+ const mainProcessItem = await getProcessParent(parentProcessItem)(services);
+ mainProcessItem && breadcrumbs.push(resourceToBreadcrumb(mainProcessItem));
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
+ dispatch(setBreadcrumbs(breadcrumbs, collectionItem));
+ } else if (uuidKind === ResourceKind.PROCESS) {
+ const processItem = await services.containerRequestService.get(currentUuid);
+ const parentProcessItem = await getProcessParent(processItem)(services);
+ if (parentProcessItem) {
+ breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
+ }
+ dispatch(setBreadcrumbs(breadcrumbs, processItem));
+ } else if (uuidKind === ResourceKind.WORKFLOW) {
+ const workflowItem = await services.workflowService.get(currentUuid);
+ dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
}
- dispatch(setBreadcrumbs(breadcrumbs, processItem));
- } else if (uuidKind === ResourceKind.WORKFLOW) {
- const workflowItem = await services.workflowService.get(currentUuid);
- dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
+ dispatch(setBreadcrumbs(breadcrumbs));
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid + "-breadcrumbs"));
}
- dispatch(setBreadcrumbs(breadcrumbs));
};
const getProcessParent = (childProcess: ContainerRequestResource) =>
import { SnackbarKind } from 'store/snackbar/snackbar-actions';
import { navigateTo } from 'store/navigation/navigation-action';
import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
export const collectionPanelActions = unionize({
SET_COLLECTION: ofType<CollectionResource>(),
const { collectionPanel: { item } } = getState();
let collection: CollectionResource | null = null;
if (!item || item.uuid !== uuid || forceReload) {
- collection = await services.collectionService.get(uuid);
- dispatch(collectionPanelActions.SET_COLLECTION(collection));
- dispatch(resourcesActions.SET_RESOURCES([collection]));
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid + "-panel"));
+ collection = await services.collectionService.get(uuid);
+ dispatch(collectionPanelActions.SET_COLLECTION(collection));
+ dispatch(resourcesActions.SET_RESOURCES([collection]));
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid + "-panel"));
+ }
} else {
collection = item;
}
import { Dispatch } from "redux";
import { dialogActions } from "store/dialog/dialog-actions";
-import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form';
-import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
-import { RootState } from 'store/store';
-import { ServiceRepository } from 'services/services';
-import { getCommonResourceServiceError, CommonResourceServiceError } from 'services/common-service/common-resource-service';
-import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
+import { FormErrors, initialize, startSubmit, stopSubmit } from "redux-form";
+import { resetPickerProjectTree } from "store/project-tree-picker/project-tree-picker-actions";
+import { RootState } from "store/store";
+import { ServiceRepository } from "services/services";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "services/common-service/common-resource-service";
+import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
-import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
import { getResource } from "store/resources/resources";
import { CollectionResource } from "models/collection";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-export const COLLECTION_COPY_FORM_NAME = 'collectionCopyFormName';
+export const COLLECTION_COPY_FORM_NAME = "collectionCopyFormName";
+export const COLLECTION_MULTI_COPY_FORM_NAME = "collectionMultiCopyFormName";
-export const openCollectionCopyDialog = (resource: { name: string, uuid: string }) =>
- (dispatch: Dispatch) => {
- dispatch<any>(resetPickerProjectTree());
- dispatch<any>(initProjectsTreePicker(COLLECTION_COPY_FORM_NAME));
- const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: '', uuid: resource.uuid };
- dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
- dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
- };
+export const openCollectionCopyDialog = (resource: { name: string; uuid: string; fromContextMenu?: boolean }) => (dispatch: Dispatch) => {
+ dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(COLLECTION_COPY_FORM_NAME));
+ const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: "", uuid: resource.uuid, fromContextMenu: resource.fromContextMenu };
+ dispatch<any>(initialize(COLLECTION_COPY_FORM_NAME, initialData));
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_COPY_FORM_NAME, data: {} }));
+};
+
+export const openMultiCollectionCopyDialog = (resource: { name: string; uuid: string; fromContextMenu?: boolean }) => (dispatch: Dispatch) => {
+ dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(COLLECTION_MULTI_COPY_FORM_NAME));
+ const initialData: CopyFormDialogData = { name: `Copy of: ${resource.name}`, ownerUuid: "", uuid: resource.uuid, fromContextMenu: resource.fromContextMenu };
+ dispatch<any>(initialize(COLLECTION_MULTI_COPY_FORM_NAME, initialData));
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_MULTI_COPY_FORM_NAME, data: {} }));
+};
-export const copyCollection = (resource: CopyFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(startSubmit(COLLECTION_COPY_FORM_NAME));
+export const copyCollection =
+ (resource: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const formName = resource.fromContextMenu ? COLLECTION_COPY_FORM_NAME : COLLECTION_MULTI_COPY_FORM_NAME;
+ dispatch(startSubmit(formName));
let collection = getResource<CollectionResource>(resource.uuid)(getState().resources);
try {
if (!collection) {
collection = await services.collectionService.get(resource.uuid);
}
- const collManifestText = await services.collectionService.get(resource.uuid, undefined, ['manifestText']);
+ const collManifestText = await services.collectionService.get(resource.uuid, undefined, ["manifestText"]);
collection.manifestText = collManifestText.manifestText;
- const {href, ...collectionRecord} = collection;
- const newCollection = await services.collectionService.create({ ...collectionRecord, ownerUuid: resource.ownerUuid, name: resource.name });
- dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
+ const { href, ...collectionRecord } = collection;
+ const newCollection = await services.collectionService.create(
+ {
+ ...collectionRecord,
+ ownerUuid: resource.ownerUuid,
+ name: resource.name,
+ },
+ false
+ );
+ dispatch(dialogActions.CLOSE_DIALOG({ id: formName }));
return newCollection;
} catch (e) {
+ console.error("Error while copying collection: ", e);
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
- dispatch(stopSubmit(
- COLLECTION_COPY_FORM_NAME,
- { ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors
- ));
+ dispatch(
+ stopSubmit(formName, {
+ ownerUuid: "A collection with the same name already exists in the target project.",
+ } as FormErrors)
+ );
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Could not copy the collection.",
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
} else {
- dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_COPY_FORM_NAME }));
- throw new Error('Could not copy the collection.');
+ dispatch(dialogActions.CLOSE_DIALOG({ id: formName }));
+ throw new Error("Could not copy the collection.");
}
return;
} finally {
- dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_COPY_FORM_NAME));
+ dispatch(progressIndicatorActions.STOP_WORKING(formName));
}
};
import { Dispatch } from "redux";
import { dialogActions } from "store/dialog/dialog-actions";
-import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
-import { ServiceRepository } from 'services/services';
-import { RootState } from 'store/store';
+import { startSubmit, stopSubmit, initialize, FormErrors } from "redux-form";
+import { ServiceRepository } from "services/services";
+import { RootState } from "store/store";
import { getCommonResourceServiceError, CommonResourceServiceError } from "services/common-service/common-resource-service";
-import {snackbarActions, SnackbarKind} from 'store/snackbar/snackbar-actions';
-import { projectPanelActions } from 'store/project-panel/project-panel-action';
-import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
-import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
+import { resetPickerProjectTree } from "store/project-tree-picker/project-tree-picker-actions";
import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
-import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
import { getResource } from "store/resources/resources";
import { CollectionResource } from "models/collection";
-export const COLLECTION_MOVE_FORM_NAME = 'collectionMoveFormName';
+export const COLLECTION_MOVE_FORM_NAME = "collectionMoveFormName";
-export const openMoveCollectionDialog = (resource: { name: string, uuid: string }) =>
- (dispatch: Dispatch) => {
- dispatch<any>(resetPickerProjectTree());
- dispatch<any>(initProjectsTreePicker(COLLECTION_MOVE_FORM_NAME));
- dispatch(initialize(COLLECTION_MOVE_FORM_NAME, resource));
- dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_MOVE_FORM_NAME, data: {} }));
- };
+export const openMoveCollectionDialog = (resource: { name: string; uuid: string }) => (dispatch: Dispatch) => {
+ dispatch<any>(resetPickerProjectTree());
+ dispatch<any>(initProjectsTreePicker(COLLECTION_MOVE_FORM_NAME));
+ dispatch(initialize(COLLECTION_MOVE_FORM_NAME, resource));
+ dispatch(dialogActions.OPEN_DIALOG({ id: COLLECTION_MOVE_FORM_NAME, data: {} }));
+};
-export const moveCollection = (resource: MoveToFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const moveCollection =
+ (resource: MoveToFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(COLLECTION_MOVE_FORM_NAME));
let cachedCollection = getResource<CollectionResource>(resource.uuid)(getState().resources);
try {
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
- return {...cachedCollection, ...collection};
+ return { ...cachedCollection, ...collection };
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
- dispatch(stopSubmit(COLLECTION_MOVE_FORM_NAME, { ownerUuid: 'A collection with the same name already exists in the target project.' } as FormErrors));
+ dispatch(
+ stopSubmit(COLLECTION_MOVE_FORM_NAME, {
+ ownerUuid: "A collection with the same name already exists in the target project.",
+ } as FormErrors)
+ );
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_MOVE_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the collection.', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not move the collection.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
dispatch(progressIndicatorActions.STOP_WORKING(COLLECTION_MOVE_FORM_NAME));
return;
dispatch(progressIndicatorActions.START_WORKING(COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION));
// Copy files
- const updatedCollection = await services.collectionService.copyFiles(fileSelection.collection.portableDataHash, fileSelection.selectedPaths, {uuid: formData.destination.uuid}, formData.destination.path || '/', false);
+ const updatedCollection = await services.collectionService.copyFiles(
+ fileSelection.collection.portableDataHash,
+ fileSelection.selectedPaths,
+ {uuid: formData.destination.uuid},
+ formData.destination.subpath || '/',
+ false
+ );
dispatch(updateResources([updatedCollection]));
dispatch(dialogActions.CLOSE_DIALOG({ id: COLLECTION_PARTIAL_COPY_TO_SELECTED_COLLECTION }));
fileSelection.collection.portableDataHash,
fileSelection.selectedPaths,
{uuid: formData.destination.uuid},
- formData.destination.path || '/', false
+ formData.destination.subpath || '/', false
);
dispatch(updateResources([updatedCollection]));
//
// 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, isProcessCancelable } 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 { getProcess } from "store/processes/process";
+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>;
uuid: string;
ownerUuid: string;
description?: string;
- kind: ResourceKind,
+ kind: ResourceKind;
menuKind: ContextMenuKind | string;
isTrashed?: boolean;
isEditable?: boolean;
isFrozen?: boolean;
storageClassesDesired?: string[];
properties?: { [key: string]: string | string[] };
+ isMulti?: boolean;
+ fromContextMenu?: boolean;
};
export const isKeyboardClick = (event: React.MouseEvent<HTMLElement>) => event.nativeEvent.detail === 0;
-export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) =>
- (dispatch: Dispatch) => {
- event.preventDefault();
- const { left, top } = event.currentTarget.getBoundingClientRect();
- dispatch(
- contextMenuActions.OPEN_CONTEXT_MENU({
- position: {
- x: event.clientX || left,
- y: event.clientY || top,
- },
- resource
- })
- );
- };
+export const openContextMenu = (event: React.MouseEvent<HTMLElement>, resource: ContextMenuResource) => (dispatch: Dispatch) => {
+ event.preventDefault();
+ const { left, top } = event.currentTarget.getBoundingClientRect();
+ dispatch(
+ contextMenuActions.OPEN_CONTEXT_MENU({
+ position: {
+ x: event.clientX || left,
+ y: event.clientY || top,
+ },
+ resource,
+ })
+ );
+};
-export const openCollectionFilesContextMenu = (event: React.MouseEvent<HTMLElement>, isWritable: boolean) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+export const openCollectionFilesContextMenu =
+ (event: React.MouseEvent<HTMLElement>, isWritable: boolean) => (dispatch: Dispatch, getState: () => RootState) => {
const selectedCount = filterCollectionFilesBySelection(getState().collectionPanelFiles, true).length;
const multiple = selectedCount > 1;
- dispatch<any>(openContextMenu(event, {
- name: '',
- uuid: '',
- ownerUuid: '',
- description: '',
- kind: ResourceKind.COLLECTION,
- menuKind: selectedCount > 0
- ? isWritable
- ? multiple ? ContextMenuKind.COLLECTION_FILES_MULTIPLE : ContextMenuKind.COLLECTION_FILES
- : multiple ? ContextMenuKind.READONLY_COLLECTION_FILES_MULTIPLE : ContextMenuKind.READONLY_COLLECTION_FILES
- : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED
- }));
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
+ uuid: "",
+ ownerUuid: "",
+ description: "",
+ kind: ResourceKind.COLLECTION,
+ menuKind:
+ selectedCount > 0
+ ? isWritable
+ ? multiple
+ ? ContextMenuKind.COLLECTION_FILES_MULTIPLE
+ : ContextMenuKind.COLLECTION_FILES
+ : multiple
+ ? ContextMenuKind.READONLY_COLLECTION_FILES_MULTIPLE
+ : ContextMenuKind.READONLY_COLLECTION_FILES
+ : ContextMenuKind.COLLECTION_FILES_NOT_SELECTED,
+ })
+ );
};
-export const openRepositoryContextMenu = (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- dispatch<any>(openContextMenu(event, {
- name: '',
- uuid: repository.uuid,
- ownerUuid: repository.ownerUuid,
- kind: ResourceKind.REPOSITORY,
- menuKind: ContextMenuKind.REPOSITORY
- }));
+export const openRepositoryContextMenu =
+ (event: React.MouseEvent<HTMLElement>, repository: RepositoryResource) => (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
+ uuid: repository.uuid,
+ ownerUuid: repository.ownerUuid,
+ kind: ResourceKind.REPOSITORY,
+ menuKind: ContextMenuKind.REPOSITORY,
+ })
+ );
};
-export const openVirtualMachinesContextMenu = (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- dispatch<any>(openContextMenu(event, {
- name: '',
- uuid: repository.uuid,
- ownerUuid: repository.ownerUuid,
- kind: ResourceKind.VIRTUAL_MACHINE,
- menuKind: ContextMenuKind.VIRTUAL_MACHINE
- }));
+export const openVirtualMachinesContextMenu =
+ (event: React.MouseEvent<HTMLElement>, repository: VirtualMachinesResource) => (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
+ uuid: repository.uuid,
+ ownerUuid: repository.ownerUuid,
+ kind: ResourceKind.VIRTUAL_MACHINE,
+ menuKind: ContextMenuKind.VIRTUAL_MACHINE,
+ })
+ );
};
-export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) =>
- (dispatch: Dispatch) => {
- dispatch<any>(openContextMenu(event, {
- name: '',
+export const openSshKeyContextMenu = (event: React.MouseEvent<HTMLElement>, sshKey: SshKeyResource) => (dispatch: Dispatch) => {
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
uuid: sshKey.uuid,
ownerUuid: sshKey.ownerUuid,
kind: ResourceKind.SSH_KEY,
- menuKind: ContextMenuKind.SSH_KEY
- }));
- };
+ menuKind: ContextMenuKind.SSH_KEY,
+ })
+ );
+};
-export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) =>
- (dispatch: Dispatch) => {
- dispatch<any>(openContextMenu(event, {
- name: '',
+export const openKeepServiceContextMenu = (event: React.MouseEvent<HTMLElement>, keepService: KeepServiceResource) => (dispatch: Dispatch) => {
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
uuid: keepService.uuid,
ownerUuid: keepService.ownerUuid,
kind: ResourceKind.KEEP_SERVICE,
- menuKind: ContextMenuKind.KEEP_SERVICE
- }));
- };
+ menuKind: ContextMenuKind.KEEP_SERVICE,
+ })
+ );
+};
-export const openApiClientAuthorizationContextMenu =
- (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
- (dispatch: Dispatch) => {
- dispatch<any>(openContextMenu(event, {
- name: '',
- uuid: resourceUuid,
- ownerUuid: '',
- kind: ResourceKind.API_CLIENT_AUTHORIZATION,
- menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION
- }));
- };
+export const openApiClientAuthorizationContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch) => {
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
+ uuid: resourceUuid,
+ ownerUuid: "",
+ kind: ResourceKind.API_CLIENT_AUTHORIZATION,
+ menuKind: ContextMenuKind.API_CLIENT_AUTHORIZATION,
+ })
+ );
+};
-export const openRootProjectContextMenu = (event: React.MouseEvent<HTMLElement>, projectUuid: string) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+export const openRootProjectContextMenu =
+ (event: React.MouseEvent<HTMLElement>, projectUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
const res = getResource<UserResource>(projectUuid)(getState().resources);
if (res) {
- dispatch<any>(openContextMenu(event, {
- name: '',
- uuid: res.uuid,
- ownerUuid: res.uuid,
- kind: res.kind,
- menuKind: ContextMenuKind.ROOT_PROJECT,
- isTrashed: false
- }));
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
+ uuid: res.uuid,
+ ownerUuid: res.uuid,
+ kind: res.kind,
+ menuKind: ContextMenuKind.ROOT_PROJECT,
+ isTrashed: false,
+ })
+ );
}
};
-export const openProjectContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+export const openProjectContextMenu =
+ (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
const res = getResource<GroupContentsResource>(resourceUuid)(getState().resources);
const menuKind = dispatch<any>(resourceUuidToContextMenuKind(resourceUuid));
if (res && menuKind) {
- dispatch<any>(openContextMenu(event, {
- name: res.name,
- uuid: res.uuid,
- kind: res.kind,
- menuKind,
- description: res.description,
- ownerUuid: res.ownerUuid,
- isTrashed: ('isTrashed' in res) ? res.isTrashed : false,
- isFrozen: !!(res as ProjectResource).frozenByUuid,
- }));
+ dispatch<any>(
+ openContextMenu(event, {
+ name: res.name,
+ uuid: res.uuid,
+ kind: res.kind,
+ menuKind,
+ description: res.description,
+ ownerUuid: res.ownerUuid,
+ isTrashed: "isTrashed" in res ? res.isTrashed : false,
+ isFrozen: !!(res as ProjectResource).frozenByUuid,
+ })
+ );
}
};
-export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- if (!isSidePanelTreeCategory(id)) {
- const kind = extractUuidKind(id);
- if (kind === ResourceKind.USER) {
- dispatch<any>(openRootProjectContextMenu(event, id));
- } else if (kind === ResourceKind.PROJECT) {
- dispatch<any>(openProjectContextMenu(event, id));
- }
+export const openSidePanelContextMenu = (event: React.MouseEvent<HTMLElement>, id: string) => (dispatch: Dispatch, getState: () => RootState) => {
+ if (!isSidePanelTreeCategory(id)) {
+ const kind = extractUuidKind(id);
+ if (kind === ResourceKind.USER) {
+ dispatch<any>(openRootProjectContextMenu(event, id));
+ } else if (kind === ResourceKind.PROJECT) {
+ dispatch<any>(openProjectContextMenu(event, id));
}
- };
+ }
+};
-export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
- if (res) {
- dispatch<any>(openContextMenu(event, {
+export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, process: Process) => (dispatch: Dispatch, getState: () => RootState) => {
+ const res = getResource<ProcessResource>(process.containerRequest.uuid)(getState().resources);
+ if (res) {
+ dispatch<any>(
+ openContextMenu(event, {
uuid: res.uuid,
ownerUuid: res.ownerUuid,
kind: ResourceKind.PROCESS,
name: res.name,
description: res.description,
- outputUuid: res.outputUuid || '',
- workflowUuid: res.properties.template_uuid || '',
- menuKind: ContextMenuKind.PROCESS_RESOURCE
- }));
- }
- };
+ outputUuid: res.outputUuid || "",
+ workflowUuid: res.properties.template_uuid || "",
+ menuKind: isProcessCancelable(process) ? ContextMenuKind.RUNNING_PROCESS_RESOURCE : ContextMenuKind.PROCESS_RESOURCE
+ })
+ );
+ }
+};
-export const openPermissionEditContextMenu = (event: React.MouseEvent<HTMLElement>, link: LinkResource) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+export const openPermissionEditContextMenu =
+ (event: React.MouseEvent<HTMLElement>, link: LinkResource) => (dispatch: Dispatch, getState: () => RootState) => {
if (link) {
- dispatch<any>(openContextMenu(event, {
- name: link.name,
- uuid: link.uuid,
- kind: link.kind,
- menuKind: ContextMenuKind.PERMISSION_EDIT,
- ownerUuid: link.ownerUuid,
- }));
+ dispatch<any>(
+ openContextMenu(event, {
+ name: link.name,
+ uuid: link.uuid,
+ kind: link.kind,
+ menuKind: ContextMenuKind.PERMISSION_EDIT,
+ ownerUuid: link.ownerUuid,
+ })
+ );
}
};
-export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- dispatch<any>(openContextMenu(event, {
- name: '',
+export const openUserContextMenu = (event: React.MouseEvent<HTMLElement>, user: UserResource) => (dispatch: Dispatch, getState: () => RootState) => {
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
uuid: user.uuid,
ownerUuid: user.ownerUuid,
kind: user.kind,
- menuKind: ContextMenuKind.USER
- }));
- };
+ menuKind: ContextMenuKind.USER,
+ })
+ );
+};
-export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
+export const resourceUuidToContextMenuKind =
+ (uuid: string, readonly = false) =>
(dispatch: Dispatch, getState: () => RootState) => {
const { isAdmin: isAdminUser, uuid: userUuid } = getState().auth.user!;
const kind = extractUuidKind(uuid);
const resource = getResourceWithEditableStatus<GroupResource & EditableResource>(uuid, userUuid)(getState().resources);
const isFrozen = resourceIsFrozen(resource, getState().resources);
- const isEditable = (isAdminUser || (resource || {} as EditableResource).isEditable) && !readonly && !isFrozen;
+ const isEditable = (isAdminUser || (resource || ({} as EditableResource)).isEditable) && !readonly && !isFrozen;
switch (kind) {
case ResourceKind.PROJECT:
return isAdminUser ? ContextMenuKind.FROZEN_PROJECT_ADMIN : ContextMenuKind.FROZEN_PROJECT;
}
- return (isAdminUser && !readonly)
- ? (resource && resource.groupClass !== GroupClass.FILTER)
+ return isAdminUser && !readonly
+ ? resource && resource.groupClass !== GroupClass.FILTER
? ContextMenuKind.PROJECT_ADMIN
: ContextMenuKind.FILTER_GROUP_ADMIN
: isEditable
- ? (resource && resource.groupClass !== GroupClass.FILTER)
- ? ContextMenuKind.PROJECT
- : ContextMenuKind.FILTER_GROUP
- : ContextMenuKind.READONLY_PROJECT;
+ ? resource && resource.groupClass !== GroupClass.FILTER
+ ? ContextMenuKind.PROJECT
+ : ContextMenuKind.FILTER_GROUP
+ : ContextMenuKind.READONLY_PROJECT;
case ResourceKind.COLLECTION:
const c = getResource<CollectionResource>(uuid)(getState().resources);
- if (c === undefined) { return; }
+ if (c === undefined) {
+ return;
+ }
const isOldVersion = c.uuid !== c.currentVersionUuid;
const isTrashed = c.isTrashed;
return isOldVersion
? ContextMenuKind.OLD_VERSION_COLLECTION
- : (isTrashed && isEditable)
- ? ContextMenuKind.TRASHED_COLLECTION
- : (isAdminUser && isEditable)
- ? ContextMenuKind.COLLECTION_ADMIN
- : isEditable
- ? ContextMenuKind.COLLECTION
- : ContextMenuKind.READONLY_COLLECTION;
+ : isTrashed && isEditable
+ ? ContextMenuKind.TRASHED_COLLECTION
+ : isAdminUser && isEditable
+ ? ContextMenuKind.COLLECTION_ADMIN
+ : isEditable
+ ? ContextMenuKind.COLLECTION
+ : ContextMenuKind.READONLY_COLLECTION;
case ResourceKind.PROCESS:
- return (isAdminUser && isEditable)
- ? ContextMenuKind.PROCESS_ADMIN
+ return isAdminUser && isEditable
+ ? resource && isProcessCancelable(getProcess(resource.uuid)(getState().resources) as Process)
+ ? ContextMenuKind.RUNNING_PROCESS_ADMIN
+ : ContextMenuKind.PROCESS_ADMIN
: readonly
- ? ContextMenuKind.READONLY_PROCESS_RESOURCE
- : ContextMenuKind.PROCESS_RESOURCE;
+ ? ContextMenuKind.READONLY_PROCESS_RESOURCE
+ : resource && isProcessCancelable(getProcess(resource.uuid)(getState().resources) as Process)
+ ? ContextMenuKind.RUNNING_PROCESS_RESOURCE
+ : ContextMenuKind.PROCESS_RESOURCE;
case ResourceKind.USER:
return ContextMenuKind.ROOT_PROJECT;
case ResourceKind.LINK:
}
};
-export const openSearchResultsContextMenu = (event: React.MouseEvent<HTMLElement>, uuid: string) =>
- (dispatch: Dispatch, getState: () => RootState) => {
+export const openSearchResultsContextMenu =
+ (event: React.MouseEvent<HTMLElement>, uuid: string) => (dispatch: Dispatch, getState: () => RootState) => {
const res = getResource<Resource>(uuid)(getState().resources);
if (res) {
- dispatch<any>(openContextMenu(event, {
- name: '',
- uuid: res.uuid,
- ownerUuid: '',
- kind: res.kind,
- menuKind: ContextMenuKind.SEARCH_RESULTS,
- }));
+ dispatch<any>(
+ openContextMenu(event, {
+ name: "",
+ uuid: res.uuid,
+ ownerUuid: "",
+ kind: res.kind,
+ menuKind: ContextMenuKind.SEARCH_RESULTS,
+ })
+ );
}
};
name: string;
uuid: string;
ownerUuid: string;
-}
\ No newline at end of file
+ fromContextMenu?: boolean;
+}
import { unionize, ofType, UnionOf } from "common/unionize";
import { DataColumns, DataTableFetchMode } from "components/data-table/data-table";
-import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
+import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
export enum DataTableRequestState {
IDLE,
PENDING,
- NEED_REFRESH
+ NEED_REFRESH,
}
export const dataExplorerActions = unionize({
CLEAR: ofType<{ id: string }>(),
RESET_PAGINATION: ofType<{ id: string }>(),
- REQUEST_ITEMS: ofType<{ id: string, criteriaChanged?: boolean }>(),
- REQUEST_STATE: ofType<{ id: string, criteriaChanged?: boolean }>(),
- SET_FETCH_MODE: ofType<({ id: string, fetchMode: DataTableFetchMode })>(),
- SET_COLUMNS: ofType<{ id: string, columns: DataColumns<any, any> }>(),
- SET_FILTERS: ofType<{ id: string, columnName: string, filters: DataTableFilters }>(),
- SET_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
- APPEND_ITEMS: ofType<{ id: string, items: any[], page: number, rowsPerPage: number, itemsAvailable: number }>(),
- SET_PAGE: ofType<{ id: string, page: number }>(),
- SET_ROWS_PER_PAGE: ofType<{ id: string, rowsPerPage: number }>(),
- TOGGLE_COLUMN: ofType<{ id: string, columnName: string }>(),
- TOGGLE_SORT: ofType<{ id: string, columnName: string }>(),
- SET_EXPLORER_SEARCH_VALUE: ofType<{ id: string, searchValue: string }>(),
+ REQUEST_ITEMS: ofType<{ id: string; criteriaChanged?: boolean, background?: boolean }>(),
+ REQUEST_STATE: ofType<{ id: string; criteriaChanged?: boolean }>(),
+ SET_FETCH_MODE: ofType<{ id: string; fetchMode: DataTableFetchMode }>(),
+ SET_COLUMNS: ofType<{ id: string; columns: DataColumns<any, any> }>(),
+ SET_FILTERS: ofType<{ id: string; columnName: string; filters: DataTableFilters }>(),
+ SET_ITEMS: ofType<{ id: string; items: any[]; page: number; rowsPerPage: number; itemsAvailable: number }>(),
+ APPEND_ITEMS: ofType<{ id: string; items: any[]; page: number; rowsPerPage: number; itemsAvailable: number }>(),
+ SET_PAGE: ofType<{ id: string; page: number }>(),
+ SET_ROWS_PER_PAGE: ofType<{ id: string; rowsPerPage: number }>(),
+ TOGGLE_COLUMN: ofType<{ id: string; columnName: string }>(),
+ TOGGLE_SORT: ofType<{ id: string; columnName: string }>(),
+ SET_EXPLORER_SEARCH_VALUE: ofType<{ id: string; searchValue: string }>(),
RESET_EXPLORER_SEARCH_VALUE: ofType<{ id: string }>(),
- SET_REQUEST_STATE: ofType<{ id: string, requestState: DataTableRequestState }>(),
+ SET_REQUEST_STATE: ofType<{ id: string; requestState: DataTableRequestState }>(),
});
export type DataExplorerAction = UnionOf<typeof dataExplorerActions>;
export const bindDataExplorerActions = (id: string) => ({
- CLEAR: () =>
- dataExplorerActions.CLEAR({ id }),
- RESET_PAGINATION: () =>
- dataExplorerActions.RESET_PAGINATION({ id }),
- REQUEST_ITEMS: (criteriaChanged?: boolean) =>
- dataExplorerActions.REQUEST_ITEMS({ id, criteriaChanged }),
- SET_FETCH_MODE: (payload: { fetchMode: DataTableFetchMode }) =>
- dataExplorerActions.SET_FETCH_MODE({ ...payload, id }),
- SET_COLUMNS: (payload: { columns: DataColumns<any, any> }) =>
- dataExplorerActions.SET_COLUMNS({ ...payload, id }),
- SET_FILTERS: (payload: { columnName: string, filters: DataTableFilters }) =>
- dataExplorerActions.SET_FILTERS({ ...payload, id }),
- SET_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
+ CLEAR: () => dataExplorerActions.CLEAR({ id }),
+ RESET_PAGINATION: () => dataExplorerActions.RESET_PAGINATION({ id }),
+ REQUEST_ITEMS: (criteriaChanged?: boolean, background?: boolean) => dataExplorerActions.REQUEST_ITEMS({ id, criteriaChanged, background }),
+ SET_FETCH_MODE: (payload: { fetchMode: DataTableFetchMode }) => dataExplorerActions.SET_FETCH_MODE({ ...payload, id }),
+ SET_COLUMNS: (payload: { columns: DataColumns<any, any> }) => dataExplorerActions.SET_COLUMNS({ ...payload, id }),
+ SET_FILTERS: (payload: { columnName: string; filters: DataTableFilters }) => dataExplorerActions.SET_FILTERS({ ...payload, id }),
+ SET_ITEMS: (payload: { items: any[]; page: number; rowsPerPage: number; itemsAvailable: number }) =>
dataExplorerActions.SET_ITEMS({ ...payload, id }),
- APPEND_ITEMS: (payload: { items: any[], page: number, rowsPerPage: number, itemsAvailable: number }) =>
+ APPEND_ITEMS: (payload: { items: any[]; page: number; rowsPerPage: number; itemsAvailable: number }) =>
dataExplorerActions.APPEND_ITEMS({ ...payload, id }),
- SET_PAGE: (payload: { page: number }) =>
- dataExplorerActions.SET_PAGE({ ...payload, id }),
- SET_ROWS_PER_PAGE: (payload: { rowsPerPage: number }) =>
- dataExplorerActions.SET_ROWS_PER_PAGE({ ...payload, id }),
- TOGGLE_COLUMN: (payload: { columnName: string }) =>
- dataExplorerActions.TOGGLE_COLUMN({ ...payload, id }),
- TOGGLE_SORT: (payload: { columnName: string }) =>
- dataExplorerActions.TOGGLE_SORT({ ...payload, id }),
- SET_EXPLORER_SEARCH_VALUE: (payload: { searchValue: string }) =>
- dataExplorerActions.SET_EXPLORER_SEARCH_VALUE({ ...payload, id }),
- RESET_EXPLORER_SEARCH_VALUE: () =>
- dataExplorerActions.RESET_EXPLORER_SEARCH_VALUE({ id }),
- SET_REQUEST_STATE: (payload: { requestState: DataTableRequestState }) =>
- dataExplorerActions.SET_REQUEST_STATE({ ...payload, id })
+ SET_PAGE: (payload: { page: number }) => dataExplorerActions.SET_PAGE({ ...payload, id }),
+ SET_ROWS_PER_PAGE: (payload: { rowsPerPage: number }) => dataExplorerActions.SET_ROWS_PER_PAGE({ ...payload, id }),
+ TOGGLE_COLUMN: (payload: { columnName: string }) => dataExplorerActions.TOGGLE_COLUMN({ ...payload, id }),
+ TOGGLE_SORT: (payload: { columnName: string }) => dataExplorerActions.TOGGLE_SORT({ ...payload, id }),
+ SET_EXPLORER_SEARCH_VALUE: (payload: { searchValue: string }) => dataExplorerActions.SET_EXPLORER_SEARCH_VALUE({ ...payload, id }),
+ RESET_EXPLORER_SEARCH_VALUE: () => dataExplorerActions.RESET_EXPLORER_SEARCH_VALUE({ id }),
+ SET_REQUEST_STATE: (payload: { requestState: DataTableRequestState }) => dataExplorerActions.SET_REQUEST_STATE({ ...payload, id }),
});
abstract requestItems(
api: MiddlewareAPI<Dispatch, RootState>,
- criteriaChanged?: boolean
+ criteriaChanged?: boolean,
+ background?: boolean
): Promise<void>;
}
? OrderDirection.ASC
: OrderDirection.DESC;
+ // Use createdAt as a secondary sort column so we break ties consistently.
return order
.addOrder(sortDirection, sortColumn.sort.field)
+ .addOrder(OrderDirection.DESC, "createdAt")
.getOrder();
} else {
return order.getOrder();
export const dataExplorerMiddleware =
(service: DataExplorerMiddlewareService): Middleware =>
- (api) =>
- (next) => {
- const actions = bindDataExplorerActions(service.getId());
+ (api) =>
+ (next) => {
+ const actions = bindDataExplorerActions(service.getId());
- return (action) => {
- const handleAction =
- <T extends { id: string }>(handler: (data: T) => void) =>
- (data: T) => {
- next(action);
- if (data.id === service.getId()) {
- handler(data);
- }
- };
- dataExplorerActions.match(action, {
- SET_PAGE: handleAction(() => {
- api.dispatch(actions.REQUEST_ITEMS(false));
- }),
- SET_ROWS_PER_PAGE: handleAction(() => {
- api.dispatch(actions.REQUEST_ITEMS(true));
- }),
- SET_FILTERS: handleAction(() => {
- api.dispatch(actions.RESET_PAGINATION());
- api.dispatch(actions.REQUEST_ITEMS(true));
- }),
- TOGGLE_SORT: handleAction(() => {
- api.dispatch(actions.REQUEST_ITEMS(true));
- }),
- SET_EXPLORER_SEARCH_VALUE: handleAction(() => {
- api.dispatch(actions.RESET_PAGINATION());
- api.dispatch(actions.REQUEST_ITEMS(true));
- }),
- REQUEST_ITEMS: handleAction(({ criteriaChanged }) => {
- api.dispatch<any>(async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- while (true) {
- let de = getDataExplorer(
- getState().dataExplorer,
- service.getId()
- );
- switch (de.requestState) {
- case DataTableRequestState.IDLE:
- // Start a new request.
- try {
- dispatch(
- actions.SET_REQUEST_STATE({
- requestState: DataTableRequestState.PENDING,
- })
- );
- await service.requestItems(api, criteriaChanged);
- } catch {
- dispatch(
- actions.SET_REQUEST_STATE({
- requestState: DataTableRequestState.NEED_REFRESH,
- })
- );
- }
- // Now check if the state is still PENDING, if it moved to NEED_REFRESH
- // then we need to reissue requestItems
- de = getDataExplorer(
+ return (action) => {
+ const handleAction =
+ <T extends { id: string }>(handler: (data: T) => void) =>
+ (data: T) => {
+ next(action);
+ if (data.id === service.getId()) {
+ handler(data);
+ }
+ };
+ dataExplorerActions.match(action, {
+ SET_PAGE: handleAction(() => {
+ api.dispatch(actions.REQUEST_ITEMS(false));
+ }),
+ SET_ROWS_PER_PAGE: handleAction(() => {
+ api.dispatch(actions.REQUEST_ITEMS(true));
+ }),
+ SET_FILTERS: handleAction(() => {
+ api.dispatch(actions.RESET_PAGINATION());
+ api.dispatch(actions.REQUEST_ITEMS(true));
+ }),
+ TOGGLE_SORT: handleAction(() => {
+ api.dispatch(actions.REQUEST_ITEMS(true));
+ }),
+ SET_EXPLORER_SEARCH_VALUE: handleAction(() => {
+ api.dispatch(actions.RESET_PAGINATION());
+ api.dispatch(actions.REQUEST_ITEMS(true));
+ }),
+ REQUEST_ITEMS: handleAction(({ criteriaChanged, background }) => {
+ api.dispatch<any>(async (
+ dispatch: Dispatch,
+ getState: () => RootState,
+ services: ServiceRepository
+ ) => {
+ while (true) {
+ let de = getDataExplorer(
getState().dataExplorer,
service.getId()
);
- const complete =
- de.requestState === DataTableRequestState.PENDING;
- dispatch(
- actions.SET_REQUEST_STATE({
- requestState: DataTableRequestState.IDLE,
- })
- );
- if (complete) {
- return;
+ switch (de.requestState) {
+ case DataTableRequestState.IDLE:
+ // Start a new request.
+ try {
+ dispatch(
+ actions.SET_REQUEST_STATE({
+ requestState: DataTableRequestState.PENDING,
+ })
+ );
+ await service.requestItems(api, criteriaChanged, background);
+ } catch {
+ dispatch(
+ actions.SET_REQUEST_STATE({
+ requestState: DataTableRequestState.NEED_REFRESH,
+ })
+ );
+ }
+ // Now check if the state is still PENDING, if it moved to NEED_REFRESH
+ // then we need to reissue requestItems
+ de = getDataExplorer(
+ getState().dataExplorer,
+ service.getId()
+ );
+ const complete =
+ de.requestState === DataTableRequestState.PENDING;
+ dispatch(
+ actions.SET_REQUEST_STATE({
+ requestState: DataTableRequestState.IDLE,
+ })
+ );
+ if (complete) {
+ return;
+ }
+ break;
+ case DataTableRequestState.PENDING:
+ // State is PENDING, move it to NEED_REFRESH so that when the current request finishes it starts a new one.
+ dispatch(
+ actions.SET_REQUEST_STATE({
+ requestState: DataTableRequestState.NEED_REFRESH,
+ })
+ );
+ return;
+ case DataTableRequestState.NEED_REFRESH:
+ // Nothing to do right now.
+ return;
}
- break;
- case DataTableRequestState.PENDING:
- // State is PENDING, move it to NEED_REFRESH so that when the current request finishes it starts a new one.
- dispatch(
- actions.SET_REQUEST_STATE({
- requestState: DataTableRequestState.NEED_REFRESH,
- })
- );
- return;
- case DataTableRequestState.NEED_REFRESH:
- // Nothing to do right now.
- return;
- }
- }
+ }
+ });
+ }),
+ default: () => next(action),
});
- }),
- default: () => next(action),
- });
- };
- };
+ };
+ };
//
// SPDX-License-Identifier: AGPL-3.0
-import { DialogAction, dialogActions } from "./dialog-actions";
+import { DialogAction, dialogActions } from './dialog-actions';
export type DialogState = Record<string, Dialog<any>>;
}
export const dialogReducer = (state: DialogState = {}, action: DialogAction) =>
-
dialogActions.match(action, {
OPEN_DIALOG: ({ id, data }) => ({ ...state, [id]: { open: true, data } }),
CLOSE_DIALOG: ({ id }) => ({
...state,
- [id]: state[id] ? { ...state[id], open: false } : { open: false, data: {} }
+ [id]: state[id] ? { ...state[id], open: false } : { open: false, data: {} },
}),
- CLOSE_ALL_DIALOGS: () => ({ }),
+ CLOSE_ALL_DIALOGS: () => ({}),
default: () => state,
});
-export const getDialog = <T>(state: DialogState, id: string) =>
- state[id] ? state[id] as Dialog<T> : undefined;
+export const getDialog = <T>(state: DialogState, id: string) => (state[id] ? (state[id] as Dialog<T>) : undefined);
};
export type WithDialogProps<T> = WithDialogStateProps<T> & WithDialogDispatchProps;
-export const withDialog = (id: string) =>
+export const withDialog =
+ (id: string) =>
// TODO: How to make compiler happy with & P instead of & any?
// eslint-disable-next-line
<T, P>(component: React.ComponentType<WithDialogProps<T> & any>) =>
const emptyData = {};
-export const mapStateToProps = (id: string) => <T>(state: { dialog: DialogState }): WithDialogStateProps<T> => {
- const dialog = state.dialog[id];
- return dialog ? dialog : { open: false, data: emptyData };
-};
+export const mapStateToProps =
+ (id: string) =>
+ <T>(state: { dialog: DialogState }): WithDialogStateProps<T> => {
+ const dialog = state.dialog[id];
+ return dialog ? dialog : { open: false, data: emptyData };
+ };
-export const mapDispatchToProps = (id: string) => (dispatch: Dispatch): WithDialogDispatchProps => ({
- closeDialog: () => {
- dispatch(dialogActions.CLOSE_DIALOG({ id }));
- }
-});
+export const mapDispatchToProps =
+ (id: string) =>
+ (dispatch: Dispatch): WithDialogDispatchProps => ({
+ closeDialog: () => {
+ dispatch(dialogActions.CLOSE_DIALOG({ id }));
+ },
+ });
import { getCurrentGroupDetailsPanelUuid, GroupMembersPanelActions } from 'store/group-details-panel/group-details-panel-actions';
import { LinkClass } from 'models/link';
import { ResourceKind } from 'models/resource';
+import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
export class GroupDetailsPanelMembersMiddlewareService extends DataExplorerMiddlewareService {
return;
} else {
try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const groupResource = await this.services.groupsService.get(groupUuid);
api.dispatch(updateResources([groupResource]));
api.dispatch(updateResources(projectsIn.items));
} catch (e) {
api.dispatch(couldNotFetchGroupDetailsContents());
+ } finally {
+ api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
}
}
}
import { ListResults } from 'services/common-service/common-service';
import { LinkResource } from 'models/link';
import { linkPanelActions } from 'store/link-panel/link-panel-actions';
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
export class LinkMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
const state = api.getState();
const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const response = await this.services.linkService.list(getParams(dataExplorer));
api.dispatch(updateResources(response.items));
api.dispatch(setItems(response));
} catch {
api.dispatch(couldNotFetchLinks());
+ } finally {
+ api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
}
}
}
name: string;
uuid: string;
ownerUuid: string;
-}
\ No newline at end of file
+ fromContextMenu?: boolean;
+}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { TCheckedList } from "components/data-table/data-table";
+
+export const multiselectActionContants = {
+ TOGGLE_VISIBLITY: "TOGGLE_VISIBLITY",
+ SET_CHECKEDLIST: "SET_CHECKEDLIST",
+ DESELECT_ONE: "DESELECT_ONE",
+};
+
+export const toggleMSToolbar = (isVisible: boolean) => {
+ return dispatch => {
+ dispatch({ type: multiselectActionContants.TOGGLE_VISIBLITY, payload: isVisible });
+ };
+};
+
+export const setCheckedListOnStore = (checkedList: TCheckedList) => {
+ return dispatch => {
+ dispatch({ type: multiselectActionContants.SET_CHECKEDLIST, payload: checkedList });
+ };
+};
+
+export const deselectOne = (uuid: string) => {
+ return dispatch => {
+ dispatch({ type: multiselectActionContants.DESELECT_ONE, payload: uuid });
+ };
+};
+
+export const multiselectActions = {
+ toggleMSToolbar,
+ setCheckedListOnStore,
+ deselectOne,
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { multiselectActionContants } from "./multiselect-actions";
+import { TCheckedList } from "components/data-table/data-table";
+
+type MultiselectToolbarState = {
+ isVisible: boolean;
+ checkedList: TCheckedList;
+};
+
+const multiselectToolbarInitialState = {
+ isVisible: false,
+ checkedList: {},
+};
+
+const { TOGGLE_VISIBLITY, SET_CHECKEDLIST, DESELECT_ONE } = multiselectActionContants;
+
+export const multiselectReducer = (state: MultiselectToolbarState = multiselectToolbarInitialState, action) => {
+ switch (action.type) {
+ case TOGGLE_VISIBLITY:
+ return { ...state, isVisible: action.payload };
+ case SET_CHECKEDLIST:
+ return { ...state, checkedList: action.payload };
+ case DESELECT_ONE:
+ return { ...state, checkedList: { ...state.checkedList, [action.payload]: false } };
+ default:
+ return state;
+ }
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch, compose, AnyAction } from 'redux';
+import { Dispatch, compose, AnyAction } from "redux";
import { push } from "react-router-redux";
-import { ResourceKind, extractUuidKind } from 'models/resource';
-import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
-import { Routes, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
-import { RootState } from 'store/store';
-import { ServiceRepository } from 'services/services';
-import { pluginConfig } from 'plugins';
-import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
+import { ResourceKind, extractUuidKind } from "models/resource";
+import { SidePanelTreeCategory } from "../side-panel-tree/side-panel-tree-actions";
+import { Routes, getGroupUrl, getNavUrl, getUserProfileUrl } from "routes/routes";
+import { RootState } from "store/store";
+import { ServiceRepository } from "services/services";
+import { pluginConfig } from "plugins";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from "store/breadcrumbs/breadcrumbs-actions";
export const navigationNotAvailable = (id: string) =>
snackbarActions.OPEN_SNACKBAR({
message: `${id} not available`,
hideDuration: 3000,
- kind: SnackbarKind.ERROR
+ kind: SnackbarKind.ERROR,
});
-export const navigateTo = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState) => {
-
- for (const navToFn of pluginConfig.navigateToHandlers) {
- if (navToFn(dispatch, getState, uuid)) {
- return;
- }
- }
-
- const kind = extractUuidKind(uuid);
- switch (kind) {
- case ResourceKind.PROJECT:
- case ResourceKind.USER:
- case ResourceKind.COLLECTION:
- case ResourceKind.CONTAINER_REQUEST:
- dispatch<any>(pushOrGoto(getNavUrl(uuid, getState().auth)));
- return;
- case ResourceKind.VIRTUAL_MACHINE:
- dispatch<any>(navigateToAdminVirtualMachines);
- return;
- case ResourceKind.WORKFLOW:
- dispatch<any>(pushOrGoto(getNavUrl(uuid, getState().auth)));
- // dispatch<any>(openDetailsPanel(uuid));
- return;
+export const navigateTo = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState) => {
+ for (const navToFn of pluginConfig.navigateToHandlers) {
+ if (navToFn(dispatch, getState, uuid)) {
+ return;
}
+ }
- switch (uuid) {
- case SidePanelTreeCategory.PROJECTS:
- const usr = getState().auth.user;
- if (usr) {
- dispatch<any>(pushOrGoto(getNavUrl(usr.uuid, getState().auth)));
- }
- return;
- case SidePanelTreeCategory.FAVORITES:
- dispatch<any>(navigateToFavorites);
- return;
- case SidePanelTreeCategory.PUBLIC_FAVORITES:
- dispatch(navigateToPublicFavorites);
- return;
- case SidePanelTreeCategory.SHARED_WITH_ME:
- dispatch(navigateToSharedWithMe);
- return;
- case SidePanelTreeCategory.TRASH:
- dispatch(navigateToTrash);
- return;
- case SidePanelTreeCategory.GROUPS:
- dispatch(navigateToGroups);
- return;
- case SidePanelTreeCategory.ALL_PROCESSES:
- dispatch(navigateToAllProcesses);
- return;
- case USERS_PANEL_LABEL:
- dispatch(navigateToUsers);
- return;
- case MY_ACCOUNT_PANEL_LABEL:
- dispatch(navigateToMyAccount);
- return;
- }
+ const kind = extractUuidKind(uuid);
+ switch (kind) {
+ case ResourceKind.PROJECT:
+ case ResourceKind.USER:
+ case ResourceKind.COLLECTION:
+ case ResourceKind.CONTAINER_REQUEST:
+ dispatch<any>(pushOrGoto(getNavUrl(uuid, getState().auth)));
+ return;
+ case ResourceKind.VIRTUAL_MACHINE:
+ dispatch<any>(navigateToAdminVirtualMachines);
+ return;
+ case ResourceKind.WORKFLOW:
+ dispatch<any>(pushOrGoto(getNavUrl(uuid, getState().auth)));
+ // dispatch<any>(openDetailsPanel(uuid));
+ return;
+ }
- dispatch(navigationNotAvailable(uuid));
- };
+ switch (uuid) {
+ case SidePanelTreeCategory.PROJECTS:
+ const usr = getState().auth.user;
+ if (usr) {
+ dispatch<any>(pushOrGoto(getNavUrl(usr.uuid, getState().auth)));
+ }
+ return;
+ case SidePanelTreeCategory.FAVORITES:
+ dispatch<any>(navigateToFavorites);
+ return;
+ case SidePanelTreeCategory.PUBLIC_FAVORITES:
+ dispatch(navigateToPublicFavorites);
+ return;
+ case SidePanelTreeCategory.SHARED_WITH_ME:
+ dispatch(navigateToSharedWithMe);
+ return;
+ case SidePanelTreeCategory.TRASH:
+ dispatch(navigateToTrash);
+ return;
+ case SidePanelTreeCategory.GROUPS:
+ dispatch(navigateToGroups);
+ return;
+ case SidePanelTreeCategory.ALL_PROCESSES:
+ dispatch(navigateToAllProcesses);
+ return;
+ case USERS_PANEL_LABEL:
+ dispatch(navigateToUsers);
+ return;
+ case MY_ACCOUNT_PANEL_LABEL:
+ dispatch(navigateToMyAccount);
+ return;
+ }
+ dispatch(navigationNotAvailable(uuid));
+};
export const navigateToNotFound = push(Routes.NO_MATCH);
export const pushOrGoto = (url: string): AnyAction => {
if (url === "") {
return { type: "noop" };
- } else if (url[0] === '/') {
+ } else if (url[0] === "/") {
return push(url);
} else {
window.location.href = url;
}
};
-
export const navigateToRootProject = (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
navigateTo(SidePanelTreeCategory.PROJECTS)(dispatch, getState);
};
export const navigateToSearchResults = (searchValue: string) => {
if (searchValue !== "") {
- return push({ pathname: Routes.SEARCH_RESULTS, search: '?q=' + encodeURIComponent(searchValue) });
+ return push({ pathname: Routes.SEARCH_RESULTS, search: "?q=" + encodeURIComponent(searchValue) });
} else {
return push({ pathname: Routes.SEARCH_RESULTS });
}
//
// SPDX-License-Identifier: AGPL-3.0
-import copy from 'copy-to-clipboard';
-import { Dispatch } from 'redux';
-import { getNavUrl } from 'routes/routes';
-import { RootState } from 'store/store';
+import copy from "copy-to-clipboard";
+import { Dispatch } from "redux";
+import { getNavUrl } from "routes/routes";
+import { RootState } from "store/store";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
export const openInNewTabAction = (resource: any) => (dispatch: Dispatch, getState: () => RootState) => {
const url = getNavUrl(resource.uuid, getState().auth);
- if (url[0] === '/') {
- window.open(`${window.location.origin}${url}`, '_blank');
+ if (url[0] === "/") {
+ window.open(`${window.location.origin}${url}`, "_blank");
} else if (url.length) {
- window.open(url, '_blank');
+ window.open(url, "_blank");
}
};
-export const copyToClipboardAction = (resource: any) => (dispatch: Dispatch, getState: () => RootState) => {
+export const copyToClipboardAction = (resources: Array<any>) => (dispatch: Dispatch, getState: () => RootState) => {
// Copy to clipboard omits token to avoid accidental sharing
- const url = getNavUrl(resource.uuid, getState().auth, false);
- if (url[0] === '/') {
- copy(`${window.location.origin}${url}`);
- } else if (url.length) {
- copy(url);
+ let url = getNavUrl(resources[0].uuid, getState().auth, false);
+ let wasCopied;
+
+ if (url[0] === "/") wasCopied = copy(`${window.location.origin}${url}`);
+ else if (url.length) {
+ wasCopied = copy(url);
}
+
+ if (wasCopied)
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Copied",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
};
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "common/unionize";
-import { getInputs, getOutputParameters, getRawInputs, getRawOutputs, loadProcess } from 'store/processes/processes-actions';
-import { Dispatch } from 'redux';
-import { ProcessStatus } from 'store/processes/process';
-import { RootState } from 'store/store';
+import { getInputs, getOutputParameters, getRawInputs, getRawOutputs, loadProcess } from "store/processes/processes-actions";
+import { Dispatch } from "redux";
+import { ProcessStatus } from "store/processes/process";
+import { RootState } from "store/store";
import { ServiceRepository } from "services/services";
-import { navigateTo, navigateToWorkflows } from 'store/navigation/navigation-action';
-import { snackbarActions } from 'store/snackbar/snackbar-actions';
-import { SnackbarKind } from '../snackbar/snackbar-actions';
-import { showWorkflowDetails } from 'store/workflow-panel/workflow-panel-actions';
+import { navigateTo, navigateToWorkflows } from "store/navigation/navigation-action";
+import { snackbarActions } from "store/snackbar/snackbar-actions";
+import { SnackbarKind } from "../snackbar/snackbar-actions";
+import { showWorkflowDetails } from "store/workflow-panel/workflow-panel-actions";
import { loadSubprocessPanel, subprocessPanelActions } from "../subprocess-panel/subprocess-panel-actions";
import { initProcessLogsPanel, processLogsPanelActions } from "store/process-logs-panel/process-logs-panel-actions";
import { CollectionFile } from "models/collection-file";
import { ContainerRequestResource } from "models/container-request";
-import { CommandOutputParameter } from 'cwlts/mappings/v1.0/CommandOutputParameter';
-import { CommandInputParameter, getIOParamId, WorkflowInputsData } from 'models/workflow';
+import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
+import { CommandInputParameter, getIOParamId, WorkflowInputsData } from "models/workflow";
import { getIOParamDisplayValue, ProcessIOParameter } from "views/process-panel/process-io-card";
import { OutputDetails, NodeInstanceType, NodeInfo } from "./process-panel";
import { AuthState } from "store/auth/auth-reducer";
export const toggleProcessPanelFilter = processPanelActions.TOGGLE_PROCESS_PANEL_FILTER;
-export const loadProcessPanel = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState) => {
- // Reset subprocess data explorer if navigating to new process
- // Avoids resetting pagination when refreshing same process
- if (getState().processPanel.containerRequestUuid !== uuid) {
- dispatch(subprocessPanelActions.CLEAR());
- }
- dispatch(processPanelActions.RESET_PROCESS_PANEL());
- dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL());
- dispatch<ProcessPanelAction>(processPanelActions.SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID(uuid));
- await dispatch<any>(loadProcess(uuid));
- dispatch(initProcessPanelFilters);
- dispatch<any>(initProcessLogsPanel(uuid));
- dispatch<any>(loadSubprocessPanel());
- };
+export const loadProcessPanel = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState) => {
+ // Reset subprocess data explorer if navigating to new process
+ // Avoids resetting pagination when refreshing same process
+ if (getState().processPanel.containerRequestUuid !== uuid) {
+ dispatch(subprocessPanelActions.CLEAR());
+ }
+ dispatch(processPanelActions.RESET_PROCESS_PANEL());
+ dispatch(processLogsPanelActions.RESET_PROCESS_LOGS_PANEL());
+ dispatch<ProcessPanelAction>(processPanelActions.SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID(uuid));
+ await dispatch<any>(loadProcess(uuid));
+ dispatch(initProcessPanelFilters);
+ dispatch<any>(initProcessLogsPanel(uuid));
+ dispatch<any>(loadSubprocessPanel());
+};
-export const navigateToOutput = (uuid: string) =>
- async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- try {
- await services.collectionService.get(uuid);
- dispatch<any>(navigateTo(uuid));
- } catch {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'This collection does not exists!', hideDuration: 2000, kind: SnackbarKind.ERROR }));
- }
- };
+export const navigateToOutput = (uuid: string) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ await services.collectionService.get(uuid);
+ dispatch<any>(navigateTo(uuid));
+ } catch {
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "This collection does not exists!", hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ }
+};
-export const loadInputs = (containerRequest: ContainerRequestResource) =>
- async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+export const loadInputs =
+ (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
dispatch<ProcessPanelAction>(processPanelActions.SET_INPUT_RAW(getRawInputs(containerRequest)));
dispatch<ProcessPanelAction>(processPanelActions.SET_INPUT_PARAMS(formatInputData(getInputs(containerRequest), getState().auth)));
};
-export const loadOutputs = (containerRequest: ContainerRequestResource) =>
- async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+export const loadOutputs =
+ (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const noOutputs = { rawOutputs: {} };
+
if (!containerRequest.outputUuid) {
- dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW(noOutputs));
+ dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW({ uuid: containerRequest.uuid, outputRaw: noOutputs }));
return;
- };
+ }
try {
const propsOutputs = getRawOutputs(containerRequest);
const filesPromise = services.collectionService.files(containerRequest.outputUuid);
// If has propsOutput, skip fetching cwl.output.json
if (propsOutputs !== undefined) {
- dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW({
- rawOutputs: propsOutputs,
- pdh: collection.portableDataHash
- }));
+ dispatch<ProcessPanelAction>(
+ processPanelActions.SET_OUTPUT_RAW({
+ rawOutputs: propsOutputs,
+ pdh: collection.portableDataHash,
+ })
+ );
} else {
// Fetch outputs from keep
- const outputFile = files.find((file) => file.name === 'cwl.output.json') as CollectionFile | undefined;
+ const outputFile = files.find(file => file.name === "cwl.output.json") as CollectionFile | undefined;
let outputData = outputFile ? await services.collectionService.getFileContents(outputFile) : undefined;
if (outputData && (outputData = JSON.parse(outputData)) && collection.portableDataHash) {
- dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW({
- rawOutputs: outputData,
- pdh: collection.portableDataHash,
- }));
+ dispatch<ProcessPanelAction>(
+ processPanelActions.SET_OUTPUT_RAW({
+ uuid: containerRequest.uuid,
+ outputRaw: { rawOutputs: outputData, pdh: collection.portableDataHash },
+ })
+ );
} else {
- dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW(noOutputs));
+ dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW({ uuid: containerRequest.uuid, outputRaw: noOutputs }));
}
}
} catch {
- dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW(noOutputs));
+ dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW({ uuid: containerRequest.uuid, outputRaw: noOutputs }));
}
};
-
-export const loadNodeJson = (containerRequest: ContainerRequestResource) =>
- async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+export const loadNodeJson =
+ (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
const noLog = { nodeInfo: null };
if (!containerRequest.logUuid) {
dispatch<ProcessPanelAction>(processPanelActions.SET_NODE_INFO(noLog));
return;
- };
+ }
try {
const filesPromise = services.collectionService.files(containerRequest.logUuid);
const collectionPromise = services.collectionService.get(containerRequest.logUuid);
const [files] = await Promise.all([filesPromise, collectionPromise]);
// Fetch node.json from keep
- const nodeFile = files.find((file) => file.name === 'node.json') as CollectionFile | undefined;
+ const nodeFile = files.find(file => file.name === "node.json") as CollectionFile | undefined;
let nodeData = nodeFile ? await services.collectionService.getFileContents(nodeFile) : undefined;
if (nodeData && (nodeData = JSON.parse(nodeData))) {
- dispatch<ProcessPanelAction>(processPanelActions.SET_NODE_INFO({
- nodeInfo: nodeData as NodeInstanceType
- }));
+ dispatch<ProcessPanelAction>(
+ processPanelActions.SET_NODE_INFO({
+ nodeInfo: nodeData as NodeInstanceType,
+ })
+ );
} else {
dispatch<ProcessPanelAction>(processPanelActions.SET_NODE_INFO(noLog));
}
}
};
-export const loadOutputDefinitions = (containerRequest: ContainerRequestResource) =>
- async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+export const loadOutputDefinitions =
+ (containerRequest: ContainerRequestResource) => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
if (containerRequest && containerRequest.mounts) {
dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_DEFINITIONS(getOutputParameters(containerRequest)));
}
};
-export const updateOutputParams = () =>
- async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const outputDefinitions = getState().processPanel.outputDefinitions;
- const outputRaw = getState().processPanel.outputRaw;
+export const updateOutputParams = () => async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const outputDefinitions = getState().processPanel.outputDefinitions;
+ const outputRaw = getState().processPanel.outputRaw;
- if (outputRaw !== null && outputRaw.rawOutputs) {
- dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_PARAMS(formatOutputData(outputDefinitions, outputRaw.rawOutputs, outputRaw.pdh, getState().auth)));
- }
- };
+ if (outputRaw && outputRaw.rawOutputs) {
+ dispatch<ProcessPanelAction>(
+ processPanelActions.SET_OUTPUT_PARAMS(formatOutputData(outputDefinitions, outputRaw.rawOutputs, outputRaw.pdh, getState().auth))
+ );
+ }
+};
-export const openWorkflow = (uuid: string) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch<any>(navigateToWorkflows);
- dispatch<any>(showWorkflowDetails(uuid));
- };
+export const openWorkflow = (uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch<any>(navigateToWorkflows);
+ dispatch<any>(showWorkflowDetails(uuid));
+};
export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FILTERS([
ProcessStatus.QUEUED,
ProcessStatus.ONHOLD,
ProcessStatus.FAILING,
ProcessStatus.WARNING,
- ProcessStatus.CANCELLED
+ ProcessStatus.CANCELLED,
]);
export const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
return {
id: getIOParamId(input),
label: input.label || "",
- value: getIOParamDisplayValue(auth, input)
+ value: getIOParamDisplayValue(auth, input),
};
});
};
-export const formatOutputData = (definitions: CommandOutputParameter[], values: any, pdh: string | undefined, auth: AuthState): ProcessIOParameter[] => {
+export const formatOutputData = (
+ definitions: CommandOutputParameter[],
+ values: any,
+ pdh: string | undefined,
+ auth: AuthState
+): ProcessIOParameter[] => {
return definitions.map(output => {
return {
id: getIOParamId(output),
label: output.label || "",
- value: getIOParamDisplayValue(auth, Object.assign(output, { value: values[getIOParamId(output)] || [] }), pdh)
+ value: getIOParamDisplayValue(auth, Object.assign(output, { value: values[getIOParamId(output)] || [] }), pdh),
};
});
};
//
// SPDX-License-Identifier: AGPL-3.0
-import { ProcessPanel } from 'store/process-panel/process-panel';
-import { ProcessPanelAction, processPanelActions } from 'store/process-panel/process-panel-actions';
+import { ProcessPanel } from "store/process-panel/process-panel";
+import { ProcessPanelAction, processPanelActions } from "store/process-panel/process-panel-actions";
const initialState: ProcessPanel = {
containerRequestUuid: "",
processPanelActions.match(action, {
RESET_PROCESS_PANEL: () => initialState,
SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID: containerRequestUuid => ({
- ...state, containerRequestUuid
+ ...state,
+ containerRequestUuid,
}),
SET_PROCESS_PANEL_FILTERS: statuses => {
const filters = statuses.reduce((filters, status) => ({ ...filters, [status]: true }), {});
return state;
}
},
- SET_OUTPUT_RAW: outputRaw => {
- return { ...state, outputRaw };
+ SET_OUTPUT_RAW: (data: any) => {
+ //never set output to {} unless initializing
+ if (state.outputRaw?.rawOutputs && Object.keys(state.outputRaw?.rawOutputs).length && state.containerRequestUuid === data.uuid) {
+ return state;
+ }
+ return { ...state, outputRaw: data.outputRaw };
},
SET_NODE_INFO: ({ nodeInfo }) => {
return { ...state, nodeInfo };
SET_OUTPUT_DEFINITIONS: outputDefinitions => {
// Set output definitions is only additive to avoid clearing when mounts go temporarily missing
if (outputDefinitions.length) {
- return { ...state, outputDefinitions }
+ return { ...state, outputDefinitions };
} else {
return state;
}
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch } from "redux";
-import { dialogActions } from "store/dialog/dialog-actions";
+import { Dispatch } from 'redux';
+import { dialogActions } from 'store/dialog/dialog-actions';
import { initialize, startSubmit } from 'redux-form';
import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
import { RootState } from 'store/store';
import { ServiceRepository } from 'services/services';
import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
import { getProcess } from 'store/processes/process';
-import {snackbarActions, SnackbarKind} from 'store/snackbar/snackbar-actions';
+import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
-import { ContainerRequestState } from "models/container-request";
+import { ContainerRequestState } from 'models/container-request';
export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
+export const MULTI_PROCESS_COPY_FORM_NAME = 'multiProcessCopyFormName';
-export const openCopyProcessDialog = (resource: { name: string, uuid: string }) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const openCopyProcessDialog =
+ (resource: { name: string; uuid: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getProcess(resource.uuid)(getState().resources);
if (process) {
dispatch<any>(resetPickerProjectTree());
}
};
-export const copyProcess = (resource: CopyFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
- try {
- const process = await services.containerRequestService.get(resource.uuid);
- const {
- command,
- containerCountMax,
- containerImage,
- cwd,
- description,
- environment,
- kind,
- mounts,
- outputName,
- outputPath,
- outputProperties,
- outputStorageClasses,
- outputTtl,
- properties,
- runtimeConstraints,
- schedulingParameters,
- useExisting,
- } = process;
- const newProcess = await services.containerRequestService.create({
- command,
- containerCountMax,
- containerImage,
- cwd,
- description,
- environment,
- kind,
- mounts,
- name: resource.name,
- outputName,
- outputPath,
- outputProperties,
- outputStorageClasses,
- outputTtl,
- ownerUuid: resource.ownerUuid,
- priority: 500,
- properties,
- runtimeConstraints,
- schedulingParameters,
- state: ContainerRequestState.UNCOMMITTED,
- useExisting,
- });
- dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
- return newProcess;
- } catch (e) {
- dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
- throw new Error('Could not copy the process.');
- }
- };
+export const copyProcess = (resource: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
+ try {
+ const process = await services.containerRequestService.get(resource.uuid);
+ const {
+ command,
+ containerCountMax,
+ containerImage,
+ cwd,
+ description,
+ environment,
+ kind,
+ mounts,
+ outputName,
+ outputPath,
+ outputProperties,
+ outputStorageClasses,
+ outputTtl,
+ properties,
+ runtimeConstraints,
+ schedulingParameters,
+ useExisting,
+ } = process;
+ const newProcess = await services.containerRequestService.create({
+ command,
+ containerCountMax,
+ containerImage,
+ cwd,
+ description,
+ environment,
+ kind,
+ mounts,
+ name: resource.name,
+ outputName,
+ outputPath,
+ outputProperties,
+ outputStorageClasses,
+ outputTtl,
+ ownerUuid: resource.ownerUuid,
+ priority: 500,
+ properties,
+ runtimeConstraints,
+ schedulingParameters,
+ state: ContainerRequestState.UNCOMMITTED,
+ useExisting,
+ });
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+ return newProcess;
+ } catch (e) {
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
+ throw new Error('Could not copy the process.');
+ }
+};
import { Dispatch } from "redux";
import { dialogActions } from "store/dialog/dialog-actions";
-import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
-import { ServiceRepository } from 'services/services';
-import { RootState } from 'store/store';
+import { startSubmit, stopSubmit, initialize, FormErrors } from "redux-form";
+import { ServiceRepository } from "services/services";
+import { RootState } from "store/store";
import { getCommonResourceServiceError, CommonResourceServiceError } from "services/common-service/common-resource-service";
-import {snackbarActions, SnackbarKind} from 'store/snackbar/snackbar-actions';
-import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
-import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
-import { projectPanelActions } from 'store/project-panel/project-panel-action';
-import { getProcess } from 'store/processes/process';
-import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
+import { resetPickerProjectTree } from "store/project-tree-picker/project-tree-picker-actions";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import { getProcess } from "store/processes/process";
+import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
-export const PROCESS_MOVE_FORM_NAME = 'processMoveFormName';
+export const PROCESS_MOVE_FORM_NAME = "processMoveFormName";
-export const openMoveProcessDialog = (resource: { name: string, uuid: string }) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const openMoveProcessDialog =
+ (resource: { name: string; uuid: string }) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getProcess(resource.uuid)(getState().resources);
if (process) {
dispatch<any>(resetPickerProjectTree());
dispatch(initialize(PROCESS_MOVE_FORM_NAME, resource));
dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_MOVE_FORM_NAME, data: {} }));
} else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Process not found", hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
};
-export const moveProcess = (resource: MoveToFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch(startSubmit(PROCESS_MOVE_FORM_NAME));
- try {
- const process = await services.containerRequestService.get(resource.uuid);
- await services.containerRequestService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
- dispatch(projectPanelActions.REQUEST_ITEMS());
+export const moveProcess = (resource: MoveToFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(startSubmit(PROCESS_MOVE_FORM_NAME));
+ try {
+ const process = await services.containerRequestService.get(resource.uuid);
+ await services.containerRequestService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
+ return process;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
+ dispatch(
+ stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: "A process with the same name already exists in the target project." } as FormErrors)
+ );
+ } else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
- return process;
- } catch (e) {
- const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
- dispatch(stopSubmit(PROCESS_MOVE_FORM_NAME, { ownerUuid: 'A process with the same name already exists in the target project.' } as FormErrors));
- } else {
- dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_MOVE_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not move the process.', hideDuration: 2000, kind: SnackbarKind.ERROR }));
- }
- return;
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not move the process.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
- };
+ return;
+ }
+};
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import { FormErrors, initialize, startSubmit, stopSubmit } from 'redux-form';
+import { FormErrors, initialize, startSubmit, stopSubmit } from "redux-form";
import { RootState } from "store/store";
import { dialogActions } from "store/dialog/dialog-actions";
import { getCommonResourceServiceError, CommonResourceServiceError } from "services/common-service/common-resource-service";
import { ServiceRepository } from "services/services";
-import { getProcess } from 'store/processes/process';
-import { projectPanelActions } from 'store/project-panel/project-panel-action';
-import {snackbarActions, SnackbarKind} from 'store/snackbar/snackbar-actions';
+import { getProcess } from "store/processes/process";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
export interface ProcessUpdateFormDialogData {
uuid: string;
description?: string;
}
-export const PROCESS_UPDATE_FORM_NAME = 'processUpdateFormName';
+export const PROCESS_UPDATE_FORM_NAME = "processUpdateFormName";
-export const openProcessUpdateDialog = (resource: ProcessUpdateFormDialogData) =>
- (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const openProcessUpdateDialog =
+ (resource: ProcessUpdateFormDialogData) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const process = getProcess(resource.uuid)(getState().resources);
- if(process) {
+ if (process) {
dispatch(initialize(PROCESS_UPDATE_FORM_NAME, { ...resource, name: process.containerRequest.name }));
dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_UPDATE_FORM_NAME, data: {} }));
} else {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process not found', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Process not found", hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
};
-export const updateProcess = (resource: ProcessUpdateFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const updateProcess =
+ (resource: ProcessUpdateFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch(startSubmit(PROCESS_UPDATE_FORM_NAME));
try {
- const updatedProcess = await services.containerRequestService.update(resource.uuid, { name: resource.name, description: resource.description });
+ const updatedProcess = await services.containerRequestService.update(resource.uuid, {
+ name: resource.name,
+ description: resource.description,
+ });
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
return updatedProcess;
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
- dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: 'Process with the same name already exists.' } as FormErrors));
+ dispatch(stopSubmit(PROCESS_UPDATE_FORM_NAME, { name: "Process with the same name already exists." } as FormErrors));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_UPDATE_FORM_NAME }));
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Could not update the process.', hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not update the process.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
return;
}
case containerRequest.containerUuid && !container:
return ProcessStatus.UNKNOWN;
+ case containerRequest.state === ContainerRequestState.UNCOMMITTED:
+ return ProcessStatus.DRAFT;
+
+ case containerRequest.state === ContainerRequestState.FINAL &&
+ container?.state === ContainerState.RUNNING:
+ // It is about to be completed but we haven't
+ // gotten the updated container record yet,
+ // if we don't catch this and show it as "Running"
+ // it will flicker "Cancelled" briefly
+ return ProcessStatus.RUNNING;
+
case containerRequest.state === ContainerRequestState.FINAL &&
container?.state !== ContainerState.COMPLETE:
// Request was finalized before its container started (or the
// container was cancelled)
return ProcessStatus.CANCELLED;
- case containerRequest.state === ContainerRequestState.UNCOMMITTED:
- return ProcessStatus.DRAFT;
-
case container && container.state === ContainerState.COMPLETE:
if (container?.exitCode === 0) {
if (containerRequest && container.finishedAt) {
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import { RootState } from 'store/store';
-import { ServiceRepository } from 'services/services';
-import { updateResources } from 'store/resources/resources-actions';
-import { Process } from './process';
-import { dialogActions } from 'store/dialog/dialog-actions';
-import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import { projectPanelActions } from 'store/project-panel/project-panel-action';
-import { navigateToRunProcess } from 'store/navigation/navigation-action';
-import { goToStep, runProcessPanelActions } from 'store/run-process-panel/run-process-panel-actions';
-import { getResource } from 'store/resources/resources';
+import { RootState } from "store/store";
+import { ServiceRepository } from "services/services";
+import { updateResources } from "store/resources/resources-actions";
+import { Process } from "./process";
+import { dialogActions } from "store/dialog/dialog-actions";
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import { navigateToRunProcess } from "store/navigation/navigation-action";
+import { goToStep, runProcessPanelActions } from "store/run-process-panel/run-process-panel-actions";
+import { getResource } from "store/resources/resources";
import { initialize } from "redux-form";
import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from "views/run-process-panel/run-process-basic-form";
import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from "views/run-process-panel/run-process-advanced-form";
-import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from 'models/process';
+import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from "models/process";
import { CommandInputParameter, getWorkflow, getWorkflowInputs, getWorkflowOutputs, WorkflowInputsData } from "models/workflow";
import { ProjectResource } from "models/project";
import { UserResource } from "models/user";
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 {
try {
const collection = await services.collectionService.get(containerRequest.outputUuid, false);
dispatch<any>(updateResources([collection]));
- } catch { }
+ } catch {}
}
if (containerRequest.containerUuid) {
try {
container = await services.containerService.get(containerRequest.containerUuid, false);
dispatch<any>(updateResources([container]));
- } catch { }
+ } catch {}
try {
if (container && container.runtimeUserUuid) {
const runtimeUser = await services.userService.get(container.runtimeUserUuid, false);
dispatch<any>(updateResources([runtimeUser]));
}
- } catch { }
+ } catch {}
return { containerRequest, container };
}
return { containerRequest };
};
-export const loadContainers = (containerUuids: string[], loadMounts: boolean = true) =>
+export const loadContainers =
+ (containerUuids: string[], loadMounts: boolean = true) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
let args: any = {
- filters: new FilterBuilder().addIn('uuid', containerUuids).getFilters(),
+ filters: new FilterBuilder().addIn("uuid", containerUuids).getFilters(),
limit: containerUuids.length,
};
if (!loadMounts) {
"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);
if (workflow && process) {
const mainWf = getWorkflow(process.mounts[MOUNT_PATH_CWL_WORKFLOW]);
- if (mainWf) { mainWf.inputs = getInputs(process); }
+ if (mainWf) {
+ mainWf.inputs = getInputs(process);
+ }
const stringifiedDefinition = JSON.stringify(process.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
const newWorkflow = { ...workflow, definition: stringifiedDefinition };
ram: process.runtimeConstraints.ram,
vcpus: process.runtimeConstraints.vcpus,
keep_cache_ram: process.runtimeConstraints.keep_cache_ram,
- acr_container_image: process.containerImage
+ acr_container_image: process.containerImage,
};
dispatch<any>(initialize(RUN_PROCESS_ADVANCED_FORM, advancedInitialData));
* Returns {} if inputs not found in mounts or props
*/
export const getRawInputs = (data: any): WorkflowInputsData | undefined => {
- if (!data) { return undefined; }
+ if (!data) {
+ return undefined;
+ }
const mountInput = data.mounts?.[MOUNT_PATH_CWL_INPUT]?.content;
const propsInput = data.properties?.cwl_input;
- if (!mountInput && !propsInput) { return {}; }
- return (mountInput || propsInput);
-}
+ if (!mountInput && !propsInput) {
+ return {};
+ }
+ return mountInput || propsInput;
+};
export const getInputs = (data: any): CommandInputParameter[] => {
// Definitions from mounts are needed so we return early if missing
- if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) { return []; }
+ if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) {
+ return [];
+ }
const content = getRawInputs(data) as any;
// Only escape if content is falsy to allow displaying definitions if no inputs are present
// (Don't check raw content length)
- if (!content) { return []; }
+ if (!content) {
+ return [];
+ }
const inputs = getWorkflowInputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
- return inputs ? inputs.map(
- (it: any) => (
- {
- type: it.type,
- id: it.id,
- label: it.label,
- default: content[it.id],
- value: content[it.id.split('/').pop()] || [],
- doc: it.doc
- }
- )
- ) : [];
+ return inputs
+ ? inputs.map((it: any) => ({
+ type: it.type,
+ id: it.id,
+ label: it.label,
+ default: content[it.id],
+ value: content[it.id.split("/").pop()] || [],
+ doc: it.doc,
+ }))
+ : [];
};
/*
* Assumes containerRequest is loaded
*/
export const getRawOutputs = (data: any): CommandInputParameter[] | undefined => {
- if (!data || !data.properties || !data.properties.cwl_output) { return undefined; }
- return (data.properties.cwl_output);
-}
+ if (!data || !data.properties || !data.properties.cwl_output) {
+ return undefined;
+ }
+ return data.properties.cwl_output;
+};
export type InputCollectionMount = {
path: string;
pdh: string;
-}
+};
export const getInputCollectionMounts = (data: any): InputCollectionMount[] => {
- if (!data || !data.mounts) { return []; }
+ if (!data || !data.mounts) {
+ return [];
+ }
return Object.keys(data.mounts)
.map(key => ({
...data.mounts[key],
path: key,
}))
- .filter(mount => mount.kind === 'collection' &&
- mount.portable_data_hash &&
- mount.path)
+ .filter(mount => mount.kind === "collection" && mount.portable_data_hash && mount.path)
.map(mount => ({
path: mount.path,
pdh: mount.portable_data_hash,
};
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.fromContextMenu ? [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) {
try {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Removing ...', kind: SnackbarKind.INFO }));
- await services.containerRequestService.delete(uuid, false);
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Removing ...", kind: SnackbarKind.INFO }));
+ await services.containerRequestService.delete(process.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: `Deletion failed`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
}
}
- };
+ }
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
+
+const PROJECT_PANEL_ID = "projectPanel";
+
+export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
//
// SPDX-License-Identifier: AGPL-3.0
-import { Dispatch } from 'redux';
-import { bindDataExplorerActions } from "store/data-explorer/data-explorer-action";
+import { Dispatch } from "redux";
import { propertiesActions } from "store/properties/properties-actions";
-import { RootState } from 'store/store';
+import { RootState } from "store/store";
import { getProperty } from "store/properties/properties";
import { loadProject } from "store/workbench/workbench-actions";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
export const PROJECT_PANEL_ID = "projectPanel";
export const PROJECT_PANEL_CURRENT_UUID = "projectPanelCurrentUuid";
-export const IS_PROJECT_PANEL_TRASHED = 'isProjectPanelTrashed';
-export const projectPanelActions = bindDataExplorerActions(PROJECT_PANEL_ID);
+export const IS_PROJECT_PANEL_TRASHED = "isProjectPanelTrashed";
-export const openProjectPanel = (projectUuid: string) =>
- async (dispatch: Dispatch) => {
- await dispatch<any>(loadProject(projectUuid));
- dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
- dispatch(projectPanelActions.RESET_EXPLORER_SEARCH_VALUE());
- dispatch(projectPanelActions.REQUEST_ITEMS());
- };
+export const openProjectPanel = (projectUuid: string) => async (dispatch: Dispatch) => {
+ await dispatch<any>(loadProject(projectUuid));
+ dispatch(propertiesActions.SET_PROPERTY({ key: PROJECT_PANEL_CURRENT_UUID, value: projectUuid }));
+ dispatch(projectPanelActions.RESET_EXPLORER_SEARCH_VALUE());
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+};
export const getProjectPanelCurrentUuid = (state: RootState) => getProperty<string>(PROJECT_PANEL_CURRENT_UUID)(state.properties);
-export const setIsProjectPanelTrashed = (isTrashed: boolean) =>
- propertiesActions.SET_PROPERTY({ key: IS_PROJECT_PANEL_TRASHED, value: isTrashed });
+export const setIsProjectPanelTrashed = (isTrashed: boolean) => propertiesActions.SET_PROPERTY({ key: IS_PROJECT_PANEL_TRASHED, value: isTrashed });
DataExplorerMiddlewareService,
dataExplorerToListParams,
getDataExplorerColumnFilters,
- listResultsToDataExplorerItemsMeta
-} from 'store/data-explorer/data-explorer-middleware-service';
+ listResultsToDataExplorerItemsMeta,
+} from "store/data-explorer/data-explorer-middleware-service";
import { ProjectPanelColumnNames } from "views/project-panel/project-panel";
import { RootState } from "store/store";
import { DataColumns } from "components/data-table/data-table";
import { FilterBuilder, joinFilters } from "services/api/filter-builder";
import { GroupContentsResource, GroupContentsResourcePrefix } from "services/groups-service/groups-service";
import { updateFavorites } from "store/favorites/favorites-actions";
-import {
- IS_PROJECT_PANEL_TRASHED,
- projectPanelActions,
- getProjectPanelCurrentUuid
-} from 'store/project-panel/project-panel-action';
+import { IS_PROJECT_PANEL_TRASHED, getProjectPanelCurrentUuid } from "store/project-panel/project-panel-action";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
import { Dispatch, MiddlewareAPI } from "redux";
import { ProjectResource } from "models/project";
import { updateResources } from "store/resources/resources-actions";
import { getProperty } from "store/properties/properties";
-import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
-import { DataExplorer, getDataExplorer } from 'store/data-explorer/data-explorer-reducer';
-import { ListResults } from 'services/common-service/common-service';
-import { loadContainers } from 'store/processes/processes-actions';
-import { ResourceKind } from 'models/resource';
+import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { DataExplorer, getDataExplorer } from "store/data-explorer/data-explorer-reducer";
+import { ListResults } from "services/common-service/common-service";
+import { loadContainers } from "store/processes/processes-actions";
+import { ResourceKind } from "models/resource";
import { getSortColumn } from "store/data-explorer/data-explorer-reducer";
-import {
- serializeResourceTypeFilters,
- buildProcessStatusFilters
-} from 'store/resource-type-filters/resource-type-filters';
-import { updatePublicFavorites } from 'store/public-favorites/public-favorites-actions';
-import { selectedFieldsOfGroup } from 'models/group';
-import { defaultCollectionSelectedFields } from 'models/collection';
-import { containerRequestFieldsNoMounts } from 'models/container-request';
+import { serializeResourceTypeFilters, buildProcessStatusFilters } from "store/resource-type-filters/resource-type-filters";
+import { updatePublicFavorites } from "store/public-favorites/public-favorites-actions";
+import { selectedFieldsOfGroup } from "models/group";
+import { defaultCollectionSelectedFields } from "models/collection";
+import { containerRequestFieldsNoMounts } from "models/container-request";
export class ProjectPanelMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
super(id);
}
- async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean, background?: boolean) {
const state = api.getState();
const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
const projectUuid = getProjectPanelCurrentUuid(state);
api.dispatch(projectPanelDataExplorerIsNotSet());
} else {
try {
- api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); }
const response = await this.services.groupsService.contents(projectUuid, getParams(dataExplorer, !!isProjectTrashed));
const resourceUuids = response.items.map(item => item.uuid);
api.dispatch<any>(updateFavorites(resourceUuids));
await api.dispatch<any>(loadMissingProcessesInformation(response.items));
api.dispatch(setItems(response));
} catch (e) {
- api.dispatch(projectPanelActions.SET_ITEMS({
- items: [],
- itemsAvailable: 0,
- page: 0,
- rowsPerPage: dataExplorer.rowsPerPage
- }));
+ api.dispatch(
+ projectPanelActions.SET_ITEMS({
+ items: [],
+ itemsAvailable: 0,
+ page: 0,
+ rowsPerPage: dataExplorer.rowsPerPage,
+ })
+ );
api.dispatch(couldNotFetchProjectContents());
} finally {
- api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
}
}
}
}
-export const loadMissingProcessesInformation = (resources: GroupContentsResource[]) =>
- async (dispatch: Dispatch) => {
- const containerUuids = resources.reduce((uuids, resource) => {
- return resource.kind === ResourceKind.CONTAINER_REQUEST &&
- resource.containerUuid &&
- !uuids.includes(resource.containerUuid)
- ? [...uuids, resource.containerUuid]
- : uuids;
- }, [] as string[]);
- if (containerUuids.length > 0) {
- await dispatch<any>(loadContainers(
- containerUuids,
- false
- ));
- }
- };
+export const loadMissingProcessesInformation = (resources: GroupContentsResource[]) => async (dispatch: Dispatch) => {
+ const containerUuids = resources.reduce((uuids, resource) => {
+ return resource.kind === ResourceKind.CONTAINER_REQUEST && resource.containerUuid && !uuids.includes(resource.containerUuid)
+ ? [...uuids, resource.containerUuid]
+ : uuids;
+ }, [] as string[]);
+ if (containerUuids.length > 0) {
+ await dispatch<any>(loadContainers(containerUuids, false));
+ }
+};
export const setItems = (listResults: ListResults<GroupContentsResource>) =>
projectPanelActions.SET_ITEMS({
order: getOrder(dataExplorer),
filters: getFilters(dataExplorer),
includeTrash: isProjectTrashed,
- select: selectedFieldsOfGroup.concat(defaultCollectionSelectedFields, containerRequestFieldsNoMounts)
+ select: selectedFieldsOfGroup.concat(defaultCollectionSelectedFields, containerRequestFieldsNoMounts),
});
export const getFilters = (dataExplorer: DataExplorer) => {
const columns = dataExplorer.columns as DataColumns<string, ProjectResource>;
const typeFilters = serializeResourceTypeFilters(getDataExplorerColumnFilters(columns, ProjectPanelColumnNames.TYPE));
- const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
- const activeStatusFilter = Object.keys(statusColumnFilters).find(
- filterName => statusColumnFilters[filterName].selected
- );
+ const statusColumnFilters = getDataExplorerColumnFilters(columns, "Status");
+ const activeStatusFilter = Object.keys(statusColumnFilters).find(filterName => statusColumnFilters[filterName].selected);
// TODO: Extract group contents name filter
const nameFilters = new FilterBuilder()
.getFilters();
// Filter by container status
- const statusFilters = buildProcessStatusFilters(
- new FilterBuilder(),
- activeStatusFilter || '',
- GroupContentsResourcePrefix.PROCESS).getFilters();
+ const statusFilters = buildProcessStatusFilters(new FilterBuilder(), activeStatusFilter || "", GroupContentsResourcePrefix.PROCESS).getFilters();
- return joinFilters(
- statusFilters,
- typeFilters,
- nameFilters,
- );
+ return joinFilters(statusFilters, typeFilters, nameFilters);
};
const getOrder = (dataExplorer: DataExplorer) => {
const sortColumn = getSortColumn<ProjectResource>(dataExplorer);
const order = new OrderBuilder<ProjectResource>();
if (sortColumn && sortColumn.sort) {
- const sortDirection = sortColumn.sort.direction === SortDirection.ASC
- ? OrderDirection.ASC
- : OrderDirection.DESC;
+ const sortDirection = sortColumn.sort.direction === SortDirection.ASC ? OrderDirection.ASC : OrderDirection.DESC;
+ // Use createdAt as a secondary sort column so we break ties consistently.
return order
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.COLLECTION)
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.PROCESS)
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.PROJECT)
+ .addOrder(OrderDirection.DESC, "createdAt", GroupContentsResourcePrefix.PROCESS)
.getOrder();
} else {
return order.getOrder();
const projectPanelCurrentUuidIsNotSet = () =>
snackbarActions.OPEN_SNACKBAR({
- message: 'Project panel is not opened.',
- kind: SnackbarKind.ERROR
+ message: "Project panel is not opened.",
+ kind: SnackbarKind.ERROR,
});
const couldNotFetchProjectContents = () =>
snackbarActions.OPEN_SNACKBAR({
- message: 'Could not fetch project contents.',
- kind: SnackbarKind.ERROR
+ message: "Could not fetch project contents.",
+ kind: SnackbarKind.ERROR,
});
const projectPanelDataExplorerIsNotSet = () =>
snackbarActions.OPEN_SNACKBAR({
- message: 'Project panel is not ready.',
- kind: SnackbarKind.ERROR
+ message: "Project panel is not ready.",
+ kind: SnackbarKind.ERROR,
});
import { Dispatch } from "redux";
import { ServiceRepository } from "services/services";
-import { projectPanelActions } from "store/project-panel/project-panel-action";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
import { loadResource } from "store/resources/resources-actions";
import { RootState } from "store/store";
-export const freezeProject = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUUID = getState().auth.user!.uuid;
+export const freezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const userUUID = getState().auth.user!.uuid;
- const updatedProject = await services.projectService.update(uuid, {
- frozenByUuid: userUUID
- });
+ const updatedProject = await services.projectService.update(uuid, {
+ frozenByUuid: userUUID,
+ });
- dispatch(projectPanelActions.REQUEST_ITEMS());
- dispatch<any>(loadResource(uuid, false));
- return updatedProject;
- };
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch<any>(loadResource(uuid, false));
+ return updatedProject;
+};
-export const unfreezeProject = (uuid: string) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+export const unfreezeProject = (uuid: string) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const updatedProject = await services.projectService.update(uuid, {
+ frozenByUuid: null,
+ });
- const updatedProject = await services.projectService.update(uuid, {
- frozenByUuid: null
- });
-
- dispatch(projectPanelActions.REQUEST_ITEMS());
- dispatch<any>(loadResource(uuid, false));
- return updatedProject;
- };
\ No newline at end of file
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+ dispatch<any>(loadResource(uuid, false));
+ return updatedProject;
+};
import { Dispatch } from "redux";
import { dialogActions } from "store/dialog/dialog-actions";
-import { startSubmit, stopSubmit, initialize, FormErrors } from 'redux-form';
-import { ServiceRepository } from 'services/services';
-import { RootState } from 'store/store';
+import { startSubmit, stopSubmit, initialize, FormErrors } from "redux-form";
+import { ServiceRepository } from "services/services";
+import { RootState } from "store/store";
import { getUserUuid } from "common/getuser";
import { getCommonResourceServiceError, CommonResourceServiceError } from "services/common-service/common-resource-service";
-import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
-import { resetPickerProjectTree } from 'store/project-tree-picker/project-tree-picker-actions';
-import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
-import { projectPanelActions } from 'store/project-panel/project-panel-action';
-import { loadSidePanelTreeProjects } from '../side-panel-tree/side-panel-tree-actions';
+import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
+import { resetPickerProjectTree } from "store/project-tree-picker/project-tree-picker-actions";
+import { initProjectsTreePicker } from "store/tree-picker/tree-picker-actions";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import { loadSidePanelTreeProjects } from "../side-panel-tree/side-panel-tree-actions";
-export const PROJECT_MOVE_FORM_NAME = 'projectMoveFormName';
+export const PROJECT_MOVE_FORM_NAME = "projectMoveFormName";
-export const openMoveProjectDialog = (resource: { name: string, uuid: string }) =>
- (dispatch: Dispatch) => {
+export const openMoveProjectDialog = (resource: any) => {
+ return (dispatch: Dispatch) => {
dispatch<any>(resetPickerProjectTree());
dispatch<any>(initProjectsTreePicker(PROJECT_MOVE_FORM_NAME));
dispatch(initialize(PROJECT_MOVE_FORM_NAME, resource));
dispatch(dialogActions.OPEN_DIALOG({ id: PROJECT_MOVE_FORM_NAME, data: {} }));
};
+};
-export const moveProject = (resource: MoveToFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const userUuid = getUserUuid(getState());
- if (!userUuid) { return; }
- dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
- try {
- const newProject = await services.projectService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
- dispatch(projectPanelActions.REQUEST_ITEMS());
+export const moveProject = (resource: MoveToFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getUserUuid(getState());
+ if (!userUuid) {
+ return;
+ }
+ dispatch(startSubmit(PROJECT_MOVE_FORM_NAME));
+ try {
+ const newProject = await services.projectService.update(resource.uuid, { ownerUuid: resource.ownerUuid });
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+
+ dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
+ await dispatch<any>(loadSidePanelTreeProjects(userUuid));
+ return newProject;
+ } catch (e) {
+ const error = getCommonResourceServiceError(e);
+ if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
+ dispatch(
+ stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: "A project with the same name already exists in the target project." } as FormErrors)
+ );
+ } else if (error === CommonResourceServiceError.OWNERSHIP_CYCLE) {
+ dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: "Cannot move a project into itself." } as FormErrors));
+ } else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
- await dispatch<any>(loadSidePanelTreeProjects(userUuid));
- return newProject;
- } catch (e) {
- const error = getCommonResourceServiceError(e);
- if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
- dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'A project with the same name already exists in the target project.' } as FormErrors));
- } else if (error === CommonResourceServiceError.OWNERSHIP_CYCLE) {
- dispatch(stopSubmit(PROJECT_MOVE_FORM_NAME, { ownerUuid: 'Cannot move a project into itself.' } as FormErrors));
- } else {
- dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_MOVE_FORM_NAME }));
- throw new Error('Could not move the project.');
- }
- return;
+ throw new Error("Could not move the project.");
}
- };
+ return;
+ }
+};
// SPDX-License-Identifier: AGPL-3.0
import { Dispatch } from "redux";
-import {
- FormErrors,
- formValueSelector,
- initialize,
- reset,
- startSubmit,
- stopSubmit
-} from 'redux-form';
+import { FormErrors, formValueSelector, initialize, reset, startSubmit, stopSubmit } from "redux-form";
import { RootState } from "store/store";
import { dialogActions } from "store/dialog/dialog-actions";
-import {
- getCommonResourceServiceError,
- CommonResourceServiceError
-} from "services/common-service/common-resource-service";
+import { getCommonResourceServiceError, CommonResourceServiceError } from "services/common-service/common-resource-service";
import { ServiceRepository } from "services/services";
-import { projectPanelActions } from 'store/project-panel/project-panel-action';
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
import { GroupClass } from "models/group";
import { Participant } from "views-components/sharing-dialog/participant-select";
import { ProjectProperties } from "./project-create-actions";
properties?: ProjectProperties;
}
-export const PROJECT_UPDATE_FORM_NAME = 'projectUpdateFormName';
-export const PROJECT_UPDATE_PROPERTIES_FORM_NAME = 'projectUpdatePropertiesFormName';
+export const PROJECT_UPDATE_FORM_NAME = "projectUpdateFormName";
+export const PROJECT_UPDATE_PROPERTIES_FORM_NAME = "projectUpdatePropertiesFormName";
export const PROJECT_UPDATE_FORM_SELECTOR = formValueSelector(PROJECT_UPDATE_FORM_NAME);
-export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
- (dispatch: Dispatch, getState: () => RootState) => {
- // Get complete project resource from store to handle consumers passing in partial resources
- const project = getResource<ProjectResource>(resource.uuid)(getState().resources);
- dispatch(initialize(PROJECT_UPDATE_FORM_NAME, project));
- dispatch(dialogActions.OPEN_DIALOG({
+export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) => (dispatch: Dispatch, getState: () => RootState) => {
+ // Get complete project resource from store to handle consumers passing in partial resources
+ const project = getResource<ProjectResource>(resource.uuid)(getState().resources);
+ dispatch(initialize(PROJECT_UPDATE_FORM_NAME, project));
+ dispatch(
+ dialogActions.OPEN_DIALOG({
id: PROJECT_UPDATE_FORM_NAME,
data: {
sourcePanel: GroupClass.PROJECT,
- }
- }));
- };
+ },
+ })
+ );
+};
-export const updateProject = (project: ProjectUpdateFormDialogData) =>
- async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- const uuid = project.uuid || '';
+export const updateProject =
+ (project: ProjectUpdateFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const uuid = project.uuid || "";
dispatch(startSubmit(PROJECT_UPDATE_FORM_NAME));
try {
const updatedProject = await services.projectService.update(
description: project.description,
properties: project.properties,
},
- false);
+ false
+ );
dispatch(projectPanelActions.REQUEST_ITEMS());
dispatch(reset(PROJECT_UPDATE_FORM_NAME));
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
} catch (e) {
const error = getCommonResourceServiceError(e);
if (error === CommonResourceServiceError.UNIQUE_NAME_VIOLATION) {
- dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: 'Project with the same name already exists.' } as FormErrors));
+ dispatch(stopSubmit(PROJECT_UPDATE_FORM_NAME, { name: "Project with the same name already exists." } as FormErrors));
} else {
dispatch(dialogActions.CLOSE_DIALOG({ id: PROJECT_UPDATE_FORM_NAME }));
- const errMsg = e.errors
- ? e.errors.join('')
- : 'There was an error while updating the project';
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: errMsg,
- hideDuration: 2000,
- kind: SnackbarKind.ERROR }));
+ const errMsg = e.errors ? e.errors.join("") : "There was an error while updating the project";
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: errMsg,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
}
return;
}
});
it("should serialize all but collection filters", () => {
- const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters());
+ const filters = deselectNode(ObjectTypeFilter.COLLECTION, true)(getInitialResourceTypeFilters());
const serializedFilters = serializeResourceTypeFilters(filters);
expect(serializedFilters)
.toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.WORKFLOW}","${ResourceKind.PROCESS}"]],["container_requests.requesting_container_uuid","=",null]`);
it("should serialize output collections and projects", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
- deselectNode(CollectionTypeFilter.LOG_COLLECTION),
- deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(CollectionTypeFilter.GENERAL_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.LOG_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize output collections and projects", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
- deselectNode(CollectionTypeFilter.LOG_COLLECTION),
- deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(CollectionTypeFilter.GENERAL_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.LOG_COLLECTION, true),
+ deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize general collections", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.PROJECT),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
+ deselectNode(ObjectTypeFilter.PROJECT, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION, true)
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize only main processes", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.PROJECT),
- deselectNode(ProcessTypeFilter.CHILD_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION),
- deselectNode(ObjectTypeFilter.DEFINITION),
+ deselectNode(ObjectTypeFilter.PROJECT, true),
+ deselectNode(ProcessTypeFilter.CHILD_PROCESS, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize only child processes", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.PROJECT),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(ObjectTypeFilter.PROJECT, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
- selectNode(ProcessTypeFilter.CHILD_PROCESS),
+ selectNode(ProcessTypeFilter.CHILD_PROCESS, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize all project types", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(ObjectTypeFilter.COLLECTION),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize filter groups", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(GroupTypeFilter.PROJECT),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(GroupTypeFilter.PROJECT, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
it("should serialize projects (normal)", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
- deselectNode(GroupTypeFilter.FILTER_GROUP),
- deselectNode(ObjectTypeFilter.DEFINITION),
- deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(GroupTypeFilter.FILTER_GROUP, true),
+ deselectNode(ObjectTypeFilter.DEFINITION, true),
+ deselectNode(ProcessTypeFilter.MAIN_PROCESS, true),
+ deselectNode(ObjectTypeFilter.COLLECTION, true),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
import { Resource, EditableResource } from "models/resource";
import { ResourceKind } from 'models/resource';
-import { ProjectResource } from "models/project";
import { GroupResource } from "models/group";
export type ResourcesState = { [key: string]: Resource };
const advancedFormValues = getWorkflowRunnerSettings(workflow);
let owner = getResource<ProjectResource | UserResource>(getState().runProcessPanel.processOwnerUuid)(getState().resources);
- const userUuid = getUserUuid(getState());
if (!owner || !owner.canWrite) {
owner = undefined;
}
}).catch(() => {
api.dispatch(couldNotFetchSearchResults(session.clusterId));
});
- }
+ }
);
}
}
? OrderDirection.ASC
: OrderDirection.DESC;
+ // Use createdAt as a secondary sort column so we break ties consistently.
return order
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.COLLECTION)
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.PROCESS)
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.PROJECT)
+ .addOrder(OrderDirection.DESC, "createdAt", GroupContentsResourcePrefix.PROCESS)
.getOrder();
} else {
return order.getOrder();
? OrderDirection.ASC
: OrderDirection.DESC;
+ // Use createdAt as a secondary sort column so we break ties consistently.
return order
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.COLLECTION)
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.PROCESS)
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.PROJECT)
+ .addOrder(OrderDirection.DESC, "createdAt", GroupContentsResourcePrefix.PROCESS)
.getOrder();
} else {
return order.getOrder();
//
// SPDX-License-Identifier: AGPL-3.0
-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";
+import { composeWithDevTools } from "redux-devtools-extension";
+
+declare global {
+ interface Window {
+ __REDUX_DEVTOOLS_EXTENSION_COMPOSE__?: typeof compose;
+ }
+}
export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
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 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 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 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();
publicFavoritesMiddleware,
collectionsContentAddress,
subprocessMiddleware,
- treePickerSearchMiddleware
+ treePickerSearchMiddleware,
];
- const reduceMiddlewaresFn: (a: Middleware[],
- b: MiddlewareListReducer) => Middleware[] = (a, b) => b(a, services);
+ const reduceMiddlewaresFn: (a: Middleware[], b: MiddlewareListReducer) => Middleware[] = (a, b) => b(a, services);
middlewares = pluginConfig.middlewares.reduce(reduceMiddlewaresFn, middlewares);
- const enhancer = composeWithDevTools({/* options */ })(applyMiddleware(redirectToMiddleware, ...middlewares));
+ const enhancer = composeWithDevTools({
+ /* options */
+ })(applyMiddleware(redirectToMiddleware, ...middlewares));
return createStore(rootReducer, enhancer);
}
-const createRootReducer = (services: ServiceRepository) => combineReducers({
- auth: authReducer(services),
- banner: bannerReducer,
- collectionPanel: collectionPanelReducer,
- collectionPanelFiles: collectionPanelFilesReducer,
- contextMenu: contextMenuReducer,
- dataExplorer: dataExplorerReducer,
- detailsPanel: detailsPanelReducer,
- dialog: dialogReducer,
- favorites: favoritesReducer,
- ownerName: ownerNameReducer,
- publicFavorites: publicFavoritesReducer,
- form: formReducer,
- processLogsPanel: processLogsPanelReducer,
- properties: propertiesReducer,
- resources: resourcesReducer,
- router: routerReducer,
- snackbar: snackbarReducer,
- treePicker: treePickerReducer,
- treePickerSearch: treePickerSearchReducer,
- fileUploader: fileUploaderReducer,
- processPanel: processPanelReducer,
- progressIndicator: progressIndicatorReducer,
- runProcessPanel: runProcessPanelReducer,
- appInfo: appInfoReducer,
- searchBar: searchBarReducer,
- virtualMachines: virtualMachinesReducer,
- repositories: repositoriesReducer,
- keepServices: keepServicesReducer,
- linkAccountPanel: linkAccountPanelReducer,
- sidePanel: sidePanelReducer
-});
+const createRootReducer = (services: ServiceRepository) =>
+ combineReducers({
+ auth: authReducer(services),
+ banner: bannerReducer,
+ collectionPanel: collectionPanelReducer,
+ collectionPanelFiles: collectionPanelFilesReducer,
+ contextMenu: contextMenuReducer,
+ dataExplorer: dataExplorerReducer,
+ detailsPanel: detailsPanelReducer,
+ dialog: dialogReducer,
+ favorites: favoritesReducer,
+ ownerName: ownerNameReducer,
+ publicFavorites: publicFavoritesReducer,
+ form: formReducer,
+ processLogsPanel: processLogsPanelReducer,
+ properties: propertiesReducer,
+ resources: resourcesReducer,
+ router: routerReducer,
+ snackbar: snackbarReducer,
+ treePicker: treePickerReducer,
+ treePickerSearch: treePickerSearchReducer,
+ fileUploader: fileUploaderReducer,
+ processPanel: processPanelReducer,
+ progressIndicator: progressIndicatorReducer,
+ runProcessPanel: runProcessPanelReducer,
+ appInfo: appInfoReducer,
+ searchBar: searchBarReducer,
+ virtualMachines: virtualMachinesReducer,
+ repositories: repositoriesReducer,
+ keepServices: keepServicesReducer,
+ linkAccountPanel: linkAccountPanelReducer,
+ sidePanel: sidePanelReducer,
+ multiselect: multiselectReducer,
+ });
super(id);
}
- async requestItems(api: MiddlewareAPI<Dispatch, RootState>) {
+ async requestItems(api: MiddlewareAPI<Dispatch, RootState>, criteriaChanged?: boolean, background?: boolean) {
const state = api.getState();
const parentContainerRequestUuid = state.processPanel.containerRequestUuid;
if (parentContainerRequestUuid === "") { return; }
const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
try {
- api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.START_WORKING(this.getId())); }
const parentContainerRequest = await this.services.containerRequestService.get(parentContainerRequestUuid);
if (parentContainerRequest.containerUuid) {
const containerRequests = await this.services.containerRequestService.list(
{
- ...getParams(dataExplorer, parentContainerRequest) ,
+ ...getParams(dataExplorer, parentContainerRequest),
select: containerRequestFieldsNoMounts
});
api.dispatch(updateResources(containerRequests.items));
// Populate the actual user view
api.dispatch(setItems(containerRequests));
}
- api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
} catch {
- api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
+ if (!background) { api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId())); }
api.dispatch(couldNotFetchSubprocesses());
}
}
export const getFilters = (
dataExplorer: DataExplorer,
parentContainerRequest: ContainerRequestResource) => {
- const columns = dataExplorer.columns as DataColumns<string, ProcessResource>;
- const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
- const activeStatusFilter = Object.keys(statusColumnFilters).find(
- filterName => statusColumnFilters[filterName].selected
- ) || ProcessStatusFilter.ALL;
+ const columns = dataExplorer.columns as DataColumns<string, ProcessResource>;
+ const statusColumnFilters = getDataExplorerColumnFilters(columns, 'Status');
+ const activeStatusFilter = Object.keys(statusColumnFilters).find(
+ filterName => statusColumnFilters[filterName].selected
+ ) || ProcessStatusFilter.ALL;
- // Get all the subprocess' container requests and containers.
- const fb = new FilterBuilder().addEqual('requesting_container_uuid', parentContainerRequest.containerUuid);
- const statusFilters = buildProcessStatusFilters(fb, activeStatusFilter).getFilters();
+ // Get all the subprocess' container requests and containers.
+ const fb = new FilterBuilder().addEqual('requesting_container_uuid', parentContainerRequest.containerUuid);
+ const statusFilters = buildProcessStatusFilters(fb, activeStatusFilter).getFilters();
- const nameFilters = dataExplorer.searchValue
- ? new FilterBuilder()
- .addILike("name", dataExplorer.searchValue)
- .getFilters()
- : '';
+ const nameFilters = dataExplorer.searchValue
+ ? new FilterBuilder()
+ .addILike("name", dataExplorer.searchValue)
+ .getFilters()
+ : '';
- return joinFilters(
- nameFilters,
- statusFilters
- );
- };
+ return joinFilters(
+ nameFilters,
+ statusFilters
+ );
+};
export const setItems = (listResults: ListResults<ProcessResource>) =>
subprocessPanelActions.SET_ITEMS({
? OrderDirection.ASC
: OrderDirection.DESC;
+ // Use createdAt as a secondary sort column so we break ties consistently.
return order
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.COLLECTION)
.addOrder(sortDirection, sortColumn.sort.field, GroupContentsResourcePrefix.PROJECT)
+ .addOrder(OrderDirection.DESC, "createdAt", GroupContentsResourcePrefix.PROCESS)
.getOrder();
} else {
return order.getOrder();
import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
import { trashPanelActions } from "store/trash-panel/trash-panel-action";
import { activateSidePanelTreeItem, loadSidePanelTreeProjects } from "store/side-panel-tree/side-panel-tree-actions";
-import { projectPanelActions } from "store/project-panel/project-panel-action";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
import { ResourceKind } from "models/resource";
-import { navigateTo, navigateToTrash } from 'store/navigation/navigation-action';
-import { matchCollectionRoute } from 'routes/routes';
+import { navigateTo, navigateToTrash } from "store/navigation/navigation-action";
+import { matchCollectionRoute } from "routes/routes";
-export const toggleProjectTrashed = (uuid: string, ownerUuid: string, isTrashed: boolean) =>
+export const toggleProjectTrashed =
+ (uuid: string, ownerUuid: string, isTrashed: boolean, isMulti: boolean) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
- let errorMessage = '';
- let successMessage = '';
+ let errorMessage = "";
+ let successMessage = "";
+ let untrashedResource;
try {
if (isTrashed) {
errorMessage = "Could not restore project from trash";
- successMessage = "Restored from trash";
- await services.groupsService.untrash(uuid);
- dispatch<any>(navigateTo(uuid));
+ successMessage = "Restored project from trash";
+ untrashedResource = await services.groupsService.untrash(uuid);
+ dispatch<any>(isMulti || !untrashedResource ? navigateToTrash : navigateTo(uuid));
dispatch<any>(activateSidePanelTreeItem(uuid));
} else {
errorMessage = "Could not move project to trash";
- successMessage = "Added to trash";
+ successMessage = "Added project to trash";
await services.groupsService.trash(uuid);
dispatch<any>(loadSidePanelTreeProjects(ownerUuid));
dispatch<any>(navigateTo(ownerUuid));
}
+ if (untrashedResource) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: successMessage,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ }
} catch (e) {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: errorMessage,
- kind: SnackbarKind.ERROR
- }));
+ if (e.status === 422) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Could not restore project from trash: Duplicate name at destination",
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ } else {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: errorMessage,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
}
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: successMessage,
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
};
-export const toggleCollectionTrashed = (uuid: string, isTrashed: boolean) =>
+export const toggleCollectionTrashed =
+ (uuid: string, isTrashed: boolean) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<any> => {
- let errorMessage = '';
- let successMessage = '';
+ let errorMessage = "";
+ let successMessage = "";
try {
if (isTrashed) {
const { location } = getState().router;
errorMessage = "Could not restore collection from trash";
successMessage = "Restored from trash";
await services.collectionService.untrash(uuid);
- if (matchCollectionRoute(location ? location.pathname : '')) {
+ if (matchCollectionRoute(location ? location.pathname : "")) {
dispatch(navigateToTrash);
}
dispatch(trashPanelActions.REQUEST_ITEMS());
await services.collectionService.trash(uuid);
dispatch(projectPanelActions.REQUEST_ITEMS());
}
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: successMessage,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
} catch (e) {
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: errorMessage,
- kind: SnackbarKind.ERROR
- }));
+ if (e.status === 422) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Could not restore collection from trash: Duplicate name at destination",
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ } else {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: errorMessage,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
}
- dispatch(snackbarActions.OPEN_SNACKBAR({
- message: successMessage,
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS
- }));
};
-export const toggleTrashed = (kind: ResourceKind, uuid: string, ownerUuid: string, isTrashed: boolean) =>
- (dispatch: Dispatch) => {
- if (kind === ResourceKind.PROJECT) {
- dispatch<any>(toggleProjectTrashed(uuid, ownerUuid, isTrashed!!));
- } else if (kind === ResourceKind.COLLECTION) {
- dispatch<any>(toggleCollectionTrashed(uuid, isTrashed!!));
- }
- };
+export const toggleTrashed = (kind: ResourceKind, uuid: string, ownerUuid: string, isTrashed: boolean) => (dispatch: Dispatch) => {
+ if (kind === ResourceKind.PROJECT) {
+ dispatch<any>(toggleProjectTrashed(uuid, ownerUuid, isTrashed!!, false));
+ } else if (kind === ResourceKind.COLLECTION) {
+ dispatch<any>(toggleCollectionTrashed(uuid, isTrashed!!));
+ }
+};
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
+import React from "react";
export interface PickerIdProp {
pickerId: string;
export const pickerId =
(id: string) =>
- <P extends PickerIdProp>(Component: React.ComponentType<P>) =>
- (props: P) =>
- <Component {...props} pickerId={id} />;
-
\ No newline at end of file
+ <P extends PickerIdProp>(Component: React.ComponentType<P>) =>
+ (props: P) => {
+ return (
+ <Component
+ {...props}
+ pickerId={id}
+ />
+ );
+ };
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ServiceRepository, createServices } from "services/services";
+import { configureStore, RootStore } from "../store";
+import { createBrowserHistory } from "history";
+import { mockConfig } from 'common/config';
+import { ApiActions } from "services/api/api-actions";
+import Axios from "axios";
+import MockAdapter from "axios-mock-adapter";
+import { ResourceKind } from 'models/resource';
+import { SHARED_PROJECT_ID, initProjectsTreePicker } from "./tree-picker-actions";
+import { CollectionResource } from "models/collection";
+import { GroupResource } from "models/group";
+import { CollectionDirectory, CollectionFile, CollectionFileType } from "models/collection-file";
+import { GroupContentsResource } from "services/groups-service/groups-service";
+import { ListResults } from "services/common-service/common-service";
+
+describe('tree-picker-actions', () => {
+ const axiosInst = Axios.create({ headers: {} });
+ const axiosMock = new MockAdapter(axiosInst);
+
+ let store: RootStore;
+ let services: ServiceRepository;
+ const config: any = {
+
+
+ };
+ const actions: ApiActions = {
+ progressFn: (id: string, working: boolean) => { },
+ errorFn: (id: string, message: string) => { }
+ };
+ let importMocks: any[];
+
+ beforeEach(() => {
+ axiosMock.reset();
+ services = createServices(mockConfig({}), actions, axiosInst);
+ store = configureStore(createBrowserHistory(), services, config);
+ localStorage.clear();
+ importMocks = [];
+ });
+
+ afterEach(() => {
+ importMocks.map(m => m.restore());
+ });
+
+ it('initializes preselected tree picker nodes', async () => {
+ const dispatchMock = jest.fn();
+ const dispatchWrapper = (action: any) => {
+ dispatchMock(action);
+ return store.dispatch(action);
+ };
+
+ const emptyCollectionUuid = "zzzzz-4zz18-000000000000000";
+ const collectionUuid = "zzzzz-4zz18-111111111111111";
+ const parentProjectUuid = "zzzzz-j7d0g-000000000000000";
+ const childCollectionUuid = "zzzzz-4zz18-222222222222222";
+
+ const fakeResources = {
+ [emptyCollectionUuid]: {
+ kind: ResourceKind.COLLECTION,
+ ownerUuid: '',
+ files: [],
+ },
+ [collectionUuid]: {
+ kind: ResourceKind.COLLECTION,
+ ownerUuid: '',
+ files: [{
+ id: `${collectionUuid}/directory`,
+ name: "directory",
+ path: "",
+ type: CollectionFileType.DIRECTORY,
+ url: `/c=${collectionUuid}/directory/`,
+ }]
+ },
+ [parentProjectUuid]: {
+ kind: ResourceKind.GROUP,
+ ownerUuid: '',
+ },
+ [childCollectionUuid]: {
+ kind: ResourceKind.COLLECTION,
+ ownerUuid: parentProjectUuid,
+ files: [
+ {
+ id: `${childCollectionUuid}/mainDir`,
+ name: "mainDir",
+ path: "",
+ type: CollectionFileType.DIRECTORY,
+ url: `/c=${childCollectionUuid}/mainDir/`,
+ },
+ {
+ id: `${childCollectionUuid}/mainDir/subDir`,
+ name: "subDir",
+ path: "/mainDir",
+ type: CollectionFileType.DIRECTORY,
+ url: `/c=${childCollectionUuid}/mainDir/subDir`,
+ }
+ ],
+ },
+ };
+
+ services.ancestorsService.ancestors = jest.fn(async (startUuid, endUuid) => {
+ let ancestors: (GroupResource | CollectionResource)[] = [];
+ let uuid = startUuid;
+ while (uuid?.length && fakeResources[uuid]) {
+ const resource = fakeResources[uuid];
+ if (resource.kind === ResourceKind.COLLECTION) {
+ ancestors.unshift({
+ uuid, kind: resource.kind,
+ ownerUuid: resource.ownerUuid,
+ } as CollectionResource);
+ } else if (resource.kind === ResourceKind.GROUP) {
+ ancestors.unshift({
+ uuid, kind: resource.kind,
+ ownerUuid: resource.ownerUuid,
+ } as GroupResource);
+ }
+ uuid = resource.ownerUuid;
+ }
+ return ancestors;
+ });
+
+ services.collectionService.files = jest.fn(async (uuid): Promise<(CollectionDirectory | CollectionFile)[]> => {
+ return fakeResources[uuid]?.files || [];
+ });
+
+ services.groupsService.contents = jest.fn(async (uuid, args) => {
+ const items = Object.keys(fakeResources).map(uuid => ({...fakeResources[uuid], uuid})).filter(item => item.ownerUuid === uuid);
+ return {items: items as GroupContentsResource[], itemsAvailable: items.length} as ListResults<GroupContentsResource>;
+ });
+
+ const pickerId = "pickerId";
+
+ // When collection preselected
+ await initProjectsTreePicker(pickerId, {
+ selectedItemUuids: [emptyCollectionUuid],
+ includeDirectories: true,
+ includeFiles: false,
+ multi: true,
+ })(dispatchWrapper, store.getState, services);
+
+ // Expect ancestor service to be called
+ expect(services.ancestorsService.ancestors).toHaveBeenCalledWith(emptyCollectionUuid, '');
+ // Expect top level to be expanded and node to be selected
+ expect(store.getState().treePicker["pickerId_shared"][SHARED_PROJECT_ID].expanded).toBe(true);
+ expect(store.getState().treePicker["pickerId_shared"][emptyCollectionUuid].selected).toBe(true);
+
+
+ // When collection subdirectory is preselected
+ await initProjectsTreePicker(pickerId, {
+ selectedItemUuids: [`${collectionUuid}/directory`],
+ includeDirectories: true,
+ includeFiles: false,
+ multi: true,
+ })(dispatchWrapper, store.getState, services);
+
+ // Expect ancestor service to be called
+ expect(services.ancestorsService.ancestors).toHaveBeenCalledWith(collectionUuid, '');
+ // Expect top level to be expanded and node to be selected
+ expect(store.getState().treePicker["pickerId_shared"][SHARED_PROJECT_ID].expanded).toBe(true);
+ expect(store.getState().treePicker["pickerId_shared"][collectionUuid].expanded).toBe(true);
+ expect(store.getState().treePicker["pickerId_shared"][collectionUuid].selected).toBe(false);
+ expect(store.getState().treePicker["pickerId_shared"][`${collectionUuid}/directory`].selected).toBe(true);
+
+
+ // When subdirectory of collection inside project is preselected
+ await initProjectsTreePicker(pickerId, {
+ selectedItemUuids: [`${childCollectionUuid}/mainDir/subDir`],
+ includeDirectories: true,
+ includeFiles: false,
+ multi: true,
+ })(dispatchWrapper, store.getState, services);
+
+ // Expect ancestor service to be called
+ expect(services.ancestorsService.ancestors).toHaveBeenCalledWith(childCollectionUuid, '');
+ // Expect parent project and collection to be expanded
+ expect(store.getState().treePicker["pickerId_shared"][SHARED_PROJECT_ID].expanded).toBe(true);
+ expect(store.getState().treePicker["pickerId_shared"][parentProjectUuid].expanded).toBe(true);
+ expect(store.getState().treePicker["pickerId_shared"][parentProjectUuid].selected).toBe(false);
+ expect(store.getState().treePicker["pickerId_shared"][childCollectionUuid].expanded).toBe(true);
+ expect(store.getState().treePicker["pickerId_shared"][childCollectionUuid].selected).toBe(false);
+ // Expect main directory to be expanded
+ expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir`].expanded).toBe(true);
+ expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir`].selected).toBe(false);
+ // Expect sub directory to be selected
+ expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir/subDir`].expanded).toBe(false);
+ expect(store.getState().treePicker["pickerId_shared"][`${childCollectionUuid}/mainDir/subDir`].selected).toBe(true);
+
+
+ });
+});
import { sortFilesTree } from "services/collection-service/collection-service-files-response";
import { GroupClass, GroupResource } from "models/group";
import { CollectionResource } from "models/collection";
+import { getResource } from "store/resources/resources";
+import { updateResources } from "store/resources/resources-actions";
+import { SnackbarKind, snackbarActions } from "store/snackbar/snackbar-actions";
export const treePickerActions = unionize({
LOAD_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
APPEND_TREE_PICKER_NODE_SUBTREE: ofType<{ id: string, subtree: Tree<any>, pickerId: string }>(),
TOGGLE_TREE_PICKER_NODE_COLLAPSE: ofType<{ id: string, pickerId: string }>(),
EXPAND_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string }>(),
+ EXPAND_TREE_PICKER_NODE_ANCESTORS: ofType<{ id: string, pickerId: string }>(),
ACTIVATE_TREE_PICKER_NODE: ofType<{ id: string, pickerId: string, relatedTreePickers?: string[] }>(),
DEACTIVATE_TREE_PICKER_NODE: ofType<{ pickerId: string }>(),
- TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string }>(),
- SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
- DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string }>(),
+ TOGGLE_TREE_PICKER_NODE_SELECTION: ofType<{ id: string, pickerId: string, cascade: boolean }>(),
+ SELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
+ DESELECT_TREE_PICKER_NODE: ofType<{ id: string | string[], pickerId: string, cascade: boolean }>(),
EXPAND_TREE_PICKER_NODES: ofType<{ ids: string[], pickerId: string }>(),
RESET_TREE_PICKER: ofType<{ pickerId: string }>()
});
export const getSelectedNodes = <Value>(pickerId: string) => (state: TreePicker) =>
getAllNodes<Value>(pickerId, node => node.selected)(state);
-export const initProjectsTreePicker = (pickerId: string, selectedItemUuid?: string) =>
+interface TreePickerPreloadParams {
+ selectedItemUuids: string[];
+ includeDirectories: boolean;
+ includeFiles: boolean;
+ multi: boolean;
+}
+
+export const initProjectsTreePicker = (pickerId: string, preloadParams?: TreePickerPreloadParams) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
dispatch<any>(initUserProject(home));
dispatch<any>(initPublicFavoritesProject(publicFavorites));
dispatch<any>(initSearchProject(search));
- if (selectedItemUuid) {
- dispatch<any>(loadInitialValue(selectedItemUuid, pickerId));
+ if (preloadParams && preloadParams.selectedItemUuids.length) {
+ await dispatch<any>(loadInitialValue(
+ preloadParams.selectedItemUuids,
+ pickerId,
+ preloadParams.includeDirectories,
+ preloadParams.includeFiles,
+ preloadParams.multi
+ ));
}
};
searchProjects?: boolean;
}
+/**
+ * loadProject is used to load or refresh a project node in a tree picker
+ * Errors are caught and a toast is shown if the project fails to load
+ */
export const loadProject = (params: LoadProjectParamsWithId) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const {
const itemLimit = 200;
- const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
-
- if (itemsAvailable > itemLimit) {
- items.push({
- uuid: "more-items-available",
- kind: ResourceKind.WORKFLOW,
- name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
- description: "",
- definition: "",
- ownerUuid: "",
- createdAt: "",
- modifiedByClientUuid: "",
- modifiedByUserUuid: "",
- modifiedAt: "",
- href: "",
- etag: ""
- });
- }
-
- dispatch<any>(receiveTreePickerData<GroupContentsResource>({
- id,
- pickerId,
- data: items.filter((item) => {
- if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
- return false;
- }
+ try {
+ const { items, itemsAvailable } = await services.groupsService.contents((loadShared || searchProjects) ? '' : id, { filters, excludeHomeProject: loadShared || undefined, limit: itemLimit });
+ dispatch<any>(updateResources(items));
+
+ if (itemsAvailable > itemLimit) {
+ items.push({
+ uuid: "more-items-available",
+ kind: ResourceKind.WORKFLOW,
+ name: `*** Not all items listed (${items.length} out of ${itemsAvailable}), reduce item count with search or filter ***`,
+ description: "",
+ definition: "",
+ ownerUuid: "",
+ createdAt: "",
+ modifiedByClientUuid: "",
+ modifiedByUserUuid: "",
+ modifiedAt: "",
+ href: "",
+ etag: ""
+ });
+ }
- if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
- return false;
- }
+ dispatch<any>(receiveTreePickerData<GroupContentsResource>({
+ id,
+ pickerId,
+ data: items.filter((item) => {
+ if (!includeFilterGroups && (item as GroupResource).groupClass && (item as GroupResource).groupClass === GroupClass.FILTER) {
+ return false;
+ }
- return true;
- }),
- extractNodeData: item => (
- item.uuid === "more-items-available" ?
- {
- id: item.uuid,
- value: item,
- status: TreeNodeStatus.LOADED
+ if (options && options.showOnlyWritable && item.hasOwnProperty('frozenByUuid') && (item as ProjectResource).frozenByUuid) {
+ return false;
}
- : {
- id: item.uuid,
- value: item,
- status: item.kind === ResourceKind.PROJECT
- ? TreeNodeStatus.INITIAL
- : includeDirectories || includeFiles
+
+ return true;
+ }),
+ extractNodeData: item => (
+ item.uuid === "more-items-available" ?
+ {
+ id: item.uuid,
+ value: item,
+ status: TreeNodeStatus.LOADED
+ }
+ : {
+ id: item.uuid,
+ value: item,
+ status: item.kind === ResourceKind.PROJECT
? TreeNodeStatus.INITIAL
- : TreeNodeStatus.LOADED
- }),
- }));
+ : includeDirectories || includeFiles
+ ? TreeNodeStatus.INITIAL
+ : TreeNodeStatus.LOADED
+ }),
+ }));
+ } catch(e) {
+ console.error("Failed to load project into tree picker:", e);;
+ dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to load project`, kind: SnackbarKind.ERROR }));
+ }
};
export const loadCollection = (id: string, pickerId: string, includeDirectories?: boolean, includeFiles?: boolean) =>
const sorted = sortFilesTree(tree);
const filesTree = mapTreeValues(services.collectionService.extendFileURL)(sorted);
- dispatch(
+ // await tree modifications so that consumers can guarantee node presence
+ await dispatch(
treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
id,
pickerId,
subtree: mapTree(node => ({ ...node, status: TreeNodeStatus.LOADED }))(filesTree)
}));
+ // Expand collection root node
dispatch(treePickerActions.TOGGLE_TREE_PICKER_NODE_COLLAPSE({ id, pickerId }));
}
}
}));
};
-export const loadInitialValue = (initialValue: string, pickerId: string) =>
+type PickerItemPreloadData = {
+ itemId: string;
+ mainItemUuid: string;
+ ancestors: (GroupResource | CollectionResource)[];
+ isHomeProjectItem: boolean;
+}
+
+type PickerTreePreloadData = {
+ tree: Tree<GroupResource | CollectionResource>;
+ pickerTreeId: string;
+ pickerTreeRootUuid: string;
+};
+
+export const loadInitialValue = (pickerItemIds: string[], pickerId: string, includeDirectories: boolean, includeFiles: boolean, multi: boolean,) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
- const { home, shared } = getProjectsTreePickerIds(pickerId);
const homeUuid = getUserUuid(getState());
- const ancestors = (await services.ancestorsService.ancestors(initialValue, ''))
+
+ // Request ancestor trees in paralell and save home project status
+ const pickerItemsData: PickerItemPreloadData[] = await Promise.allSettled(pickerItemIds.map(async itemId => {
+ const mainItemUuid = itemId.includes('/') ? itemId.split('/')[0] : itemId;
+
+ const ancestors = (await services.ancestorsService.ancestors(mainItemUuid, ''))
.filter(item =>
item.kind === ResourceKind.GROUP ||
item.kind === ResourceKind.COLLECTION
) as (GroupResource | CollectionResource)[];
- if (ancestors.length) {
- const isUserHomeProject = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
- const pickerTreeId = isUserHomeProject ? home : shared;
- const pickerTreeRootUuid: string = (homeUuid && isUserHomeProject) ? homeUuid : SHARED_PROJECT_ID;
+ if (ancestors.length === 0) {
+ return Promise.reject({item: itemId});
+ }
+
+ const isHomeProjectItem = !!(homeUuid && ancestors.some(item => item.ownerUuid === homeUuid));
- ancestors[0].ownerUuid = '';
- const tree = createInitialLocationTree(ancestors, initialValue);
+ return {
+ itemId,
+ mainItemUuid,
+ ancestors,
+ isHomeProjectItem,
+ };
+ })).then((res) => {
+ // Show toast if any selections failed to restore
+ const rejectedPromises = res.filter((promiseResult): promiseResult is PromiseRejectedResult => (promiseResult.status === 'rejected'));
+ if (rejectedPromises.length) {
+ rejectedPromises.forEach(item => {
+ console.error("The following item failed to load into the tree picker", item.reason);
+ });
+ dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Some selections failed to load and were removed. See console for details.`, kind: SnackbarKind.ERROR }));
+ }
+ // Filter out any failed promises and map to resulting preload data with ancestors
+ return res.filter((promiseResult): promiseResult is PromiseFulfilledResult<PickerItemPreloadData> => (
+ promiseResult.status === 'fulfilled'
+ )).map(res => res.value)
+ });
+
+ // Group items to preload / ancestor data by home/shared picker and create initial Trees to preload
+ const initialTreePreloadData: PickerTreePreloadData[] = [
+ pickerItemsData.filter((item) => item.isHomeProjectItem),
+ pickerItemsData.filter((item) => !item.isHomeProjectItem),
+ ]
+ .filter((items) => items.length > 0)
+ .map((itemGroup) =>
+ itemGroup.reduce(
+ (preloadTree, itemData) => ({
+ tree: createInitialPickerTree(
+ itemData.ancestors,
+ itemData.mainItemUuid,
+ preloadTree.tree
+ ),
+ pickerTreeId: getPickerItemTreeId(itemData, homeUuid, pickerId),
+ pickerTreeRootUuid: getPickerItemRootUuid(itemData, homeUuid),
+ }),
+ {
+ tree: createTree<GroupResource | CollectionResource>(),
+ pickerTreeId: '',
+ pickerTreeRootUuid: '',
+ } as PickerTreePreloadData
+ )
+ );
+
+ // Load initial trees into corresponding picker store
+ await Promise.all(initialTreePreloadData.map(preloadTree => (
dispatch(
treePickerActions.APPEND_TREE_PICKER_NODE_SUBTREE({
- id: pickerTreeRootUuid,
- pickerId: pickerTreeId,
- subtree: tree
- }));
- dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: initialValue, pickerId: pickerTreeId }));
- dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: pickerTreeId }));
- }
+ id: preloadTree.pickerTreeRootUuid,
+ pickerId: preloadTree.pickerTreeId,
+ subtree: preloadTree.tree,
+ })
+ )
+ )));
+
+ // Await loading collection before attempting to select items
+ await Promise.all(pickerItemsData.map(async itemData => {
+ const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
+
+ // Selected item resides in collection subpath
+ if (itemData.itemId.includes('/')) {
+ // Load collection into tree
+ // loadCollection includes more than dispatched actions and must be awaited
+ await dispatch(loadCollection(itemData.mainItemUuid, pickerTreeId, includeDirectories, includeFiles));
+ }
+ // Expand nodes down to destination
+ dispatch(treePickerActions.EXPAND_TREE_PICKER_NODE_ANCESTORS({ id: itemData.itemId, pickerId: pickerTreeId }));
+ }));
+ // Select or activate nodes
+ pickerItemsData.forEach(itemData => {
+ const pickerTreeId = getPickerItemTreeId(itemData, homeUuid, pickerId);
+
+ if (multi) {
+ dispatch(treePickerActions.SELECT_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId, cascade: false}));
+ } else {
+ dispatch(treePickerActions.ACTIVATE_TREE_PICKER_NODE({ id: itemData.itemId, pickerId: pickerTreeId }));
+ }
+ });
+
+ // Refresh triggers loading in all adjacent items that were not included in the ancestor tree
+ await initialTreePreloadData.map(preloadTree => dispatch(treePickerSearchActions.REFRESH_TREE_PICKER({ pickerId: preloadTree.pickerTreeId })));
}
+const getPickerItemTreeId = (itemData: PickerItemPreloadData, homeUuid: string | undefined, pickerId: string) => {
+ const { home, shared } = getProjectsTreePickerIds(pickerId);
+ return ((itemData.isHomeProjectItem && homeUuid) ? home : shared);
+};
+
+const getPickerItemRootUuid = (itemData: PickerItemPreloadData, homeUuid: string | undefined) => {
+ return (itemData.isHomeProjectItem && homeUuid) ? homeUuid : SHARED_PROJECT_ID;
+};
+
export const FAVORITES_PROJECT_ID = 'Favorites';
export const initFavoritesProject = (pickerId: string) =>
async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
* if the item represents a valid target/destination location
*/
export type FileOperationLocation = {
+ name: string;
uuid: string;
- path: string;
+ pdh?: string;
+ subpath: string;
}
-export const getFileOperationLocation = (item: ProjectsTreePickerItem): FileOperationLocation | undefined => {
- if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
- return {
- uuid: item.uuid,
- path: '/'
- };
- } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
- const uuid = getCollectionResourceCollectionUuid(item.id);
- if (uuid) {
+export const getFileOperationLocation = (item: ProjectsTreePickerItem) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<FileOperationLocation | undefined> => {
+ if ('kind' in item && item.kind === ResourceKind.COLLECTION) {
return {
- uuid,
- path: [item.path, item.name].join('/')
+ name: item.name,
+ uuid: item.uuid,
+ pdh: item.portableDataHash,
+ subpath: '/',
};
- } else {
- return undefined;
+ } else if ('type' in item && item.type === CollectionFileType.DIRECTORY) {
+ const uuid = getCollectionResourceCollectionUuid(item.id);
+ if (uuid) {
+ const collection = getResource<CollectionResource>(uuid)(getState().resources);
+ if (collection) {
+ const itemPath = [item.path, item.name].join('/');
+
+ return {
+ name: item.name,
+ uuid,
+ pdh: collection.portableDataHash,
+ subpath: itemPath,
+ };
+ }
+ }
}
- } else {
return undefined;
- }
-};
+ };
/**
* Create an expanded tree picker subtree from array of nested projects/collection
- * Assumes the root item of the subtree already has an empty string ownerUuid
+ * First item is assumed to be root and gets empty parent id
+ * Nodes must be sorted from top down to prevent orphaned nodes
*/
-export const createInitialLocationTree = (data: Array<GroupResource | CollectionResource>, tailUuid: string) => {
- return data
- .reduce((tree, item) => setNode({
- children: [],
- id: item.uuid,
- parent: item.ownerUuid,
- value: item,
- active: false,
- selected: false,
- expanded: false,
- status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
- })(tree), createTree<GroupResource | CollectionResource>());
+export const createInitialPickerTree = (sortedAncestors: Array<GroupResource | CollectionResource>, tailUuid: string, initialTree: Tree<GroupResource | CollectionResource>) => {
+ return sortedAncestors
+ .reduce((tree, item, index) => {
+ if (getNode(item.uuid)(tree)) {
+ return tree;
+ } else {
+ return setNode({
+ children: [],
+ id: item.uuid,
+ parent: index === 0 ? '' : item.ownerUuid,
+ value: item,
+ active: false,
+ selected: false,
+ expanded: false,
+ status: item.uuid !== tailUuid ? TreeNodeStatus.LOADED : TreeNodeStatus.INITIAL,
+ })(tree);
+ }
+ }, initialTree);
};
+
+export const fileOperationLocationToPickerId = (location: FileOperationLocation): string => {
+ let id = location.uuid;
+ if (location.subpath.length && location.subpath !== '/') {
+ id = id + location.subpath;
+ }
+ return id;
+}
const newState = pipe(
(state: TreePicker) => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '', nodes: [node], pickerId: "projects" })),
state => treePickerReducer(state, treePickerActions.LOAD_TREE_PICKER_NODE_SUCCESS({ id: '1', nodes: [subNode], pickerId: "projects" })),
- state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: '1.1', pickerId: "projects" })),
+ state => treePickerReducer(state, treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: '1.1', pickerId: "projects", cascade: true })),
)({ projects: createTree<{}>() });
expect(getNode('1')(newState.projects)).toEqual({
...initTreeNode({ id: '1', value: '1' }),
import {
createTree, TreeNode, setNode, Tree, TreeNodeStatus, setNodeStatus,
expandNode, deactivateNode, selectNodes, deselectNodes,
- activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree
+ activateNode, getNode, toggleNodeCollapse, toggleNodeSelection, appendSubtree, expandNodeAncestors
} from 'models/tree';
import { TreePicker } from "./tree-picker";
import { treePickerActions, treePickerSearchActions, TreePickerAction, TreePickerSearchAction, LoadProjectParams } from "./tree-picker-actions";
EXPAND_TREE_PICKER_NODE: ({ id, pickerId }) =>
updateOrCreatePicker(state, pickerId, expandNode(id)),
+ EXPAND_TREE_PICKER_NODE_ANCESTORS: ({ id, pickerId }) =>
+ updateOrCreatePicker(state, pickerId, expandNodeAncestors(id)),
+
ACTIVATE_TREE_PICKER_NODE: ({ id, pickerId, relatedTreePickers = [] }) =>
pipe(
() => relatedTreePickers.reduce(
DEACTIVATE_TREE_PICKER_NODE: ({ pickerId }) =>
updateOrCreatePicker(state, pickerId, deactivateNode),
- TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId }) =>
- updateOrCreatePicker(state, pickerId, toggleNodeSelection(id)),
+ TOGGLE_TREE_PICKER_NODE_SELECTION: ({ id, pickerId, cascade }) =>
+ updateOrCreatePicker(state, pickerId, toggleNodeSelection(id, cascade)),
- SELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
- updateOrCreatePicker(state, pickerId, selectNodes(id)),
+ SELECT_TREE_PICKER_NODE: ({ id, pickerId, cascade }) =>
+ updateOrCreatePicker(state, pickerId, selectNodes(id, cascade)),
- DESELECT_TREE_PICKER_NODE: ({ id, pickerId }) =>
- updateOrCreatePicker(state, pickerId, deselectNodes(id)),
+ DESELECT_TREE_PICKER_NODE: ({ id, pickerId, cascade }) =>
+ updateOrCreatePicker(state, pickerId, deselectNodes(id, cascade)),
RESET_TREE_PICKER: ({ pickerId }) =>
updateOrCreatePicker(state, pickerId, createTree),
import { UserPanelColumnNames } from 'views/user-panel/user-panel';
import { BuiltinGroups, getBuiltinGroupUuid } from 'models/group';
import { LinkClass } from 'models/link';
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
export class UserMiddlewareService extends DataExplorerMiddlewareService {
constructor(private services: ServiceRepository, id: string) {
const state = api.getState();
const dataExplorer = getDataExplorer(state.dataExplorer, this.getId());
try {
+ api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
const users = await this.services.userService.list(getParams(dataExplorer));
api.dispatch(updateResources(users.items));
api.dispatch(setItems(users));
api.dispatch(updateResources(allUserMemberships.items));
} catch {
api.dispatch(couldNotFetchUsers());
+ } finally {
+ api.dispatch(progressIndicatorActions.STOP_WORKING(this.getId()));
}
}
}
} else {
order.addOrder(sortDirection, sortColumn.sort.field);
}
+
+ // Use createdAt as a secondary sort column so we break ties consistently.
+ order.addOrder(OrderDirection.DESC, "createdAt");
}
return order.getOrder();
};
import { Participant } from "views-components/sharing-dialog/participant-select";
import { initialize, reset } from "redux-form";
import { getUserDisplayName, UserResource } from "models/user";
+import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
export const virtualMachinesActions = unionize({
SET_REQUESTED_DATE: ofType<string>(),
export const loadVirtualMachinesAdminData = () =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch<any>(loadRequestedDate());
-
- const virtualMachines = await services.virtualMachineService.list();
- dispatch(updateResources(virtualMachines.items));
- dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
-
-
- const logins = await services.permissionService.list({
- filters: new FilterBuilder()
- .addIn('head_uuid', virtualMachines.items.map(item => item.uuid))
- .addEqual('name', PermissionLevel.CAN_LOGIN)
- .getFilters(),
- limit: 1000
- });
- dispatch(updateResources(logins.items));
- dispatch(virtualMachinesActions.SET_LINKS(logins));
-
- const users = await services.userService.list({
- filters: new FilterBuilder()
- .addIn('uuid', logins.items.map(item => item.tailUuid))
- .getFilters(),
- count: "none", // Necessary for federated queries
- limit: 1000
- });
- dispatch(updateResources(users.items));
-
- const getAllLogins = await services.virtualMachineService.getAllLogins();
- dispatch(virtualMachinesActions.SET_LOGINS(getAllLogins));
+ try {
+ dispatch(progressIndicatorActions.START_WORKING("virtual-machines-admin"));
+ dispatch<any>(loadRequestedDate());
+
+ const virtualMachines = await services.virtualMachineService.list();
+ dispatch(updateResources(virtualMachines.items));
+ dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
+
+
+ const logins = await services.permissionService.list({
+ filters: new FilterBuilder()
+ .addIn('head_uuid', virtualMachines.items.map(item => item.uuid))
+ .addEqual('name', PermissionLevel.CAN_LOGIN)
+ .getFilters(),
+ limit: 1000
+ });
+ dispatch(updateResources(logins.items));
+ dispatch(virtualMachinesActions.SET_LINKS(logins));
+
+ const users = await services.userService.list({
+ filters: new FilterBuilder()
+ .addIn('uuid', logins.items.map(item => item.tailUuid))
+ .getFilters(),
+ count: "none", // Necessary for federated queries
+ limit: 1000
+ });
+ dispatch(updateResources(users.items));
+
+ const getAllLogins = await services.virtualMachineService.getAllLogins();
+ dispatch(virtualMachinesActions.SET_LOGINS(getAllLogins));
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING("virtual-machines-admin"));
+ }
};
export const loadVirtualMachinesUserData = () =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
- dispatch<any>(loadRequestedDate());
- const user = getState().auth.user;
- const virtualMachines = await services.virtualMachineService.list();
- const virtualMachinesUuids = virtualMachines.items.map(it => it.uuid);
- const links = await services.linkService.list({
- filters: new FilterBuilder()
- .addIn("head_uuid", virtualMachinesUuids)
- .addEqual("tail_uuid", user?.uuid)
- .getFilters()
- });
- dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
- dispatch(virtualMachinesActions.SET_LINKS(links));
+ try {
+ dispatch(progressIndicatorActions.START_WORKING("virtual-machines-user"));
+
+ dispatch<any>(loadRequestedDate());
+ const user = getState().auth.user;
+ const virtualMachines = await services.virtualMachineService.list();
+ const virtualMachinesUuids = virtualMachines.items.map(it => it.uuid);
+ const links = await services.linkService.list({
+ filters: new FilterBuilder()
+ .addIn("head_uuid", virtualMachinesUuids)
+ .addEqual("tail_uuid", user?.uuid)
+ .getFilters()
+ });
+ dispatch(virtualMachinesActions.SET_VIRTUAL_MACHINES(virtualMachines));
+ dispatch(virtualMachinesActions.SET_LINKS(links));
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING("virtual-machines-user"));
+ }
};
export const openAddVirtualMachineLoginDialog = (vmUuid: string) =>
dispatch(updateResources(virtualMachines.items));
const logins = await services.permissionService.list({
filters: new FilterBuilder()
- .addIn('head_uuid', virtualMachines.items.map(item => item.uuid))
- .addEqual('name', PermissionLevel.CAN_LOGIN)
- .getFilters()
+ .addIn('head_uuid', virtualMachines.items.map(item => item.uuid))
+ .addEqual('name', PermissionLevel.CAN_LOGIN)
+ .getFilters()
});
dispatch(updateResources(logins.items));
dispatch(initialize(VIRTUAL_MACHINE_ADD_LOGIN_FORM, {
- [VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: vmUuid,
- [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: [],
- }));
- dispatch(dialogActions.OPEN_DIALOG( {id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: {excludedParticipants: logins.items.map(it => it.tailUuid)}} ));
+ [VIRTUAL_MACHINE_ADD_LOGIN_VM_FIELD]: vmUuid,
+ [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: [],
+ }));
+ dispatch(dialogActions.OPEN_DIALOG({ id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: { excludedParticipants: logins.items.map(it => it.tailUuid) } }));
}
export const openEditVirtualMachineLoginDialog = (permissionUuid: string) =>
const login = await services.permissionService.get(permissionUuid);
const user = await services.userService.get(login.tailUuid);
dispatch(initialize(VIRTUAL_MACHINE_ADD_LOGIN_FORM, {
- [VIRTUAL_MACHINE_UPDATE_LOGIN_UUID_FIELD]: permissionUuid,
- [VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: {name: getUserDisplayName(user, true, true), uuid: login.tailUuid},
- [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: login.properties.groups,
- }));
- dispatch(dialogActions.OPEN_DIALOG( {id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: {updating: true}} ));
+ [VIRTUAL_MACHINE_UPDATE_LOGIN_UUID_FIELD]: permissionUuid,
+ [VIRTUAL_MACHINE_ADD_LOGIN_USER_FIELD]: { name: getUserDisplayName(user, true, true), uuid: login.tailUuid },
+ [VIRTUAL_MACHINE_ADD_LOGIN_GROUPS_FIELD]: login.properties.groups,
+ }));
+ dispatch(dialogActions.OPEN_DIALOG({ id: VIRTUAL_MACHINE_ADD_LOGIN_DIALOG, data: { updating: true } }));
}
export interface AddLoginFormData {
}
-export const addUpdateVirtualMachineLogin = ({uuid, vmUuid, user, groups}: AddLoginFormData) =>
+export const addUpdateVirtualMachineLogin = ({ uuid, vmUuid, user, groups }: AddLoginFormData) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
let userResource: UserResource | undefined = undefined;
try {
// Get user
userResource = await services.userService.get(user.uuid, false);
} catch (e) {
- dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Failed to get user details.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
- return;
+ dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Failed to get user details.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
+ return;
}
try {
if (uuid) {
//
// 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, setIsProjectPanelTrashed } from "store/project-panel/project-panel-action";
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
import {
activateSidePanelTreeItem,
initSidePanelTree,
loadSidePanelTreeProjects,
SidePanelTreeCategory,
-} from 'store/side-panel-tree/side-panel-tree-actions';
-import { updateResources } from 'store/resources/resources-actions';
-import { projectPanelColumns } from 'views/project-panel/project-panel';
-import { favoritePanelColumns } from 'views/favorite-panel/favorite-panel';
-import { matchRootRoute } from 'routes/routes';
+} from "store/side-panel-tree/side-panel-tree-actions";
+import { updateResources } from "store/resources/resources-actions";
+import { projectPanelColumns } from "views/project-panel/project-panel";
+import { favoritePanelColumns } from "views/favorite-panel/favorite-panel";
+import { matchRootRoute } from "routes/routes";
import {
setBreadcrumbs,
setGroupDetailsBreadcrumbs,
setUsersBreadcrumbs,
setMyAccountBreadcrumbs,
setUserProfileBreadcrumbs,
-} from 'store/breadcrumbs/breadcrumbs-actions';
-import {
- navigateTo,
- navigateToRootProject,
-} from 'store/navigation/navigation-action';
-import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
-import { ServiceRepository } from 'services/services';
-import { getResource } from 'store/resources/resources';
-import * as projectCreateActions from 'store/projects/project-create-actions';
-import * as projectMoveActions from 'store/projects/project-move-actions';
-import * as projectUpdateActions from 'store/projects/project-update-actions';
-import * as collectionCreateActions from 'store/collections/collection-create-actions';
-import * as collectionCopyActions from 'store/collections/collection-copy-actions';
-import * as collectionMoveActions from 'store/collections/collection-move-actions';
-import * as processesActions from 'store/processes/processes-actions';
-import * as processMoveActions from 'store/processes/process-move-actions';
-import * as processUpdateActions from 'store/processes/process-update-actions';
-import * as processCopyActions from 'store/processes/process-copy-actions';
-import { trashPanelColumns } from 'views/trash-panel/trash-panel';
-import {
- loadTrashPanel,
- trashPanelActions,
-} from 'store/trash-panel/trash-panel-action';
-import { loadProcessPanel } from 'store/process-panel/process-panel-actions';
-import {
- loadSharedWithMePanel,
- sharedWithMePanelActions,
-} from 'store/shared-with-me-panel/shared-with-me-panel-actions';
-import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
-import { workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
-import { loadSshKeysPanel } from 'store/auth/auth-action-ssh';
-import {
- loadLinkAccountPanel,
- linkAccountPanelActions,
-} from 'store/link-account-panel/link-account-panel-actions';
-import { loadSiteManagerPanel } from 'store/auth/auth-action-session';
-import { workflowPanelColumns } from 'views/workflow-panel/workflow-panel-view';
-import { progressIndicatorActions } from 'store/progress-indicator/progress-indicator-actions';
-import { getProgressIndicator } from 'store/progress-indicator/progress-indicator-reducer';
-import { extractUuidKind, ResourceKind } from 'models/resource';
-import { FilterBuilder } from 'services/api/filter-builder';
-import { GroupContentsResource } from 'services/groups-service/groups-service';
-import { MatchCases, ofType, unionize, UnionOf } from 'common/unionize';
-import { loadRunProcessPanel } from 'store/run-process-panel/run-process-panel-actions';
-import {
- collectionPanelActions,
- loadCollectionPanel,
-} from 'store/collection-panel/collection-panel-action';
-import { CollectionResource } from 'models/collection';
-import { WorkflowResource } from 'models/workflow';
-import {
- loadSearchResultsPanel,
- searchResultsPanelActions,
-} from 'store/search-results-panel/search-results-panel-actions';
-import { searchResultsPanelColumns } from 'views/search-results-panel/search-results-panel-view';
-import { loadVirtualMachinesPanel } from 'store/virtual-machines/virtual-machines-actions';
-import { loadRepositoriesPanel } from 'store/repositories/repositories-actions';
-import { loadKeepServicesPanel } from 'store/keep-services/keep-services-actions';
-import { loadUsersPanel, userBindedActions } from 'store/users/users-actions';
-import * as userProfilePanelActions from 'store/user-profile/user-profile-actions';
-import {
- linkPanelActions,
- loadLinkPanel,
-} from 'store/link-panel/link-panel-actions';
-import { linkPanelColumns } from 'views/link-panel/link-panel-root';
-import { userPanelColumns } from 'views/user-panel/user-panel';
-import {
- loadApiClientAuthorizationsPanel,
- apiClientAuthorizationsActions,
-} from 'store/api-client-authorizations/api-client-authorizations-actions';
-import { apiClientAuthorizationPanelColumns } from 'views/api-client-authorization-panel/api-client-authorization-panel-root';
-import * as groupPanelActions from 'store/groups-panel/groups-panel-actions';
-import { groupsPanelColumns } from 'views/groups-panel/groups-panel';
-import * as groupDetailsPanelActions from 'store/group-details-panel/group-details-panel-actions';
-import {
- groupDetailsMembersPanelColumns,
- groupDetailsPermissionsPanelColumns,
-} from 'views/group-details-panel/group-details-panel';
-import { DataTableFetchMode } from 'components/data-table/data-table';
-import {
- loadPublicFavoritePanel,
- publicFavoritePanelActions,
-} from 'store/public-favorites-panel/public-favorites-action';
-import { publicFavoritePanelColumns } from 'views/public-favorites-panel/public-favorites-panel';
+} from "store/breadcrumbs/breadcrumbs-actions";
+import { navigateTo, navigateToRootProject } from "store/navigation/navigation-action";
+import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
+import { ServiceRepository } from "services/services";
+import { getResource } from "store/resources/resources";
+import * as projectCreateActions from "store/projects/project-create-actions";
+import * as projectMoveActions from "store/projects/project-move-actions";
+import * as projectUpdateActions from "store/projects/project-update-actions";
+import * as collectionCreateActions from "store/collections/collection-create-actions";
+import * as collectionCopyActions from "store/collections/collection-copy-actions";
+import * as collectionMoveActions from "store/collections/collection-move-actions";
+import * as processesActions from "store/processes/processes-actions";
+import * as processMoveActions from "store/processes/process-move-actions";
+import * as processUpdateActions from "store/processes/process-update-actions";
+import * as processCopyActions from "store/processes/process-copy-actions";
+import { trashPanelColumns } from "views/trash-panel/trash-panel";
+import { loadTrashPanel, trashPanelActions } from "store/trash-panel/trash-panel-action";
+import { loadProcessPanel } from "store/process-panel/process-panel-actions";
+import { loadSharedWithMePanel, sharedWithMePanelActions } from "store/shared-with-me-panel/shared-with-me-panel-actions";
+import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
+import { workflowPanelActions } from "store/workflow-panel/workflow-panel-actions";
+import { loadSshKeysPanel } from "store/auth/auth-action-ssh";
+import { loadLinkAccountPanel, linkAccountPanelActions } from "store/link-account-panel/link-account-panel-actions";
+import { loadSiteManagerPanel } from "store/auth/auth-action-session";
+import { workflowPanelColumns } from "views/workflow-panel/workflow-panel-view";
+import { progressIndicatorActions } from "store/progress-indicator/progress-indicator-actions";
+import { getProgressIndicator } from "store/progress-indicator/progress-indicator-reducer";
+import { extractUuidKind, Resource, ResourceKind } from "models/resource";
+import { FilterBuilder } from "services/api/filter-builder";
+import { GroupContentsResource } from "services/groups-service/groups-service";
+import { MatchCases, ofType, unionize, UnionOf } from "common/unionize";
+import { loadRunProcessPanel } from "store/run-process-panel/run-process-panel-actions";
+import { collectionPanelActions, loadCollectionPanel } from "store/collection-panel/collection-panel-action";
+import { CollectionResource } from "models/collection";
+import { WorkflowResource } from "models/workflow";
+import { loadSearchResultsPanel, searchResultsPanelActions } from "store/search-results-panel/search-results-panel-actions";
+import { searchResultsPanelColumns } from "views/search-results-panel/search-results-panel-view";
+import { loadVirtualMachinesPanel } from "store/virtual-machines/virtual-machines-actions";
+import { loadRepositoriesPanel } from "store/repositories/repositories-actions";
+import { loadKeepServicesPanel } from "store/keep-services/keep-services-actions";
+import { loadUsersPanel, userBindedActions } from "store/users/users-actions";
+import * as userProfilePanelActions from "store/user-profile/user-profile-actions";
+import { linkPanelActions, loadLinkPanel } from "store/link-panel/link-panel-actions";
+import { linkPanelColumns } from "views/link-panel/link-panel-root";
+import { userPanelColumns } from "views/user-panel/user-panel";
+import { loadApiClientAuthorizationsPanel, apiClientAuthorizationsActions } from "store/api-client-authorizations/api-client-authorizations-actions";
+import { apiClientAuthorizationPanelColumns } from "views/api-client-authorization-panel/api-client-authorization-panel-root";
+import * as groupPanelActions from "store/groups-panel/groups-panel-actions";
+import { groupsPanelColumns } from "views/groups-panel/groups-panel";
+import * as groupDetailsPanelActions from "store/group-details-panel/group-details-panel-actions";
+import { groupDetailsMembersPanelColumns, groupDetailsPermissionsPanelColumns } from "views/group-details-panel/group-details-panel";
+import { DataTableFetchMode } from "components/data-table/data-table";
+import { loadPublicFavoritePanel, publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
+import { publicFavoritePanelColumns } from "views/public-favorites-panel/public-favorites-panel";
import {
loadCollectionsContentAddressPanel,
collectionsContentAddressActions,
-} from 'store/collections-content-address-panel/collections-content-address-panel-actions';
-import { collectionContentAddressPanelColumns } from 'views/collection-content-address-panel/collection-content-address-panel';
-import { subprocessPanelActions } from 'store/subprocess-panel/subprocess-panel-actions';
-import { subprocessPanelColumns } from 'views/subprocess-panel/subprocess-panel-root';
-import {
- loadAllProcessesPanel,
- allProcessesPanelActions,
-} from '../all-processes-panel/all-processes-panel-action';
-import { allProcessesPanelColumns } from 'views/all-processes-panel/all-processes-panel';
-import { AdminMenuIcon } from 'components/icon/icon';
-import { userProfileGroupsColumns } from 'views/user-profile-panel/user-profile-panel-root';
-
-export const WORKBENCH_LOADING_SCREEN = 'workbenchLoadingScreen';
+} from "store/collections-content-address-panel/collections-content-address-panel-actions";
+import { collectionContentAddressPanelColumns } from "views/collection-content-address-panel/collection-content-address-panel";
+import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-actions";
+import { subprocessPanelColumns } from "views/subprocess-panel/subprocess-panel-root";
+import { loadAllProcessesPanel, allProcessesPanelActions } from "../all-processes-panel/all-processes-panel-action";
+import { allProcessesPanelColumns } from "views/all-processes-panel/all-processes-panel";
+import { AdminMenuIcon } from "components/icon/icon";
+import { userProfileGroupsColumns } from "views/user-profile-panel/user-profile-panel-root";
+import { selectedToArray, selectedToKindSet } from "components/multiselect-toolbar/MultiselectToolbar";
+import { multiselectActions } from "store/multiselect/multiselect-actions";
+
+export const WORKBENCH_LOADING_SCREEN = "workbenchLoadingScreen";
export const isWorkbenchLoading = (state: RootState) => {
- const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(
- state.progressIndicator
- );
+ const progress = getProgressIndicator(WORKBENCH_LOADING_SCREEN)(state.progressIndicator);
return progress ? progress.working : false;
};
-export const handleFirstTimeLoad =
- (action: any) =>
- async (dispatch: Dispatch<any>, getState: () => RootState) => {
- try {
- await dispatch(action);
- } finally {
- if (isWorkbenchLoading(getState())) {
- dispatch(
- progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN)
- );
- }
- }
- };
+export const handleFirstTimeLoad = (action: any) => async (dispatch: Dispatch<any>, getState: () => RootState) => {
+ try {
+ await dispatch(action);
+ } finally {
+ if (isWorkbenchLoading(getState())) {
+ dispatch(progressIndicatorActions.STOP_WORKING(WORKBENCH_LOADING_SCREEN));
+ }
+ }
+};
-export const loadWorkbench =
- () =>
- async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
- const { auth, router } = getState();
- const { user } = auth;
- if (user) {
- dispatch(
- projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns })
- );
- dispatch(
- favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns })
- );
- dispatch(
- allProcessesPanelActions.SET_COLUMNS({
- columns: allProcessesPanelColumns,
- })
- );
- dispatch(
- publicFavoritePanelActions.SET_COLUMNS({
- columns: publicFavoritePanelColumns,
- })
- );
- dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
- dispatch(
- sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns })
- );
- dispatch(
- workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns })
- );
- dispatch(
- searchResultsPanelActions.SET_FETCH_MODE({
- fetchMode: DataTableFetchMode.INFINITE,
- })
- );
- dispatch(
- searchResultsPanelActions.SET_COLUMNS({
- columns: searchResultsPanelColumns,
- })
- );
- dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
- dispatch(
- groupPanelActions.GroupsPanelActions.SET_COLUMNS({
- columns: groupsPanelColumns,
- })
- );
- dispatch(
- groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({
- columns: groupDetailsMembersPanelColumns,
- })
- );
- dispatch(
- groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({
- columns: groupDetailsPermissionsPanelColumns,
- })
- );
- dispatch(
- userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({
- columns: userProfileGroupsColumns,
- })
- );
- dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
- dispatch(
- apiClientAuthorizationsActions.SET_COLUMNS({
- columns: apiClientAuthorizationPanelColumns,
- })
- );
- dispatch(
- collectionsContentAddressActions.SET_COLUMNS({
- columns: collectionContentAddressPanelColumns,
- })
- );
- dispatch(
- subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns })
- );
+export const loadWorkbench = () => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ dispatch(progressIndicatorActions.START_WORKING(WORKBENCH_LOADING_SCREEN));
+ const { auth, router } = getState();
+ const { user } = auth;
+ if (user) {
+ dispatch(projectPanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+ dispatch(favoritePanelActions.SET_COLUMNS({ columns: favoritePanelColumns }));
+ dispatch(
+ allProcessesPanelActions.SET_COLUMNS({
+ columns: allProcessesPanelColumns,
+ })
+ );
+ dispatch(
+ publicFavoritePanelActions.SET_COLUMNS({
+ columns: publicFavoritePanelColumns,
+ })
+ );
+ dispatch(trashPanelActions.SET_COLUMNS({ columns: trashPanelColumns }));
+ dispatch(sharedWithMePanelActions.SET_COLUMNS({ columns: projectPanelColumns }));
+ dispatch(workflowPanelActions.SET_COLUMNS({ columns: workflowPanelColumns }));
+ dispatch(
+ searchResultsPanelActions.SET_FETCH_MODE({
+ fetchMode: DataTableFetchMode.INFINITE,
+ })
+ );
+ dispatch(
+ searchResultsPanelActions.SET_COLUMNS({
+ columns: searchResultsPanelColumns,
+ })
+ );
+ dispatch(userBindedActions.SET_COLUMNS({ columns: userPanelColumns }));
+ dispatch(
+ groupPanelActions.GroupsPanelActions.SET_COLUMNS({
+ columns: groupsPanelColumns,
+ })
+ );
+ dispatch(
+ groupDetailsPanelActions.GroupMembersPanelActions.SET_COLUMNS({
+ columns: groupDetailsMembersPanelColumns,
+ })
+ );
+ dispatch(
+ groupDetailsPanelActions.GroupPermissionsPanelActions.SET_COLUMNS({
+ columns: groupDetailsPermissionsPanelColumns,
+ })
+ );
+ dispatch(
+ userProfilePanelActions.UserProfileGroupsActions.SET_COLUMNS({
+ columns: userProfileGroupsColumns,
+ })
+ );
+ dispatch(linkPanelActions.SET_COLUMNS({ columns: linkPanelColumns }));
+ dispatch(
+ apiClientAuthorizationsActions.SET_COLUMNS({
+ columns: apiClientAuthorizationPanelColumns,
+ })
+ );
+ dispatch(
+ collectionsContentAddressActions.SET_COLUMNS({
+ columns: collectionContentAddressPanelColumns,
+ })
+ );
+ dispatch(subprocessPanelActions.SET_COLUMNS({ columns: subprocessPanelColumns }));
- if (services.linkAccountService.getAccountToLink()) {
- dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
- }
+ if (services.linkAccountService.getAccountToLink()) {
+ dispatch(linkAccountPanelActions.HAS_SESSION_DATA());
+ }
- dispatch<any>(initSidePanelTree());
- if (router.location) {
- const match = matchRootRoute(router.location.pathname);
- if (match) {
- dispatch<any>(navigateToRootProject);
- }
- }
- } else {
- dispatch(userIsNotAuthenticated);
+ dispatch<any>(initSidePanelTree());
+ if (router.location) {
+ const match = matchRootRoute(router.location.pathname);
+ if (match) {
+ dispatch<any>(navigateToRootProject);
}
- };
+ }
+ } else {
+ dispatch(userIsNotAuthenticated);
+ }
+};
export const loadFavorites = () =>
handleFirstTimeLoad((dispatch: Dispatch) => {
dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.FAVORITES));
});
-export const loadCollectionContentAddress = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadCollectionsContentAddressPanel());
- }
-);
+export const loadCollectionContentAddress = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadCollectionsContentAddressPanel());
+});
export const loadTrash = () =>
handleFirstTimeLoad((dispatch: Dispatch) => {
export const loadAllProcesses = () =>
handleFirstTimeLoad((dispatch: Dispatch) => {
- dispatch<any>(
- activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES)
- );
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.ALL_PROCESSES));
dispatch<any>(loadAllProcessesPanel());
dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.ALL_PROCESSES));
});
export const loadProject = (uuid: string) =>
- handleFirstTimeLoad(
- async (
- dispatch: Dispatch<any>,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- const userUuid = getUserUuid(getState());
- dispatch(setIsProjectPanelTrashed(false));
- if (!userUuid) {
- return;
- }
+ handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getUserUuid(getState());
+ dispatch(setIsProjectPanelTrashed(false));
+ if (!userUuid) {
+ return;
+ }
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid));
if (extractUuidKind(uuid) === ResourceKind.USER && userUuid !== uuid) {
// Load another users home projects
dispatch(finishLoadingProject(uuid));
dispatch<any>(setSharedWithMeBreadcrumbs(uuid));
},
TRASHED: async () => {
- await dispatch(
- activateSidePanelTreeItem(SidePanelTreeCategory.TRASH)
- );
+ await dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
dispatch<any>(setTrashBreadcrumbs(uuid));
dispatch(setIsProjectPanelTrashed(true));
},
await dispatch(activateSidePanelTreeItem(userUuid));
dispatch<any>(setSidePanelBreadcrumbs(userUuid));
}
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid));
}
- );
+ });
-export const createProject =
- (data: projectCreateActions.ProjectCreateFormDialogData) =>
- async (dispatch: Dispatch) => {
- const newProject = await dispatch<any>(
- projectCreateActions.createProject(data)
- );
- if (newProject) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: 'Project has been successfully created.',
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
- })
- );
- await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
- dispatch<any>(navigateTo(newProject.uuid));
- }
- };
+export const createProject = (data: projectCreateActions.ProjectCreateFormDialogData) => async (dispatch: Dispatch) => {
+ const newProject = await dispatch<any>(projectCreateActions.createProject(data));
+ if (newProject) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully created.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(loadSidePanelTreeProjects(newProject.ownerUuid));
+ dispatch<any>(navigateTo(newProject.uuid));
+ }
+};
export const moveProject =
- (data: MoveToFormDialogData) =>
- async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- try {
- const oldProject = getResource(data.uuid)(getState().resources);
- const oldOwnerUuid = oldProject ? oldProject.ownerUuid : '';
- const movedProject = await dispatch<any>(
- projectMoveActions.moveProject(data)
- );
- if (movedProject) {
+ (data: MoveToFormDialogData, isSecondaryMove = false) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist default to normal context menu behavior
+ if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
+
+ const sourceUuid = getResource(data.uuid)(getState().resources)?.ownerUuid;
+ const destinationUuid = data.ownerUuid;
+
+ const projectsToMove: MoveableResource[] = uuidsToMove
+ .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
+ .filter(resource => resource.kind === ResourceKind.PROJECT);
+
+ for (const project of projectsToMove) {
+ await moveSingleProject(project);
+ }
+
+ //omly propagate if this call is the original
+ if (!isSecondaryMove) {
+ const kindsToMove: Set<string> = selectedToKindSet(checkedList);
+ kindsToMove.delete(ResourceKind.PROJECT);
+
+ kindsToMove.forEach(kind => {
+ secondaryMove[kind](data, true)(dispatch, getState, services);
+ });
+ }
+
+ async function moveSingleProject(project: MoveableResource) {
+ try {
+ const oldProject: MoveToFormDialogData = { name: project.name, uuid: project.uuid, ownerUuid: data.ownerUuid };
+ const oldOwnerUuid = oldProject ? oldProject.ownerUuid : "";
+ const movedProject = await dispatch<any>(projectMoveActions.moveProject(oldProject));
+ if (movedProject) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been moved",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(reloadProjectMatchingUuid([oldOwnerUuid, movedProject.ownerUuid, movedProject.uuid]));
+ }
+ } catch (e) {
dispatch(
snackbarActions.OPEN_SNACKBAR({
- message: 'Project has been moved',
+ message: e.message,
hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
+ kind: SnackbarKind.ERROR,
})
);
- if (oldProject) {
- await dispatch<any>(loadSidePanelTreeProjects(oldProject.ownerUuid));
- }
- dispatch<any>(
- reloadProjectMatchingUuid([
- oldOwnerUuid,
- movedProject.ownerUuid,
- movedProject.uuid,
- ])
- );
}
- } catch (e) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: e.message,
- hideDuration: 2000,
- kind: SnackbarKind.ERROR,
- })
- );
}
+ if (sourceUuid) await dispatch<any>(loadSidePanelTreeProjects(sourceUuid));
+ await dispatch<any>(loadSidePanelTreeProjects(destinationUuid));
};
-export const updateProject =
- (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
- async (dispatch: Dispatch) => {
- const updatedProject = await dispatch<any>(
- projectUpdateActions.updateProject(data)
- );
- if (updatedProject) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: 'Project has been successfully updated.',
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
- })
- );
- await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
- dispatch<any>(
- reloadProjectMatchingUuid([
- updatedProject.ownerUuid,
- updatedProject.uuid,
- ])
- );
- }
- };
+export const updateProject = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
+ const updatedProject = await dispatch<any>(projectUpdateActions.updateProject(data));
+ if (updatedProject) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Project has been successfully updated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(loadSidePanelTreeProjects(updatedProject.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([updatedProject.ownerUuid, updatedProject.uuid]));
+ }
+};
-export const updateGroup =
- (data: projectUpdateActions.ProjectUpdateFormDialogData) =>
- async (dispatch: Dispatch) => {
- const updatedGroup = await dispatch<any>(
- groupPanelActions.updateGroup(data)
- );
- if (updatedGroup) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: 'Group has been successfully updated.',
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
- })
- );
- await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
- dispatch<any>(
- reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid])
- );
- }
- };
+export const updateGroup = (data: projectUpdateActions.ProjectUpdateFormDialogData) => async (dispatch: Dispatch) => {
+ const updatedGroup = await dispatch<any>(groupPanelActions.updateGroup(data));
+ if (updatedGroup) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Group has been successfully updated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ await dispatch<any>(loadSidePanelTreeProjects(updatedGroup.ownerUuid));
+ dispatch<any>(reloadProjectMatchingUuid([updatedGroup.ownerUuid, updatedGroup.uuid]));
+ }
+};
export const loadCollection = (uuid: string) =>
- handleFirstTimeLoad(
- async (
- dispatch: Dispatch<any>,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- const userUuid = getUserUuid(getState());
+ handleFirstTimeLoad(async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ const userUuid = getUserUuid(getState());
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid));
if (userUuid) {
const match = await loadGroupContentsResource({
uuid,
services,
});
let collection: CollectionResource | undefined;
- let breadcrumbfunc: ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>) | undefined;
+ let breadcrumbfunc:
+ | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
+ | undefined;
let sidepanel: string | undefined;
match({
- OWNED: (thecollection) => {
+ OWNED: thecollection => {
collection = thecollection as CollectionResource;
sidepanel = collection.ownerUuid;
breadcrumbfunc = setSidePanelBreadcrumbs;
},
- SHARED: (thecollection) => {
+ SHARED: thecollection => {
collection = thecollection as CollectionResource;
sidepanel = collection.ownerUuid;
breadcrumbfunc = setSharedWithMeBreadcrumbs;
},
- TRASHED: (thecollection) => {
+ TRASHED: thecollection => {
collection = thecollection as CollectionResource;
sidepanel = SidePanelTreeCategory.TRASH;
- breadcrumbfunc = () => setTrashBreadcrumbs('');
+ breadcrumbfunc = () => setTrashBreadcrumbs("");
},
});
if (collection && breadcrumbfunc && sidepanel) {
dispatch(loadCollectionPanel(collection.uuid));
}
}
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid));
}
- );
+ });
-export const createCollection =
- (data: collectionCreateActions.CollectionCreateFormDialogData) =>
- async (dispatch: Dispatch) => {
+export const createCollection = (data: collectionCreateActions.CollectionCreateFormDialogData) => async (dispatch: Dispatch) => {
+ const collection = await dispatch<any>(collectionCreateActions.createCollection(data));
+ if (collection) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Collection has been successfully created.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ dispatch<any>(updateResources([collection]));
+ dispatch<any>(navigateTo(collection.uuid));
+ }
+};
+
+export const copyCollection = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToCopy: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist && no items passed in, default to normal context menu behavior
+ if (!uuidsToCopy.length) uuidsToCopy.push(data.uuid);
+
+ const collectionsToCopy: CollectionCopyResource[] = uuidsToCopy
+ .map(uuid => getResource(uuid)(getState().resources) as CollectionCopyResource)
+ .filter(resource => resource.kind === ResourceKind.COLLECTION);
+
+ for (const collection of collectionsToCopy) {
+ await copySingleCollection({ ...collection, ownerUuid: data.ownerUuid } as CollectionCopyResource);
+ }
+
+ async function copySingleCollection(copyToProject: CollectionCopyResource) {
+ const newName = data.fromContextMenu || collectionsToCopy.length === 1 ? data.name : `Copy of: ${copyToProject.name}`;
+ try {
const collection = await dispatch<any>(
- collectionCreateActions.createCollection(data)
+ collectionCopyActions.copyCollection({
+ ...copyToProject,
+ name: newName,
+ fromContextMenu: collectionsToCopy.length === 1 ? true : data.fromContextMenu,
+ })
);
- if (collection) {
+ if (copyToProject && collection) {
+ await dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
dispatch(
snackbarActions.OPEN_SNACKBAR({
- message: 'Collection has been successfully created.',
- hideDuration: 2000,
+ message: "Collection has been copied.",
+ hideDuration: 3000,
kind: SnackbarKind.SUCCESS,
+ link: collection.ownerUuid,
})
);
- dispatch<any>(updateResources([collection]));
- dispatch<any>(navigateTo(collection.uuid));
+ dispatch<any>(multiselectActions.deselectOne(copyToProject.uuid));
}
- };
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+ }
+ dispatch(projectPanelActions.REQUEST_ITEMS());
+};
-export const copyCollection =
- (data: CopyFormDialogData) =>
- async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- try {
- const copyToProject = getResource(data.ownerUuid)(getState().resources);
- const collection = await dispatch<any>(
- collectionCopyActions.copyCollection(data)
- );
- if (copyToProject && collection) {
- dispatch<any>(reloadProjectMatchingUuid([copyToProject.uuid]));
+export const moveCollection =
+ (data: MoveToFormDialogData, isSecondaryMove = false) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist && no items passed in, default to normal context menu behavior
+ if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
+
+ const collectionsToMove: MoveableResource[] = uuidsToMove
+ .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
+ .filter(resource => resource.kind === ResourceKind.COLLECTION);
+
+ for (const collection of collectionsToMove) {
+ await moveSingleCollection(collection);
+ }
+
+ //omly propagate if this call is the original
+ if (!isSecondaryMove) {
+ const kindsToMove: Set<string> = selectedToKindSet(checkedList);
+ kindsToMove.delete(ResourceKind.COLLECTION);
+
+ kindsToMove.forEach(kind => {
+ secondaryMove[kind](data, true)(dispatch, getState, services);
+ });
+ }
+
+ async function moveSingleCollection(collection: MoveableResource) {
+ try {
+ const oldCollection: MoveToFormDialogData = { name: collection.name, uuid: collection.uuid, ownerUuid: data.ownerUuid };
+ const movedCollection = await dispatch<any>(collectionMoveActions.moveCollection(oldCollection));
+ dispatch<any>(updateResources([movedCollection]));
+ dispatch<any>(reloadProjectMatchingUuid([movedCollection.ownerUuid]));
dispatch(
snackbarActions.OPEN_SNACKBAR({
- message: 'Collection has been copied.',
- hideDuration: 3000,
+ message: "Collection has been moved.",
+ hideDuration: 2000,
kind: SnackbarKind.SUCCESS,
- link: collection.ownerUuid,
+ })
+ );
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
})
);
}
- } catch (e) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: e.message,
- hideDuration: 2000,
- kind: SnackbarKind.ERROR,
- })
- );
- }
- };
-
-export const moveCollection =
- (data: MoveToFormDialogData) =>
- async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- try {
- const collection = await dispatch<any>(
- collectionMoveActions.moveCollection(data)
- );
- dispatch<any>(updateResources([collection]));
- dispatch<any>(reloadProjectMatchingUuid([collection.ownerUuid]));
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: 'Collection has been moved.',
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
- })
- );
- } catch (e) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: e.message,
- hideDuration: 2000,
- kind: SnackbarKind.ERROR,
- })
- );
}
};
export const loadProcess = (uuid: string) =>
handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
- dispatch<any>(loadProcessPanel(uuid));
- const process = await dispatch<any>(processesActions.loadProcess(uuid));
- if (process) {
- await dispatch<any>(finishLoadingProject(process.containerRequest.ownerUuid));
- await dispatch<any>(
- activateSidePanelTreeItem(process.containerRequest.ownerUuid)
- );
- dispatch<any>(setProcessBreadcrumbs(uuid));
- dispatch<any>(loadDetailsPanel(uuid));
+ try {
+ dispatch(progressIndicatorActions.START_WORKING(uuid));
+ dispatch<any>(loadProcessPanel(uuid));
+ const process = await dispatch<any>(processesActions.loadProcess(uuid));
+ if (process) {
+ await dispatch<any>(finishLoadingProject(process.containerRequest.ownerUuid));
+ await dispatch<any>(activateSidePanelTreeItem(process.containerRequest.ownerUuid));
+ dispatch<any>(setProcessBreadcrumbs(uuid));
+ dispatch<any>(loadDetailsPanel(uuid));
+ }
+ } finally {
+ dispatch(progressIndicatorActions.STOP_WORKING(uuid));
}
});
export const loadRegisteredWorkflow = (uuid: string) =>
- handleFirstTimeLoad(async (dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository) => {
-
+ handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
const userUuid = getUserUuid(getState());
if (userUuid) {
const match = await loadGroupContentsResource({
services,
});
let workflow: WorkflowResource | undefined;
- let breadcrumbfunc: ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>) | undefined;
+ let breadcrumbfunc:
+ | ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>)
+ | undefined;
match({
- OWNED: async (theworkflow) => {
+ OWNED: async theworkflow => {
workflow = theworkflow as WorkflowResource;
breadcrumbfunc = setSidePanelBreadcrumbs;
},
- SHARED: async (theworkflow) => {
+ SHARED: async theworkflow => {
workflow = theworkflow as WorkflowResource;
breadcrumbfunc = setSharedWithMeBreadcrumbs;
},
- TRASHED: () => { }
+ TRASHED: () => { },
});
if (workflow && breadcrumbfunc) {
dispatch(updateResources([workflow]));
}
});
-export const updateProcess =
- (data: processUpdateActions.ProcessUpdateFormDialogData) =>
- async (dispatch: Dispatch) => {
- try {
- const process = await dispatch<any>(
- processUpdateActions.updateProcess(data)
- );
- if (process) {
+export const updateProcess = (data: processUpdateActions.ProcessUpdateFormDialogData) => async (dispatch: Dispatch) => {
+ try {
+ const process = await dispatch<any>(processUpdateActions.updateProcess(data));
+ if (process) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Process has been successfully updated.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ }
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+};
+
+export const moveProcess =
+ (data: MoveToFormDialogData, isSecondaryMove = false) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const checkedList = getState().multiselect.checkedList;
+ const uuidsToMove: string[] = data.fromContextMenu ? [data.uuid] : selectedToArray(checkedList);
+
+ //if no items in checkedlist && no items passed in, default to normal context menu behavior
+ if (!isSecondaryMove && !uuidsToMove.length) uuidsToMove.push(data.uuid);
+
+ const processesToMove: MoveableResource[] = uuidsToMove
+ .map(uuid => getResource(uuid)(getState().resources) as MoveableResource)
+ .filter(resource => resource.kind === ResourceKind.PROCESS);
+
+ for (const process of processesToMove) {
+ await moveSingleProcess(process);
+ }
+
+ //omly propagate if this call is the original
+ if (!isSecondaryMove) {
+ const kindsToMove: Set<string> = selectedToKindSet(checkedList);
+ kindsToMove.delete(ResourceKind.PROCESS);
+
+ kindsToMove.forEach(kind => {
+ secondaryMove[kind](data, true)(dispatch, getState, services);
+ });
+ }
+
+ async function moveSingleProcess(process: MoveableResource) {
+ try {
+ const oldProcess: MoveToFormDialogData = { name: process.name, uuid: process.uuid, ownerUuid: data.ownerUuid };
+ const movedProcess = await dispatch<any>(processMoveActions.moveProcess(oldProcess));
+ dispatch<any>(updateResources([movedProcess]));
+ dispatch<any>(reloadProjectMatchingUuid([movedProcess.ownerUuid]));
dispatch(
snackbarActions.OPEN_SNACKBAR({
- message: 'Process has been successfully updated.',
+ message: "Process has been moved.",
hideDuration: 2000,
kind: SnackbarKind.SUCCESS,
})
);
- dispatch<any>(updateResources([process]));
- dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
}
- } catch (e) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: e.message,
- hideDuration: 2000,
- kind: SnackbarKind.ERROR,
- })
- );
- }
- };
-
-export const moveProcess =
- (data: MoveToFormDialogData) =>
- async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- try {
- const process = await dispatch<any>(processMoveActions.moveProcess(data));
- dispatch<any>(updateResources([process]));
- dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: 'Process has been moved.',
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
- })
- );
- } catch (e) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: e.message,
- hideDuration: 2000,
- kind: SnackbarKind.ERROR,
- })
- );
}
};
-export const copyProcess =
- (data: CopyFormDialogData) =>
- async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- try {
- const process = await dispatch<any>(processCopyActions.copyProcess(data));
- dispatch<any>(updateResources([process]));
- dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: 'Process has been copied.',
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
- })
- );
- dispatch<any>(navigateTo(process.uuid));
- } catch (e) {
- dispatch(
- snackbarActions.OPEN_SNACKBAR({
- message: e.message,
- hideDuration: 2000,
- kind: SnackbarKind.ERROR,
- })
- );
- }
- };
+export const copyProcess = (data: CopyFormDialogData) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const process = await dispatch<any>(processCopyActions.copyProcess(data));
+ dispatch<any>(updateResources([process]));
+ dispatch<any>(reloadProjectMatchingUuid([process.ownerUuid]));
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: "Process has been copied.",
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
+ dispatch<any>(navigateTo(process.uuid));
+ } catch (e) {
+ dispatch(
+ snackbarActions.OPEN_SNACKBAR({
+ message: e.message,
+ hideDuration: 2000,
+ kind: SnackbarKind.ERROR,
+ })
+ );
+ }
+};
export const resourceIsNotLoaded = (uuid: string) =>
snackbarActions.OPEN_SNACKBAR({
});
export const userIsNotAuthenticated = snackbarActions.OPEN_SNACKBAR({
- message: 'User is not authenticated',
+ message: "User is not authenticated",
kind: SnackbarKind.ERROR,
});
export const couldNotLoadUser = snackbarActions.OPEN_SNACKBAR({
- message: 'Could not load user',
+ message: "Could not load user",
kind: SnackbarKind.ERROR,
});
export const reloadProjectMatchingUuid =
- (matchingUuids: string[]) =>
- async (
- dispatch: Dispatch,
- getState: () => RootState,
- services: ServiceRepository
- ) => {
- const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
- if (
- currentProjectPanelUuid &&
- matchingUuids.some((uuid) => uuid === currentProjectPanelUuid)
- ) {
- dispatch<any>(loadProject(currentProjectPanelUuid));
- }
- };
+ (matchingUuids: string[]) => async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ const currentProjectPanelUuid = getProjectPanelCurrentUuid(getState());
+ if (currentProjectPanelUuid && matchingUuids.some(uuid => uuid === currentProjectPanelUuid)) {
+ dispatch<any>(loadProject(currentProjectPanelUuid));
+ }
+ };
-export const loadSharedWithMe = handleFirstTimeLoad(
- async (dispatch: Dispatch) => {
- dispatch<any>(loadSharedWithMePanel());
- await dispatch<any>(
- activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME)
- );
- await dispatch<any>(
- setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME)
- );
- }
-);
+export const loadSharedWithMe = handleFirstTimeLoad(async (dispatch: Dispatch) => {
+ dispatch<any>(loadSharedWithMePanel());
+ await dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.SHARED_WITH_ME));
+ await dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.SHARED_WITH_ME));
+});
-export const loadRunProcess = handleFirstTimeLoad(
- async (dispatch: Dispatch) => {
- await dispatch<any>(loadRunProcessPanel());
- }
-);
+export const loadRunProcess = handleFirstTimeLoad(async (dispatch: Dispatch) => {
+ await dispatch<any>(loadRunProcessPanel());
+});
export const loadPublicFavorites = () =>
handleFirstTimeLoad((dispatch: Dispatch) => {
- dispatch<any>(
- activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES)
- );
+ dispatch<any>(activateSidePanelTreeItem(SidePanelTreeCategory.PUBLIC_FAVORITES));
dispatch<any>(loadPublicFavoritePanel());
- dispatch<any>(
- setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES)
- );
+ dispatch<any>(setSidePanelBreadcrumbs(SidePanelTreeCategory.PUBLIC_FAVORITES));
});
-export const loadSearchResults = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadSearchResultsPanel());
- }
-);
+export const loadSearchResults = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadSearchResultsPanel());
+});
-export const loadLinks = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadLinkPanel());
- }
-);
+export const loadLinks = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadLinkPanel());
+});
-export const loadVirtualMachines = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadVirtualMachinesPanel());
- dispatch(setBreadcrumbs([{ label: 'Virtual Machines' }]));
- }
-);
+export const loadVirtualMachines = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setBreadcrumbs([{ label: "Virtual Machines" }]));
+});
-export const loadVirtualMachinesAdmin = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadVirtualMachinesPanel());
- dispatch(
- setBreadcrumbs([{ label: 'Virtual Machines Admin', icon: AdminMenuIcon }])
- );
- }
-);
+export const loadVirtualMachinesAdmin = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadVirtualMachinesPanel());
+ dispatch(setBreadcrumbs([{ label: "Virtual Machines Admin", icon: AdminMenuIcon }]));
+});
-export const loadRepositories = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadRepositoriesPanel());
- dispatch(setBreadcrumbs([{ label: 'Repositories' }]));
- }
-);
+export const loadRepositories = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadRepositoriesPanel());
+ dispatch(setBreadcrumbs([{ label: "Repositories" }]));
+});
-export const loadSshKeys = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadSshKeysPanel());
- }
-);
+export const loadSshKeys = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadSshKeysPanel());
+});
-export const loadSiteManager = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadSiteManagerPanel());
- }
-);
+export const loadSiteManager = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadSiteManagerPanel());
+});
export const loadUserProfile = (userUuid?: string) =>
handleFirstTimeLoad((dispatch: Dispatch<any>) => {
}
});
-export const loadLinkAccount = handleFirstTimeLoad(
- (dispatch: Dispatch<any>) => {
- dispatch(loadLinkAccountPanel());
- }
-);
+export const loadLinkAccount = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+ dispatch(loadLinkAccountPanel());
+});
-export const loadKeepServices = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadKeepServicesPanel());
- }
-);
+export const loadKeepServices = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadKeepServicesPanel());
+});
-export const loadUsers = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadUsersPanel());
- dispatch(setUsersBreadcrumbs());
- }
-);
+export const loadUsers = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadUsersPanel());
+ dispatch(setUsersBreadcrumbs());
+});
-export const loadApiClientAuthorizations = handleFirstTimeLoad(
- async (dispatch: Dispatch<any>) => {
- await dispatch(loadApiClientAuthorizationsPanel());
- }
-);
+export const loadApiClientAuthorizations = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
+ await dispatch(loadApiClientAuthorizationsPanel());
+});
-export const loadGroupsPanel = handleFirstTimeLoad(
- (dispatch: Dispatch<any>) => {
- dispatch(setGroupsBreadcrumbs());
- dispatch(groupPanelActions.loadGroupsPanel());
- }
-);
+export const loadGroupsPanel = handleFirstTimeLoad((dispatch: Dispatch<any>) => {
+ dispatch(setGroupsBreadcrumbs());
+ dispatch(groupPanelActions.loadGroupsPanel());
+});
export const loadGroupDetailsPanel = (groupUuid: string) =>
handleFirstTimeLoad((dispatch: Dispatch<any>) => {
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 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 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) {
handler =
- (resource.kind === ResourceKind.COLLECTION ||
- resource.kind === ResourceKind.PROJECT) &&
- resource.isTrashed
+ (resource.kind === ResourceKind.COLLECTION || resource.kind === ResourceKind.PROJECT) && resource.isTrashed
? groupContentsHandlers.TRASHED(resource)
: groupContentsHandlers.OWNED(resource);
} else {
} else if (kind === ResourceKind.CONTAINER_REQUEST) {
resource = await params.services.containerRequestService.get(params.uuid);
} else {
- throw new Error("loadGroupContentsResource unsupported kind " + kind)
+ throw new Error("loadGroupContentsResource unsupported kind " + kind);
}
handler = groupContentsHandlers.SHARED(resource);
}
- return (
- cases: MatchCases<
- typeof groupContentsHandlersRecord,
- GroupContentsHandler,
- void
- >
- ) => groupContentsHandlers.match(handler, cases);
+ return (cases: MatchCases<typeof groupContentsHandlersRecord, GroupContentsHandler, void>) => groupContentsHandlers.match(handler, cases);
};
const groupContentsHandlersRecord = {
const groupContentsHandlers = unionize(groupContentsHandlersRecord);
type GroupContentsHandler = UnionOf<typeof groupContentsHandlers>;
+
+type CollectionCopyResource = Resource & { name: string; fromContextMenu: boolean };
+
+type MoveableResource = Resource & { name: string };
+
+type MoveFunc = (
+ data: MoveToFormDialogData,
+ isSecondaryMove?: boolean
+) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>;
+
+const secondaryMove: Record<string, MoveFunc> = {
+ [ResourceKind.PROJECT]: moveProject,
+ [ResourceKind.PROCESS]: moveProcess,
+ [ResourceKind.COLLECTION]: moveCollection,
+};
import { getResource } from 'store/resources/resources';
import { ProjectResource } from 'models/project';
import { UserResource } from 'models/user';
-import { getUserUuid } from "common/getuser";
import { getWorkflowInputs, parseWorkflowDefinition } from 'models/workflow';
export const WORKFLOW_PANEL_ID = "workflowPanel";
let owner;
if (ownerUuid) {
// Must be writable.
- const userUuid = getUserUuid(getState());
owner = getResource<ProjectResource | UserResource>(ownerUuid)(getState().resources);
if (!owner || !owner.canWrite) {
owner = undefined;
import {
openApiClientAuthorizationAttributesDialog,
- openApiClientAuthorizationRemoveDialog
-} from 'store/api-client-authorizations/api-client-authorizations-actions';
-import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
+ openApiClientAuthorizationRemoveDialog,
+} from "store/api-client-authorizations/api-client-authorizations-actions";
+import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
-export const apiClientAuthorizationActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openApiClientAuthorizationAttributesDialog(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openApiClientAuthorizationRemoveDialog(uuid));
- }
-}]];
+export const apiClientAuthorizationActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: "Attributes",
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openApiClientAuthorizationAttributesDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: "API Details",
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openApiClientAuthorizationRemoveDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import {
- ContextMenuAction,
- ContextMenuActionSet
-} from "../context-menu-action-set";
+import { ContextMenuAction, ContextMenuActionSet } from "../context-menu-action-set";
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "store/favorites/favorites-actions";
import {
OpenIcon,
Link,
RestoreVersionIcon,
- FolderSharedIcon
+ FolderSharedIcon,
} from "components/icon/icon";
import { openCollectionUpdateDialog } from "store/collections/collection-update-actions";
import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
-import { openMoveCollectionDialog } from 'store/collections/collection-move-actions';
-import { openCollectionCopyDialog } from "store/collections/collection-copy-actions";
+import { openMoveCollectionDialog } from "store/collections/collection-move-actions";
+import { openCollectionCopyDialog, openMultiCollectionCopyDialog } from "store/collections/collection-copy-actions";
import { openWebDavS3InfoDialog } from "store/collections/collection-info-actions";
import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
import { toggleCollectionTrashed } from "store/trash/trash-actions";
-import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
+import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
-import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { toggleDetailsPanel } from "store/details-panel/details-panel-action";
import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
import { openRestoreCollectionVersionDialog } from "store/collections/collection-version-actions";
import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
import { publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
const toggleFavoriteAction: ContextMenuAction = {
component: ToggleFavoriteAction,
- name: 'ToggleFavoriteAction',
- execute: (dispatch, resource) => {
- dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
- });
- }
-};
-
-const commonActionSet: 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: CopyIcon,
- name: "Make a copy",
- execute: (dispatch, resource) => {
- dispatch<any>(openCollectionCopyDialog(resource));
- }
-
- },
- {
- icon: DetailsIcon,
- name: "View details",
- execute: dispatch => {
- dispatch<any>(toggleDetailsPanel());
- }
- },
- {
- icon: AdvancedIcon,
- name: "API Details",
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
+ name: "ToggleFavoriteAction",
+ execute: (dispatch, resources) => {
+ for (const resource of [...resources]) {
+ dispatch<any>(toggleFavorite(resource)).then(() => {
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ });
}
},
-]];
+};
+const commonActionSet: ContextMenuActionSet = [
+ [
+ {
+ icon: OpenIcon,
+ name: "Open in new tab",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openInNewTabAction(resources[0]));
+ },
+ },
+ {
+ icon: Link,
+ name: "Copy to clipboard",
+ execute: (dispatch, resources) => {
+ dispatch<any>(copyToClipboardAction(resources));
+ },
+ },
+ {
+ icon: CopyIcon,
+ name: "Make a copy",
+ execute: (dispatch, resources) => {
+ if (resources[0].fromContextMenu || resources.length === 1) dispatch<any>(openCollectionCopyDialog(resources[0]));
+ else dispatch<any>(openMultiCollectionCopyDialog(resources[0]));
+ },
+ },
+ {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: dispatch => {
+ dispatch<any>(toggleDetailsPanel());
+ },
+ },
+ {
+ icon: AdvancedIcon,
+ name: "API Details",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
-export const readOnlyCollectionActionSet: ContextMenuActionSet = [[
- ...commonActionSet.reduce((prev, next) => prev.concat(next), []),
- toggleFavoriteAction,
- {
- icon: FolderSharedIcon,
- name: "Open with 3rd party client",
- execute: (dispatch, resource) => {
- dispatch<any>(openWebDavS3InfoDialog(resource.uuid));
- }
- },
-]];
+export const readOnlyCollectionActionSet: ContextMenuActionSet = [
+ [
+ ...commonActionSet.reduce((prev, next) => prev.concat(next), []),
+ toggleFavoriteAction,
+ {
+ icon: FolderSharedIcon,
+ name: "Open with 3rd party client",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openWebDavS3InfoDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
export const collectionActionSet: ContextMenuActionSet = [
[
{
icon: RenameIcon,
name: "Edit collection",
- execute: (dispatch, resource) => {
- dispatch<any>(openCollectionUpdateDialog(resource));
- }
+ execute: (dispatch, resources) => {
+ dispatch<any>(openCollectionUpdateDialog(resources[0]));
+ },
},
{
icon: ShareIcon,
name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
+ execute: (dispatch, resources) => {
+ dispatch<any>(openSharingDialog(resources[0].uuid));
+ },
},
{
icon: MoveToIcon,
name: "Move to",
- execute: (dispatch, resource) => dispatch<any>(openMoveCollectionDialog(resource))
+ execute: (dispatch, resources) => dispatch<any>(openMoveCollectionDialog(resources[0])),
},
{
component: ToggleTrashAction,
- name: 'ToggleTrashAction',
- execute: (dispatch, resource) => {
- dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
- }
+ name: "ToggleTrashAction",
+ execute: (dispatch, resources: ContextMenuResource[]) => {
+ for (const resource of [...resources]) {
+ dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+ }
+ },
},
- ]
+ ],
];
export const collectionAdminActionSet: ContextMenuActionSet = [
...collectionActionSet.reduce((prev, next) => prev.concat(next), []),
{
component: TogglePublicFavoriteAction,
- name: 'TogglePublicFavoriteAction',
- execute: (dispatch, resource) => {
- dispatch<any>(togglePublicFavorite(resource)).then(() => {
- dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
- });
- }
+ name: "TogglePublicFavoriteAction",
+ execute: (dispatch, resources) => {
+ for (const resource of [...resources]) {
+ dispatch<any>(togglePublicFavorite(resource)).then(() => {
+ dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
+ });
+ }
+ },
},
- ]
+ ],
];
export const oldCollectionVersionActionSet: ContextMenuActionSet = [
...commonActionSet.reduce((prev, next) => prev.concat(next), []),
{
icon: RestoreVersionIcon,
- name: 'Restore version',
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openRestoreCollectionVersionDialog(uuid));
- }
+ name: "Restore version",
+ execute: (dispatch, resources) => {
+ for (const resource of [...resources]) {
+ dispatch<any>(openRestoreCollectionVersionDialog(resource.uuid));
+ }
+ },
},
- ]
+ ],
];
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: "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) => {
+ dispatch<any>(
+ openRenameFileDialog({
+ name: resources[0].name,
+ id: resources[0].uuid,
+ path: resources[0].uuid.split("/").slice(1).join("/"),
+ })
+ );
+ },
+ },
+ {
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openFileRemoveDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
export const collectionDirectoryItemActionSet: ContextMenuActionSet = readOnlyCollectionDirectoryItemActionSet.concat(writableActionSet);
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "store/favorites/favorites-actions";
-import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
+import { ContextMenuActionSet } from '../context-menu-action-set';
+import { ToggleFavoriteAction } from '../actions/favorite-action';
+import { toggleFavorite } from 'store/favorites/favorites-actions';
+import { favoritePanelActions } from 'store/favorite-panel/favorite-panel-action';
-export const favoriteActionSet: ContextMenuActionSet = [[{
- component: ToggleFavoriteAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
- });
- }
-}]];
+export const favoriteActionSet: ContextMenuActionSet = [
+ [
+ {
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) =>
+ dispatch<any>(toggleFavorite(resource)).then(() => {
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ })
+ );
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { RenameIcon, AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
-import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
-import { openGroupAttributes, openRemoveGroupDialog, openGroupUpdateDialog } from "store/groups-panel/groups-panel-actions";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { RenameIcon, AdvancedIcon, RemoveIcon, AttributesIcon } from 'components/icon/icon';
+import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
+import { openGroupAttributes, openRemoveGroupDialog, openGroupUpdateDialog } from 'store/groups-panel/groups-panel-actions';
-export const groupActionSet: ContextMenuActionSet = [[{
- name: "Rename",
- icon: RenameIcon,
- execute: (dispatch, resource) => {
- dispatch<any>(openGroupUpdateDialog(resource));
- }
-}, {
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openGroupAttributes(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openRemoveGroupDialog(uuid));
- }
-}]];
+export const groupActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Rename',
+ icon: RenameIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openGroupUpdateDialog(resources[0]))
+ },
+ },
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openGroupAttributes(resources[0].uuid))
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Remove',
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openRemoveGroupDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
-import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { AdvancedIcon, RemoveIcon, AttributesIcon } from 'components/icon/icon';
+import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
import { openGroupMemberAttributes, openRemoveGroupMemberDialog } from 'store/group-details-panel/group-details-panel-actions';
-export const groupMemberActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openGroupMemberAttributes(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openRemoveGroupMemberDialog(uuid));
- }
-}]];
+export const groupMemberActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openGroupMemberAttributes(resources[0].uuid));
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Remove',
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openRemoveGroupMemberDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
import { openKeepServiceAttributesDialog, openKeepServiceRemoveDialog } from 'store/keep-services/keep-services-actions';
import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { AdvancedIcon, RemoveIcon, AttributesIcon } from 'components/icon/icon';
-export const keepServiceActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openKeepServiceAttributesDialog(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openKeepServiceRemoveDialog(uuid));
- }
-}]];
+export const keepServiceActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openKeepServiceAttributesDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Remove',
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openKeepServiceRemoveDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
import { openLinkAttributesDialog, openLinkRemoveDialog } from 'store/link-panel/link-panel-actions';
import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { AdvancedIcon, RemoveIcon, AttributesIcon } from 'components/icon/icon';
-export const linkActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openLinkAttributesDialog(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openLinkRemoveDialog(uuid));
- }
-}]];
+export const linkActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openLinkAttributesDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Remove',
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openLinkRemoveDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { CanReadIcon, CanManageIcon, CanWriteIcon } from "components/icon/icon";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { CanReadIcon, CanManageIcon, CanWriteIcon } from 'components/icon/icon';
import { editPermissionLevel } from 'store/group-details-panel/group-details-panel-actions';
-import { PermissionLevel } from "models/permission";
+import { PermissionLevel } from 'models/permission';
-export const permissionEditActionSet: ContextMenuActionSet = [[{
- name: "Read",
- icon: CanReadIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(editPermissionLevel(uuid, PermissionLevel.CAN_READ));
- }
-}, {
- name: "Write",
- icon: CanWriteIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(editPermissionLevel(uuid, PermissionLevel.CAN_WRITE));
- }
-}, {
- name: "Manage",
- icon: CanManageIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(editPermissionLevel(uuid, PermissionLevel.CAN_MANAGE));
- }
-}]];
+export const permissionEditActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Read',
+ icon: CanReadIcon,
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) => dispatch<any>(editPermissionLevel(resource.uuid, PermissionLevel.CAN_READ)));
+ },
+ },
+ {
+ name: 'Write',
+ icon: CanWriteIcon,
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) => dispatch<any>(editPermissionLevel(resource.uuid, PermissionLevel.CAN_WRITE)));
+ },
+ },
+ {
+ name: 'Manage',
+ icon: CanManageIcon,
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) => dispatch<any>(editPermissionLevel(resource.uuid, PermissionLevel.CAN_MANAGE)));
+ },
+ },
+ ],
+];
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "store/favorites/favorites-actions";
import {
- RenameIcon, ShareIcon, MoveToIcon, DetailsIcon,
- RemoveIcon, ReRunProcessIcon, OutputIcon,
+ RenameIcon,
+ ShareIcon,
+ MoveToIcon,
+ DetailsIcon,
+ RemoveIcon,
+ ReRunProcessIcon,
+ OutputIcon,
AdvancedIcon,
- OpenIcon
+ OpenIcon,
+ StopIcon,
} from "components/icon/icon";
import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
-import { openMoveProcessDialog } from 'store/processes/process-move-actions';
+import { openMoveProcessDialog } from "store/processes/process-move-actions";
import { openProcessUpdateDialog } from "store/processes/process-update-actions";
-import { openCopyProcessDialog } from 'store/processes/process-copy-actions';
+import { openCopyProcessDialog } from "store/processes/process-copy-actions";
import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
import { openRemoveProcessDialog } from "store/processes/processes-actions";
-import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { toggleDetailsPanel } from "store/details-panel/details-panel-action";
import { navigateToOutput } from "store/process-panel/process-panel-actions";
import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
import { publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
import { openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+import { cancelRunningWorkflow } from "store/processes/processes-actions";
-export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
- {
- component: ToggleFavoriteAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleFavorite(resource)).then(() => {
- dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
- });
- }
- },
- {
- icon: OpenIcon,
- name: "Open in new tab",
- execute: (dispatch, resource) => {
- dispatch<any>(openInNewTabAction(resource));
- }
- },
- {
- icon: ReRunProcessIcon,
- name: "Copy and re-run process",
- execute: (dispatch, resource) => {
- dispatch<any>(openCopyProcessDialog(resource));
- }
- },
- {
- icon: OutputIcon,
- name: "Outputs",
- execute: (dispatch, resource) => {
- if(resource.outputUuid){
- dispatch<any>(navigateToOutput(resource.outputUuid));
- }
- }
- },
- {
- icon: DetailsIcon,
- name: "View details",
- execute: dispatch => {
- dispatch<any>(toggleDetailsPanel());
- }
- },
- {
- icon: AdvancedIcon,
- name: "API Details",
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
- },
-]];
+export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [
+ [
+ {
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resources) => {
+ dispatch<any>(toggleFavorite(resources[0])).then(() => {
+ dispatch<any>(favoritePanelActions.REQUEST_ITEMS());
+ });
+ },
+ },
+ {
+ icon: OpenIcon,
+ name: "Open in new tab",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openInNewTabAction(resources[0]));
+ },
+ },
+ {
+ icon: ReRunProcessIcon,
+ name: "Copy and re-run process",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openCopyProcessDialog(resources[0]));
+ },
+ },
+ {
+ icon: OutputIcon,
+ name: "Outputs",
+ execute: (dispatch, resources) => {
+ if (resources[0].outputUuid) {
+ dispatch<any>(navigateToOutput(resources[0].outputUuid));
+ }
+ },
+ },
+ {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: dispatch => {
+ dispatch<any>(toggleDetailsPanel());
+ },
+ },
+ {
+ icon: AdvancedIcon,
+ name: "API Details",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
-export const processResourceActionSet: ContextMenuActionSet = [[
- ...readOnlyProcessResourceActionSet.reduce((prev, next) => prev.concat(next), []),
- {
- icon: RenameIcon,
- name: "Edit process",
- execute: (dispatch, resource) => {
- dispatch<any>(openProcessUpdateDialog(resource));
- }
- },
- {
- icon: ShareIcon,
- name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
- },
- {
- icon: MoveToIcon,
- name: "Move to",
- execute: (dispatch, resource) => {
- dispatch<any>(openMoveProcessDialog(resource));
- }
- },
- {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, resource) => {
- dispatch<any>(openRemoveProcessDialog(resource.uuid));
- }
- }
-]];
+export const processResourceActionSet: ContextMenuActionSet = [
+ [
+ ...readOnlyProcessResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+ {
+ icon: RenameIcon,
+ name: "Edit process",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openProcessUpdateDialog(resources[0]));
+ },
+ },
+ {
+ icon: ShareIcon,
+ name: "Share",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openSharingDialog(resources[0].uuid));
+ },
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openMoveProcessDialog(resources[0]));
+ },
+ },
+ {
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openRemoveProcessDialog(resources[0], resources.length));
+ },
+ },
+ ],
+];
-export const processResourceAdminActionSet: ContextMenuActionSet = [[
- ...processResourceActionSet.reduce((prev, next) => prev.concat(next), []),
- {
- component: TogglePublicFavoriteAction,
- name: "Add to public favorites",
- execute: (dispatch, resource) => {
- dispatch<any>(togglePublicFavorite(resource)).then(() => {
- dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
- });
- }
- },
-]];
+const runningProcessOnlyActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: "CANCEL",
+ icon: StopIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(cancelRunningWorkflow(resources[0].uuid));
+ },
+ },
+ ]
+];
+
+export const processResourceAdminActionSet: ContextMenuActionSet = [
+ [
+ ...processResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+ {
+ component: TogglePublicFavoriteAction,
+ name: "Add to public favorites",
+ execute: (dispatch, resources) => {
+ dispatch<any>(togglePublicFavorite(resources[0])).then(() => {
+ dispatch<any>(publicFavoritePanelActions.REQUEST_ITEMS());
+ });
+ },
+ },
+ ],
+];
+
+export const runningProcessResourceActionSet = [
+ [
+ ...processResourceActionSet.reduce((prev, next) => prev.concat(next), []),
+ ...runningProcessOnlyActionSet.reduce((prev, next) => prev.concat(next), []),
+ ],
+];
+
+export const runningProcessResourceAdminActionSet: ContextMenuActionSet = [
+ [
+ ...processResourceAdminActionSet.reduce((prev, next) => prev.concat(next), []),
+ ...runningProcessOnlyActionSet.reduce((prev, next) => prev.concat(next), []),
+ ],
+];
// SPDX-License-Identifier: AGPL-3.0
import { ContextMenuActionSet } from "../context-menu-action-set";
-import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link, FolderSharedIcon } from 'components/icon/icon';
+import { NewProjectIcon, RenameIcon, MoveToIcon, DetailsIcon, AdvancedIcon, OpenIcon, Link, FolderSharedIcon } from "components/icon/icon";
import { ToggleFavoriteAction } from "../actions/favorite-action";
import { toggleFavorite } from "store/favorites/favorites-actions";
import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
-import { openMoveProjectDialog } from 'store/projects/project-move-actions';
-import { openProjectCreateDialog } from 'store/projects/project-create-actions';
-import { openProjectUpdateDialog } from 'store/projects/project-update-actions';
+import { openMoveProjectDialog } from "store/projects/project-move-actions";
+import { openProjectCreateDialog } from "store/projects/project-create-actions";
+import { openProjectUpdateDialog } from "store/projects/project-update-actions";
import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
import { toggleProjectTrashed } from "store/trash/trash-actions";
-import { ShareIcon } from 'components/icon/icon';
+import { ShareIcon } from "components/icon/icon";
import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
-import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { toggleDetailsPanel } from "store/details-panel/details-panel-action";
import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
import { openWebDavS3InfoDialog } from "store/collections/collection-info-actions";
import { ToggleLockAction } from "../actions/lock-action";
export const toggleFavoriteAction = {
component: ToggleFavoriteAction,
- name: 'ToggleFavoriteAction',
- execute: (dispatch, resource) => {
- dispatch(toggleFavorite(resource)).then(() => {
+ name: "ToggleFavoriteAction",
+ execute: (dispatch, resources) => {
+ dispatch(toggleFavorite(resources[0])).then(() => {
dispatch(favoritePanelActions.REQUEST_ITEMS());
});
- }
+ },
};
export const openInNewTabMenuAction = {
icon: OpenIcon,
name: "Open in new tab",
- execute: (dispatch, resource) => {
- dispatch(openInNewTabAction(resource));
- }
+ execute: (dispatch, resources) => {
+ dispatch(openInNewTabAction(resources[0]));
+ },
};
export const copyToClipboardMenuAction = {
icon: Link,
name: "Copy to clipboard",
- execute: (dispatch, resource) => {
- dispatch(copyToClipboardAction(resource));
- }
+ execute: (dispatch, resources) => {
+ dispatch(copyToClipboardAction(resources));
+ },
};
export const viewDetailsAction = {
name: "View details",
execute: dispatch => {
dispatch(toggleDetailsPanel());
- }
-}
+ },
+};
export const advancedAction = {
icon: AdvancedIcon,
name: "API Details",
- execute: (dispatch, resource) => {
- dispatch(openAdvancedTabDialog(resource.uuid));
- }
-}
+ execute: (dispatch, resources) => {
+ dispatch(openAdvancedTabDialog(resources[0].uuid));
+ },
+};
export const openWith3rdPartyClientAction = {
icon: FolderSharedIcon,
name: "Open with 3rd party client",
- execute: (dispatch, resource) => {
- dispatch(openWebDavS3InfoDialog(resource.uuid));
- }
-}
+ execute: (dispatch, resources) => {
+ dispatch(openWebDavS3InfoDialog(resources[0].uuid));
+ },
+};
export const editProjectAction = {
icon: RenameIcon,
name: "Edit project",
- execute: (dispatch, resource) => {
- dispatch(openProjectUpdateDialog(resource));
- }
-}
+ execute: (dispatch, resources) => {
+ dispatch(openProjectUpdateDialog(resources[0]));
+ },
+};
export const shareAction = {
icon: ShareIcon,
name: "Share",
- execute: (dispatch, { uuid }) => {
- dispatch(openSharingDialog(uuid));
- }
-}
+ execute: (dispatch, resources) => {
+ dispatch(openSharingDialog(resources[0].uuid));
+ },
+};
export const moveToAction = {
icon: MoveToIcon,
name: "Move to",
execute: (dispatch, resource) => {
- dispatch(openMoveProjectDialog(resource));
- }
-}
+ dispatch(openMoveProjectDialog(resource[0]));
+ },
+};
export const toggleTrashAction = {
component: ToggleTrashAction,
- name: 'ToggleTrashAction',
- execute: (dispatch, resource) => {
- dispatch(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!));
- }
-}
+ name: "ToggleTrashAction",
+ execute: (dispatch, resources) => {
+ dispatch(toggleProjectTrashed(resources[0].uuid, resources[0].ownerUuid, resources[0].isTrashed!!, resources.length > 1));
+ },
+};
export const freezeProjectAction = {
component: ToggleLockAction,
- name: 'ToggleLockAction',
- execute: (dispatch, resource) => {
- if (resource.isFrozen) {
- dispatch(unfreezeProject(resource.uuid));
+ name: "ToggleLockAction",
+ execute: (dispatch, resources) => {
+ if (resources[0].isFrozen) {
+ dispatch(unfreezeProject(resources[0].uuid));
} else {
- dispatch(freezeProject(resource.uuid));
+ dispatch(freezeProject(resources[0].uuid));
}
- }
-}
+ },
+};
export const newProjectAction: any = {
icon: NewProjectIcon,
name: "New project",
execute: (dispatch, resource): void => {
dispatch(openProjectCreateDialog(resource.uuid));
- }
-}
-
-export const readOnlyProjectActionSet: ContextMenuActionSet = [[
- toggleFavoriteAction,
- openInNewTabMenuAction,
- copyToClipboardMenuAction,
- viewDetailsAction,
- advancedAction,
- openWith3rdPartyClientAction,
-]];
-
-export const filterGroupActionSet: ContextMenuActionSet = [[
- toggleFavoriteAction,
- openInNewTabMenuAction,
- copyToClipboardMenuAction,
- viewDetailsAction,
- advancedAction,
- openWith3rdPartyClientAction,
- editProjectAction,
- shareAction,
- moveToAction,
- toggleTrashAction,
-]];
-
-export const frozenActionSet: ContextMenuActionSet = [[
- shareAction,
- toggleFavoriteAction,
- openInNewTabMenuAction,
- copyToClipboardMenuAction,
- viewDetailsAction,
- advancedAction,
- openWith3rdPartyClientAction,
- freezeProjectAction
-]];
-
-export const projectActionSet: ContextMenuActionSet = [[
- toggleFavoriteAction,
- openInNewTabMenuAction,
- copyToClipboardMenuAction,
- viewDetailsAction,
- advancedAction,
- openWith3rdPartyClientAction,
- editProjectAction,
- shareAction,
- moveToAction,
- toggleTrashAction,
- newProjectAction,
- freezeProjectAction,
-]];
+ },
+};
+
+export const readOnlyProjectActionSet: ContextMenuActionSet = [
+ [toggleFavoriteAction, openInNewTabMenuAction, copyToClipboardMenuAction, viewDetailsAction, advancedAction, openWith3rdPartyClientAction],
+];
+
+export const filterGroupActionSet: ContextMenuActionSet = [
+ [
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
+ ],
+];
+
+export const frozenActionSet: ContextMenuActionSet = [
+ [
+ shareAction,
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ freezeProjectAction,
+ ],
+];
+
+export const projectActionSet: ContextMenuActionSet = [
+ [
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
+ newProjectAction,
+ freezeProjectAction,
+ ],
+];
import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
import { publicFavoritePanelActions } from "store/public-favorites-panel/public-favorites-action";
-import { shareAction, toggleFavoriteAction, openInNewTabMenuAction, copyToClipboardMenuAction, viewDetailsAction, advancedAction, openWith3rdPartyClientAction, freezeProjectAction, editProjectAction, moveToAction, toggleTrashAction, newProjectAction } from "views-components/context-menu/action-sets/project-action-set";
-
-export const togglePublicFavoriteAction = {
- component: TogglePublicFavoriteAction,
- name: 'TogglePublicFavoriteAction',
- execute: (dispatch, resource) => {
- dispatch(togglePublicFavorite(resource)).then(() => {
- dispatch(publicFavoritePanelActions.REQUEST_ITEMS());
- });
-}}
-
-export const projectAdminActionSet: ContextMenuActionSet = [[
+import {
+ shareAction,
toggleFavoriteAction,
openInNewTabMenuAction,
copyToClipboardMenuAction,
viewDetailsAction,
advancedAction,
openWith3rdPartyClientAction,
+ freezeProjectAction,
editProjectAction,
- shareAction,
moveToAction,
toggleTrashAction,
newProjectAction,
- freezeProjectAction,
- togglePublicFavoriteAction
-]];
+} from "views-components/context-menu/action-sets/project-action-set";
-export const filterGroupAdminActionSet: ContextMenuActionSet = [[
- toggleFavoriteAction,
- openInNewTabMenuAction,
- copyToClipboardMenuAction,
- viewDetailsAction,
- advancedAction,
- openWith3rdPartyClientAction,
- editProjectAction,
- shareAction,
- moveToAction,
- toggleTrashAction,
- togglePublicFavoriteAction
-]];
+export const togglePublicFavoriteAction = {
+ component: TogglePublicFavoriteAction,
+ name: "TogglePublicFavoriteAction",
+ execute: (dispatch, resources) => {
+ dispatch(togglePublicFavorite(resources[0])).then(() => {
+ dispatch(publicFavoritePanelActions.REQUEST_ITEMS());
+ });
+ },
+};
+export const projectAdminActionSet: ContextMenuActionSet = [
+ [
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
+ newProjectAction,
+ freezeProjectAction,
+ togglePublicFavoriteAction,
+ ],
+];
-export const frozenAdminActionSet: ContextMenuActionSet = [[
- shareAction,
- togglePublicFavoriteAction,
- toggleFavoriteAction,
- openInNewTabMenuAction,
- copyToClipboardMenuAction,
- viewDetailsAction,
- advancedAction,
- openWith3rdPartyClientAction,
- freezeProjectAction
-]];
+export const filterGroupAdminActionSet: ContextMenuActionSet = [
+ [
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ editProjectAction,
+ shareAction,
+ moveToAction,
+ toggleTrashAction,
+ togglePublicFavoriteAction,
+ ],
+];
+
+export const frozenAdminActionSet: ContextMenuActionSet = [
+ [
+ shareAction,
+ togglePublicFavoriteAction,
+ toggleFavoriteAction,
+ openInNewTabMenuAction,
+ copyToClipboardMenuAction,
+ viewDetailsAction,
+ advancedAction,
+ openWith3rdPartyClientAction,
+ freezeProjectAction,
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, ShareIcon, AttributesIcon } from "components/icon/icon";
-import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
-import { openRepositoryAttributes, openRemoveRepositoryDialog } from "store/repositories/repositories-actions";
-import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { AdvancedIcon, RemoveIcon, ShareIcon, AttributesIcon } from 'components/icon/icon';
+import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
+import { openRepositoryAttributes, openRemoveRepositoryDialog } from 'store/repositories/repositories-actions';
+import { openSharingDialog } from 'store/sharing-dialog/sharing-dialog-actions';
-export const repositoryActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openRepositoryAttributes(uuid));
- }
-}, {
- name: "Share",
- icon: ShareIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSharingDialog(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openRemoveRepositoryDialog(uuid));
- }
-}]];
+export const repositoryActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openRepositoryAttributes(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Share',
+ icon: ShareIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openSharingDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Remove',
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openRemoveRepositoryDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleFavoriteAction } from "../actions/favorite-action";
-import { toggleFavorite } from "store/favorites/favorites-actions";
+import { ContextMenuActionSet } from '../context-menu-action-set';
+import { ToggleFavoriteAction } from '../actions/favorite-action';
+import { toggleFavorite } from 'store/favorites/favorites-actions';
-export const resourceActionSet: ContextMenuActionSet = [[{
- component: ToggleFavoriteAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleFavorite(resource));
- }
-}]];
+export const resourceActionSet: ContextMenuActionSet = [
+ [
+ {
+ component: ToggleFavoriteAction,
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) => dispatch<any>(toggleFavorite(resource)));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ContextMenuActionSet } from '../context-menu-action-set';
import { openCollectionCreateDialog } from 'store/collections/collection-create-actions';
-import { NewProjectIcon, CollectionIcon } from "components/icon/icon";
+import { NewProjectIcon, CollectionIcon } from 'components/icon/icon';
import { openProjectCreateDialog } from 'store/projects/project-create-actions';
-export const rootProjectActionSet: ContextMenuActionSet = [[
- {
- icon: NewProjectIcon,
- name: "New project",
- execute: (dispatch, resource) => {
- dispatch<any>(openProjectCreateDialog(resource.uuid));
- }
- },
- {
- icon: CollectionIcon,
- name: "New Collection",
- execute: (dispatch, resource) => {
- dispatch<any>(openCollectionCreateDialog(resource.uuid));
- }
- }
-]];
+export const rootProjectActionSet: ContextMenuActionSet = [
+ [
+ {
+ icon: NewProjectIcon,
+ name: 'New project',
+ execute: (dispatch, resources) => {
+ dispatch<any>(openProjectCreateDialog(resources[0].uuid));
+ },
+ },
+ {
+ icon: CollectionIcon,
+ name: 'New Collection',
+ execute: (dispatch, resources) => {
+ dispatch<any>(openCollectionCreateDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ContextMenuActionSet } from '../context-menu-action-set';
import { DetailsIcon, AdvancedIcon, OpenIcon, Link } from 'components/icon/icon';
-import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
+import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
-import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+import { copyToClipboardAction, openInNewTabAction } from 'store/open-in-new-tab/open-in-new-tab.actions';
export const searchResultsActionSet: ContextMenuActionSet = [
[
{
icon: OpenIcon,
- name: "Open in new tab",
- execute: (dispatch, resource) => {
- dispatch<any>(openInNewTabAction(resource));
- }
+ name: 'Open in new tab',
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) => dispatch<any>(openInNewTabAction(resource)));
+ },
},
{
icon: Link,
- name: "Copy to clipboard",
- execute: (dispatch, resource) => {
- dispatch<any>(copyToClipboardAction(resource));
- }
+ 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",
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
+ name: 'API Details',
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
},
- ]
+ ],
];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { AdvancedIcon, RemoveIcon, AttributesIcon } from 'components/icon/icon';
import { openSshKeyRemoveDialog, openSshKeyAttributesDialog } from 'store/auth/auth-action-ssh';
import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-export const sshKeyActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSshKeyAttributesDialog(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSshKeyRemoveDialog(uuid));
- }
-}]];
+export const sshKeyActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openSshKeyAttributesDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Remove',
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openSshKeyRemoveDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "../context-menu-action-set";
-import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
-import { toggleTrashed } from "store/trash/trash-actions";
+import { ContextMenuActionSet } from '../context-menu-action-set';
+import { ToggleTrashAction } from 'views-components/context-menu/actions/trash-action';
+import { toggleTrashed } from 'store/trash/trash-actions';
-export const trashActionSet: ContextMenuActionSet = [[
- {
- component: ToggleTrashAction,
- execute: (dispatch, resource) => {
- dispatch<any>(toggleTrashed(resource.kind, resource.uuid, resource.ownerUuid, resource.isTrashed!!));
- }
- },
-]];
+export const trashActionSet: ContextMenuActionSet = [
+ [
+ {
+ component: ToggleTrashAction,
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) => dispatch<any>(toggleTrashed(resource.kind, resource.uuid, resource.ownerUuid, resource.isTrashed!!)));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "../context-menu-action-set";
+import { ContextMenuActionSet } from '../context-menu-action-set';
import { DetailsIcon, ProvenanceGraphIcon, AdvancedIcon, RestoreFromTrashIcon } from 'components/icon/icon';
-import { toggleCollectionTrashed } from "store/trash/trash-actions";
-import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
+import { toggleCollectionTrashed } from 'store/trash/trash-actions';
+import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
-export const trashedCollectionActionSet: ContextMenuActionSet = [[
- {
- icon: DetailsIcon,
- name: "View details",
- execute: dispatch => {
- dispatch<any>(toggleDetailsPanel());
- }
- },
- {
- icon: ProvenanceGraphIcon,
- name: "Provenance graph",
- execute: (dispatch, resource) => {
- // add code
- }
- },
- {
- icon: AdvancedIcon,
- name: "API Details",
- execute: (dispatch, resource) => {
- dispatch<any>(openAdvancedTabDialog(resource.uuid));
- }
- },
- {
- icon: RestoreFromTrashIcon,
- name: "Restore",
- execute: (dispatch, resource) => {
- dispatch<any>(toggleCollectionTrashed(resource.uuid, true));
- }
- },
-]];
+export const trashedCollectionActionSet: ContextMenuActionSet = [
+ [
+ {
+ icon: DetailsIcon,
+ name: 'View details',
+ execute: (dispatch) => {
+ dispatch<any>(toggleDetailsPanel());
+ },
+ },
+ {
+ icon: ProvenanceGraphIcon,
+ name: 'Provenance graph',
+ execute: (dispatch, resource) => {
+ // add code
+ },
+ },
+ {
+ icon: AdvancedIcon,
+ name: 'API Details',
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ icon: RestoreFromTrashIcon,
+ name: 'Restore',
+ execute: (dispatch, resources) => {
+ resources.forEach((resource) => dispatch<any>(toggleCollectionTrashed(resource.uuid, true)));
+ },
+ },
+ ],
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
import {
AdvancedIcon,
ProjectIcon,
LoginAsIcon,
AdminMenuIcon,
ActiveIcon,
-} from "components/icon/icon";
+} from 'components/icon/icon';
import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { loginAs, openUserAttributes, openUserProjects } from "store/users/users-actions";
-import { openSetupDialog, openDeactivateDialog, openActivateDialog } from "store/user-profile/user-profile-actions";
-import { navigateToUserProfile } from "store/navigation/navigation-action";
-import { canActivateUser, canDeactivateUser, canSetupUser, isAdmin, needsUserProfileLink, isOtherUser } from "store/context-menu/context-menu-filters";
+import { loginAs, openUserAttributes, openUserProjects } from 'store/users/users-actions';
+import { openSetupDialog, openDeactivateDialog, openActivateDialog } from 'store/user-profile/user-profile-actions';
+import { navigateToUserProfile } from 'store/navigation/navigation-action';
+import {
+ canActivateUser,
+ canDeactivateUser,
+ canSetupUser,
+ isAdmin,
+ needsUserProfileLink,
+ isOtherUser,
+} from 'store/context-menu/context-menu-filters';
-export const userActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openUserAttributes(uuid));
- }
-}, {
- name: "Project",
- icon: ProjectIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openUserProjects(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
- }
-}, {
- name: "Account Settings",
- icon: UserPanelIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(navigateToUserProfile(uuid));
- },
- filters: [needsUserProfileLink]
-}],[{
- name: "Activate User",
- icon: ActiveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openActivateDialog(uuid));
- },
- filters: [
- isAdmin,
- canActivateUser,
- ],
-}, {
- name: "Setup User",
- icon: AdminMenuIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openSetupDialog(uuid));
- },
- filters: [
- isAdmin,
- canSetupUser,
- ],
-}, {
- name: "Deactivate User",
- icon: DeactivateUserIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openDeactivateDialog(uuid));
- },
- filters: [
- isAdmin,
- canDeactivateUser,
+export const userActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openUserAttributes(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Project',
+ icon: ProjectIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openUserProjects(resources[0].uuid));
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Account Settings',
+ icon: UserPanelIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(navigateToUserProfile(resources[0].uuid));
+ },
+ filters: [needsUserProfileLink],
+ },
],
-}, {
- name: "Login As User",
- icon: LoginAsIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(loginAs(uuid));
- },
- filters: [
- isAdmin,
- isOtherUser,
+ [
+ {
+ name: 'Activate User',
+ icon: ActiveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openActivateDialog(resources[0].uuid));
+ },
+ filters: [isAdmin, canActivateUser],
+ },
+ {
+ name: 'Setup User',
+ icon: AdminMenuIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openSetupDialog(resources[0].uuid));
+ },
+ filters: [isAdmin, canSetupUser],
+ },
+ {
+ name: 'Deactivate User',
+ icon: DeactivateUserIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openDeactivateDialog(resources[0].uuid));
+ },
+ filters: [isAdmin, canDeactivateUser],
+ },
+ {
+ name: 'Login As User',
+ icon: LoginAsIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(loginAs(resources[0].uuid));
+ },
+ filters: [isAdmin, isOtherUser],
+ },
],
-}]];
+];
//
// SPDX-License-Identifier: AGPL-3.0
-import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
-import { AdvancedIcon, RemoveIcon, AttributesIcon } from "components/icon/icon";
+import { ContextMenuActionSet } from 'views-components/context-menu/context-menu-action-set';
+import { AdvancedIcon, RemoveIcon, AttributesIcon } from 'components/icon/icon';
import { openAdvancedTabDialog } from 'store/advanced-tab/advanced-tab';
-import { openVirtualMachineAttributes, openRemoveVirtualMachineDialog } from "store/virtual-machines/virtual-machines-actions";
+import { openVirtualMachineAttributes, openRemoveVirtualMachineDialog } from 'store/virtual-machines/virtual-machines-actions';
-export const virtualMachineActionSet: ContextMenuActionSet = [[{
- name: "Attributes",
- icon: AttributesIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openVirtualMachineAttributes(uuid));
- }
-}, {
- name: "API Details",
- icon: AdvancedIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openAdvancedTabDialog(uuid));
- }
-}, {
- name: "Remove",
- icon: RemoveIcon,
- execute: (dispatch, { uuid }) => {
- dispatch<any>(openRemoveVirtualMachineDialog(uuid));
- }
-}]];
+export const virtualMachineActionSet: ContextMenuActionSet = [
+ [
+ {
+ name: 'Attributes',
+ icon: AttributesIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openVirtualMachineAttributes(resources[0].uuid));
+ },
+ },
+ {
+ name: 'API Details',
+ icon: AdvancedIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ name: 'Remove',
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openRemoveVirtualMachineDialog(resources[0].uuid));
+ },
+ },
+ ],
+];
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 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",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openInNewTabAction(resources[0]));
+ },
+ },
+ {
+ icon: Link,
+ name: "Copy to clipboard",
+ execute: (dispatch, resources) => {
+ dispatch<any>(copyToClipboardAction(resources));
+ },
+ },
+ {
+ icon: DetailsIcon,
+ name: "View details",
+ execute: dispatch => {
+ dispatch<any>(toggleDetailsPanel());
+ },
+ },
+ {
+ icon: AdvancedIcon,
+ name: "API Details",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openAdvancedTabDialog(resources[0].uuid));
+ },
+ },
+ {
+ icon: StartIcon,
+ 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) => {
+ dispatch<any>(deleteWorkflow(resources[0].uuid, resources[0].ownerUuid));
+ },
+ },
+ ],
+];
import { Dispatch } from "redux";
import { ContextMenuItem } from "components/context-menu/context-menu";
import { ContextMenuResource } from "store/context-menu/context-menu-actions";
-import { RootState } from "store/store";
export interface ContextMenuAction extends ContextMenuItem {
- execute(dispatch: Dispatch, resource: ContextMenuResource, state?: any): void;
+ execute(dispatch: Dispatch, resources: ContextMenuResource[], state?: any): void;
}
export type ContextMenuActionSet = Array<Array<ContextMenuAction>>;
import { createAnchorAt } from "components/popover/helpers";
import { ContextMenuActionSet, ContextMenuAction } from "./context-menu-action-set";
import { Dispatch } from "redux";
-import { memoize } from 'lodash';
+import { memoize } from "lodash";
import { sortByProperty } from "common/array-utils";
+
type DataProps = Pick<ContextMenuProps, "anchorEl" | "items" | "open"> & { resource?: ContextMenuResource };
+
const mapStateToProps = (state: RootState): DataProps => {
const { open, position, resource } = state.contextMenu;
- const filteredItems = getMenuActionSet(resource).map((group) => (group.filter((item) => {
- if (resource && item.filters) {
- // Execute all filters on this item, every returns true IFF all filters return true
- return item.filters.every((filter) => filter(state, resource));
- } else {
- return true;
- }
- })));
+ const filteredItems = getMenuActionSet(resource).map(group =>
+ group.filter(item => {
+ if (resource && item.filters) {
+ // Execute all filters on this item, every returns true IFF all filters return true
+ return item.filters.every(filter => filter(state, resource));
+ } else {
+ return true;
+ }
+ })
+ );
return {
anchorEl: resource ? createAnchorAt(position) : undefined,
items: filteredItems,
open,
- resource
+ resource,
};
};
onItemClick: (action: ContextMenuAction, resource?: ContextMenuResource) => {
dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
if (resource) {
- action.execute(dispatch, resource);
+ action.execute(dispatch, [resource]);
}
- }
+ },
});
const handleItemClick = memoize(
- (resource: DataProps['resource'], onItemClick: ActionProps['onItemClick']): ContextMenuProps['onItemClick'] =>
+ (resource: DataProps["resource"], onItemClick: ActionProps["onItemClick"]): ContextMenuProps["onItemClick"] =>
item => {
- onItemClick(item, resource);
+ onItemClick(item, { ...resource, fromContextMenu: true } as ContextMenuResource);
}
);
const mergeProps = ({ resource, ...dataProps }: DataProps, actionProps: ActionProps): ContextMenuProps => ({
...dataProps,
...actionProps,
- onItemClick: handleItemClick(resource, actionProps.onItemClick)
+ onItemClick: handleItemClick(resource, actionProps.onItemClick),
});
-
export const ContextMenu = connect(mapStateToProps, mapDispatchToProps, mergeProps)(ContextMenuComponent);
const menuActionSets = new Map<string, ContextMenuActionSet>();
export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) => {
- const sorted = itemSet.map(items => items.sort(sortByProperty('name')));
+ const sorted = itemSet.map(items => items.sort(sortByProperty("name")));
menuActionSets.set(name, sorted);
};
const emptyActionSet: ContextMenuActionSet = [];
-const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => (
- resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
-);
+const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet =>
+ resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet;
export enum ContextMenuKind {
API_CLIENT_AUTHORIZATION = "ApiClientAuthorization",
ROOT_PROJECT = "RootProject",
PROJECT = "Project",
FILTER_GROUP = "FilterGroup",
- READONLY_PROJECT = 'ReadOnlyProject',
- FROZEN_PROJECT = 'FrozenProject',
- FROZEN_PROJECT_ADMIN = 'FrozenProjectAdmin',
+ READONLY_PROJECT = "ReadOnlyProject",
+ FROZEN_PROJECT = "FrozenProject",
+ FROZEN_PROJECT_ADMIN = "FrozenProjectAdmin",
PROJECT_ADMIN = "ProjectAdmin",
FILTER_GROUP_ADMIN = "FilterGroupAdmin",
RESOURCE = "Resource",
COLLECTION_DIRECTORY_ITEM = "CollectionDirectoryItem",
READONLY_COLLECTION_FILE_ITEM = "ReadOnlyCollectionFileItem",
READONLY_COLLECTION_DIRECTORY_ITEM = "ReadOnlyCollectionDirectoryItem",
- 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',
+ RUNNING_PROCESS_ADMIN = "RunningProcessAdmin",
+ PROCESS_ADMIN = "ProcessAdmin",
+ RUNNING_PROCESS_RESOURCE = "RunningProcessResource",
+ PROCESS_RESOURCE = "ProcessResource",
+ READONLY_PROCESS_RESOURCE = "ReadOnlyProcessResource",
PROCESS_LOGS = "ProcessLogs",
REPOSITORY = "Repository",
SSH_KEY = "SshKey",
LINK = "Link",
WORKFLOW = "Workflow",
READONLY_WORKFLOW = "ReadOnlyWorkflow",
- SEARCH_RESULTS = "SearchResults"
+ SEARCH_RESULTS = "SearchResults",
}
import { Dispatch } from "redux";
import { dataExplorerActions } from "store/data-explorer/data-explorer-action";
import { DataColumn } from "components/data-table/data-column";
-import { DataColumns } from "components/data-table/data-table";
-import { DataTableFilters } from 'components/data-table-filters/data-table-filters-tree';
+import { DataColumns, TCheckedList } from "components/data-table/data-table";
+import { DataTableFilters } from "components/data-table-filters/data-table-filters-tree";
import { LAST_REFRESH_TIMESTAMP } from "components/refresh-button/refresh-button";
+import { toggleMSToolbar, setCheckedListOnStore } from "store/multiselect/multiselect-actions";
interface Props {
id: string;
const mapStateToProps = (state: RootState, { id }: Props) => {
const progress = state.progressIndicator.find(p => p.id === id);
const dataExplorerState = getDataExplorer(state.dataExplorer, id);
- const currentRoute = state.router.location ? state.router.location.pathname : '';
- const currentRefresh = localStorage.getItem(LAST_REFRESH_TIMESTAMP) || '';
- const currentItemUuid = currentRoute === '/workflows' ? state.properties.workflowPanelDetailsUuid : state.detailsPanel.resourceUuid;
+ const currentRoute = state.router.location ? state.router.location.pathname : "";
+ const currentRefresh = localStorage.getItem(LAST_REFRESH_TIMESTAMP) || "";
+ const currentItemUuid = currentRoute === "/workflows" ? state.properties.workflowPanelDetailsUuid : state.detailsPanel.resourceUuid;
+ const isMSToolbarVisible = state.multiselect.isVisible;
return {
...dataExplorerState,
working: !!progress?.working,
currentRoute: currentRoute,
paperKey: currentRoute,
currentItemUuid,
+ isMSToolbarVisible,
+ checkedList: state.multiselect.checkedList,
};
};
-const mapDispatchToProps = () => {
+const mapDispatchToProps = dispatchFn => {
return (dispatch: Dispatch, { id, onRowClick, onRowDoubleClick, onContextMenu }: Props) => ({
onSetColumns: (columns: DataColumns<any, any>) => {
dispatch(dataExplorerActions.SET_COLUMNS({ id, columns }));
dispatch(dataExplorerActions.SET_PAGE({ id, page }));
},
+ toggleMSToolbar: (isVisible: boolean) => {
+ dispatch<any>(toggleMSToolbar(isVisible));
+ },
+
+ setCheckedListOnStore: (checkedList: TCheckedList) => {
+ dispatch<any>(setCheckedListOnStore(checkedList));
+ },
+
onRowClick,
onRowDoubleClick,
// SPDX-License-Identifier: AGPL-3.0
import React from "react";
-import { memoize } from 'lodash/fp';
-import { InjectedFormProps, Field } from 'redux-form';
-import { WithDialogProps } from 'store/dialog/with-dialog';
-import { FormDialog } from 'components/form-dialog/form-dialog';
-import { ProjectTreePickerField } from 'views-components/projects-tree-picker/tree-picker-field';
-import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from 'validators/validators';
+import { memoize } from "lodash/fp";
+import { InjectedFormProps, Field } from "redux-form";
+import { WithDialogProps } from "store/dialog/with-dialog";
+import { FormDialog } from "components/form-dialog/form-dialog";
+import { ProjectTreePickerField } from "views-components/projects-tree-picker/tree-picker-field";
+import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from "validators/validators";
import { TextField } from "components/text-field/text-field";
-import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
-import { PickerIdProp } from 'store/tree-picker/picker-id';
+import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
+import { PickerIdProp } from "store/tree-picker/picker-id";
type CopyFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
-export const DialogCopy = (props: CopyFormDialogProps & PickerIdProp) =>
- <FormDialog
- dialogTitle='Make a copy'
- formFields={CopyDialogFields(props.pickerId)}
- submitLabel='Copy'
- {...props}
- />;
+export const DialogCopy = (props: CopyFormDialogProps & PickerIdProp) => {
+ return (
+ <FormDialog
+ dialogTitle="Make a copy"
+ formFields={CopyDialogFields(props.pickerId)}
+ submitLabel="Copy"
+ {...props}
+ />
+ );
+};
-const CopyDialogFields = memoize((pickerId: string) =>
- () =>
- <>
- <Field
- name='name'
- component={TextField as any}
- validate={COPY_NAME_VALIDATION}
- label="Enter a new name for the copy" />
- <Field
- name="ownerUuid"
- component={ProjectTreePickerField}
- validate={COPY_FILE_VALIDATION}
- pickerId={pickerId}/>
- </>);
+const CopyDialogFields = memoize((pickerId: string) => () => (
+ <>
+ <Field
+ name="name"
+ component={TextField as any}
+ validate={COPY_NAME_VALIDATION}
+ label="Enter a new name for the copy"
+ />
+ <Field
+ name="ownerUuid"
+ component={ProjectTreePickerField}
+ validate={COPY_FILE_VALIDATION}
+ pickerId={pickerId}
+ />
+ </>
+));
+
+export const DialogMultiCopy = (props: CopyFormDialogProps & PickerIdProp) => {
+ return (
+ <FormDialog
+ dialogTitle="Make Copies"
+ formFields={CopyMultiDialogFields(props.pickerId)}
+ submitLabel="Copy"
+ {...props}
+ />
+ );
+};
+
+const CopyMultiDialogFields = memoize((pickerId: string) => () => (
+ <Field
+ name="ownerUuid"
+ component={ProjectTreePickerField}
+ validate={COPY_FILE_VALIDATION}
+ pickerId={pickerId}
+ />
+));
//
// SPDX-License-Identifier: AGPL-3.0
-import React from "react";
+import React from 'react';
import { memoize } from 'lodash/fp';
import { InjectedFormProps, Field } from 'redux-form';
import { WithDialogProps } from 'store/dialog/with-dialog';
import { FormDialog } from 'components/form-dialog/form-dialog';
import { ProjectTreePickerField } from 'views-components/projects-tree-picker/tree-picker-field';
import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from 'validators/validators';
-import { TextField } from "components/text-field/text-field";
+import { TextField } from 'components/text-field/text-field';
import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
import { PickerIdProp } from 'store/tree-picker/picker-id';
type ProcessRerunFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
-export const DialogProcessRerun = (props: ProcessRerunFormDialogProps & PickerIdProp) =>
- <FormDialog
- dialogTitle='Choose location for re-run'
- formFields={CopyDialogFields(props.pickerId)}
- submitLabel='Copy'
- {...props}
- />;
+export const DialogProcessRerun = (props: ProcessRerunFormDialogProps & PickerIdProp) => (
+ <FormDialog dialogTitle='Choose location for re-run' formFields={CopyDialogFields(props.pickerId)} submitLabel='Copy' {...props} />
+);
-const CopyDialogFields = memoize((pickerId: string) =>
- () =>
- <>
- <Field
- name='name'
- component={TextField as any}
- validate={COPY_NAME_VALIDATION}
- label="Enter a new name for the copy" />
- <Field
- name="ownerUuid"
- component={ProjectTreePickerField}
- validate={COPY_FILE_VALIDATION}
- pickerId={pickerId}/>
- </>);
+const CopyDialogFields = memoize((pickerId: string) => () => (
+ <>
+ <Field name='name' component={TextField as any} validate={COPY_NAME_VALIDATION} label='Enter a new name for the copy' />
+ <Field name='ownerUuid' component={ProjectTreePickerField} validate={COPY_FILE_VALIDATION} pickerId={pickerId} />
+ </>
+));
import { compose } from "redux";
import { withDialog } from "store/dialog/with-dialog";
-import { reduxForm } from 'redux-form';
-import { COLLECTION_COPY_FORM_NAME } from 'store/collections/collection-copy-actions';
-import { DialogCopy } from "views-components/dialog-copy/dialog-copy";
-import { copyCollection } from 'store/workbench/workbench-actions';
-import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
-import { pickerId } from 'store/tree-picker/picker-id';
+import { reduxForm } from "redux-form";
+import { COLLECTION_COPY_FORM_NAME, COLLECTION_MULTI_COPY_FORM_NAME } from "store/collections/collection-copy-actions";
+import { DialogCopy, DialogMultiCopy } from "views-components/dialog-copy/dialog-copy";
+import { copyCollection } from "store/workbench/workbench-actions";
+import { CopyFormDialogData } from "store/copy-dialog/copy-dialog";
+import { pickerId } from "store/tree-picker/picker-id";
export const CopyCollectionDialog = compose(
withDialog(COLLECTION_COPY_FORM_NAME),
touchOnChange: true,
onSubmit: (data, dispatch) => {
dispatch(copyCollection(data));
- }
+ },
}),
- pickerId(COLLECTION_COPY_FORM_NAME),
-)(DialogCopy);
\ No newline at end of file
+ pickerId(COLLECTION_COPY_FORM_NAME)
+)(DialogCopy);
+
+export const CopyMultiCollectionDialog = compose(
+ withDialog(COLLECTION_MULTI_COPY_FORM_NAME),
+ reduxForm<CopyFormDialogData>({
+ form: COLLECTION_MULTI_COPY_FORM_NAME,
+ touchOnChange: true,
+ onSubmit: (data, dispatch) => {
+ dispatch(copyCollection(data));
+ },
+ }),
+ pickerId(COLLECTION_MULTI_COPY_FORM_NAME)
+)(DialogMultiCopy);
//
// SPDX-License-Identifier: AGPL-3.0
-import { compose } from "redux";
-import { withDialog } from "store/dialog/with-dialog";
+import { compose } from 'redux';
+import { withDialog } from 'store/dialog/with-dialog';
import { reduxForm } from 'redux-form';
import { PROCESS_COPY_FORM_NAME } from 'store/processes/process-copy-actions';
-import { DialogProcessRerun } from "views-components/dialog-copy/dialog-process-rerun";
+import { DialogProcessRerun } from 'views-components/dialog-copy/dialog-process-rerun';
import { copyProcess } from 'store/workbench/workbench-actions';
import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
-import { pickerId } from "store/tree-picker/picker-id";
+import { pickerId } from 'store/tree-picker/picker-id';
export const CopyProcessDialog = compose(
withDialog(PROCESS_COPY_FORM_NAME),
form: PROCESS_COPY_FORM_NAME,
onSubmit: (data, dispatch) => {
dispatch(copyProcess(data));
- }
+ },
}),
- pickerId(PROCESS_COPY_FORM_NAME),
+ pickerId(PROCESS_COPY_FORM_NAME)
)(DialogProcessRerun);
import { compose } from "redux";
import { withDialog } from "store/dialog/with-dialog";
-import { reduxForm } from 'redux-form';
-import { PROJECT_MOVE_FORM_NAME } from 'store/projects/project-move-actions';
-import { MoveToFormDialogData } from 'store/move-to-dialog/move-to-dialog';
-import { DialogMoveTo } from 'views-components/dialog-move/dialog-move-to';
-import { moveProject } from 'store/workbench/workbench-actions';
-import { pickerId } from 'store/tree-picker/picker-id';
+import { reduxForm } from "redux-form";
+import { PROJECT_MOVE_FORM_NAME } from "store/projects/project-move-actions";
+import { MoveToFormDialogData } from "store/move-to-dialog/move-to-dialog";
+import { DialogMoveTo } from "views-components/dialog-move/dialog-move-to";
+import { moveProject } from "store/workbench/workbench-actions";
+import { pickerId } from "store/tree-picker/picker-id";
export const MoveProjectDialog = compose(
withDialog(PROJECT_MOVE_FORM_NAME),
form: PROJECT_MOVE_FORM_NAME,
onSubmit: (data, dispatch) => {
dispatch(moveProject(data));
- }
+ },
}),
- pickerId(PROJECT_MOVE_FORM_NAME),
+ pickerId(PROJECT_MOVE_FORM_NAME)
)(DialogMoveTo);
-
<Field
name="destination"
pickerId={props.pickerId}
- component={DirectoryTreePickerField}
+ component={DirectoryTreePickerField as any}
validate={validateDirectory} />;
interface StorageClassesProps {
localCluster: state.auth.localCluster
});
-const wb1URL = (route: string) => {
- const r = route.replace(/^\//, "");
- if (r.match(/^(projects|collections)\//)) {
- return r;
- } else if (r.match(/^processes\//)) {
- return r.replace(/^processes/, "container_requests");
- }
- return "";
-};
-
type CssRules = 'link';
const styles: StyleRulesCallback<CssRules> = () => ({
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { MoveToIcon, CopyIcon } from "components/icon/icon";
+import { openMoveCollectionDialog } from "store/collections/collection-move-actions";
+import { openCollectionCopyDialog, openMultiCollectionCopyDialog } from "store/collections/collection-copy-actions";
+import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
+import { toggleCollectionTrashed } from "store/trash/trash-actions";
+import { ContextMenuResource } from "store/context-menu/context-menu-actions";
+
+export const msCollectionActionSet: ContextMenuActionSet = [
+ [
+ {
+ icon: CopyIcon,
+ name: "Make a copy",
+ execute: (dispatch, [...resources]) => {
+ if (resources[0].fromContextMenu || resources.length === 1) dispatch<any>(openCollectionCopyDialog(resources[0]));
+ else dispatch<any>(openMultiCollectionCopyDialog(resources[0]));
+ },
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resources) => dispatch<any>(openMoveCollectionDialog(resources[0])),
+ },
+ {
+ component: ToggleTrashAction,
+ name: "ToggleTrashAction",
+ execute: (dispatch, resources: ContextMenuResource[]) => {
+ for (const resource of [...resources]) {
+ dispatch<any>(toggleCollectionTrashed(resource.uuid, resource.isTrashed!!));
+ }
+ },
+ },
+ ],
+];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { MoveToIcon, RemoveIcon, ReRunProcessIcon } from "components/icon/icon";
+import { openMoveProcessDialog } from "store/processes/process-move-actions";
+import { openCopyProcessDialog } from "store/processes/process-copy-actions";
+import { openRemoveProcessDialog } from "store/processes/processes-actions";
+
+export const msProcessActionSet: ContextMenuActionSet = [
+ [
+ {
+ icon: ReRunProcessIcon,
+ name: "Copy and re-run process",
+ execute: (dispatch, resources) => {
+ for (const resource of [...resources]) {
+ dispatch<any>(openCopyProcessDialog(resource));
+ }
+ },
+ },
+ {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resources) => {
+ dispatch<any>(openMoveProcessDialog(resources[0]));
+ },
+ },
+ {
+ name: "Remove",
+ icon: RemoveIcon,
+ execute: (dispatch, resources) => {
+ dispatch<any>(openRemoveProcessDialog(resources[0], resources.length));
+ },
+ },
+ ],
+];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { MoveToIcon, Link } from "components/icon/icon";
+import { openMoveProjectDialog } from "store/projects/project-move-actions";
+import { ToggleTrashAction } from "views-components/context-menu/actions/trash-action";
+import { toggleProjectTrashed } from "store/trash/trash-actions";
+import { copyToClipboardAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+
+export const msCopyToClipboardMenuAction = {
+ icon: Link,
+ name: "Copy to clipboard",
+ execute: (dispatch, resources) => {
+ dispatch(copyToClipboardAction(resources));
+ },
+};
+
+export const msMoveToAction = {
+ icon: MoveToIcon,
+ name: "Move to",
+ execute: (dispatch, resource) => {
+ dispatch(openMoveProjectDialog(resource[0]));
+ },
+};
+
+export const msToggleTrashAction = {
+ component: ToggleTrashAction,
+ name: "ToggleTrashAction",
+ execute: (dispatch, resources) => {
+ for (const resource of [...resources]) {
+ dispatch(toggleProjectTrashed(resource.uuid, resource.ownerUuid, resource.isTrashed!!, resources.length > 1));
+ }
+ },
+};
+
+export const msProjectActionSet: ContextMenuActionSet = [[msCopyToClipboardMenuAction, msMoveToAction, msToggleTrashAction]];
type PickedTreePickerProps = Pick<TreePickerProps<ProjectsTreePickerItem>, 'onContextMenu' | 'toggleItemActive' | 'toggleItemOpen' | 'toggleItemSelection'>;
export interface ProjectsTreePickerDataProps {
+ cascadeSelection: boolean;
includeCollections?: boolean;
includeDirectories?: boolean;
includeFiles?: boolean;
export type ProjectsTreePickerProps = ProjectsTreePickerDataProps & Partial<PickedTreePickerProps>;
-const mapStateToProps = (_: any, { rootItemIcon, showSelection }: ProjectsTreePickerProps) => ({
+const mapStateToProps = (_: any, { rootItemIcon, showSelection, cascadeSelection }: ProjectsTreePickerProps) => ({
render: renderTreeItem(rootItemIcon),
- showSelection: isSelectionVisible(showSelection),
+ showSelection: isSelectionVisible(showSelection, cascadeSelection),
});
const mapDispatchToProps = (dispatch: Dispatch, { loadRootItem, includeCollections, includeDirectories, includeFiles, relatedTreePickers, options, ...props }: ProjectsTreePickerProps): PickedTreePickerProps => ({
}
},
toggleItemSelection: (event, item, pickerId) => {
- dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: item.id, pickerId }));
+ dispatch<any>(treePickerActions.TOGGLE_TREE_PICKER_NODE_SELECTION({ id: item.id, pickerId, cascade: props.cascadeSelection }));
if (props.toggleItemSelection) {
props.toggleItemSelection(event, item, pickerId);
}
}
};
-const isSelectionVisible = (shouldBeVisible?: boolean) =>
- ({ status, items }: TreeItem<ProjectsTreePickerItem>): boolean => {
+const isSelectionVisible = (shouldBeVisible: boolean | undefined, cascadeSelection: boolean) =>
+ ({ status, items, data }: TreeItem<ProjectsTreePickerItem>): boolean => {
if (shouldBeVisible) {
- if (items && items.length > 0) {
- return items.every(isSelectionVisible(shouldBeVisible));
+ if (!cascadeSelection && 'kind' in data && data.kind === ResourceKind.COLLECTION) {
+ // In non-casecade mode collections are selectable without being loaded
+ return true;
+ } else if (items && items.length > 0) {
+ return items.every(isSelectionVisible(shouldBeVisible, cascadeSelection));
}
return status === TreeItemStatus.LOADED;
}
import { ArvadosTheme } from 'common/custom-theme';
export interface ToplevelPickerProps {
- currentUuid?: string;
+ currentUuids?: string[];
pickerId: string;
+ cascadeSelection: boolean;
includeCollections?: boolean;
includeDirectories?: boolean;
includeFiles?: boolean;
componentDidMount() {
const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(this.props.pickerId);
- this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId, this.props.currentUuid));
+ const preloadParams = this.props.currentUuids ? {
+ selectedItemUuids: this.props.currentUuids,
+ includeDirectories: !!this.props.includeDirectories,
+ includeFiles: !!this.props.includeFiles,
+ multi: !!this.props.showSelection,
+ } : undefined;
+ this.props.dispatch<any>(initProjectsTreePicker(this.props.pickerId, preloadParams));
this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_PROJECT_SEARCH({ pickerId: search, projectSearchValue: "" }));
this.props.dispatch(treePickerSearchActions.SET_TREE_PICKER_COLLECTION_FILTER({ pickerId: search, collectionFilterValue: "" }));
const { home, shared, favorites, publicFavorites, search } = getProjectsTreePickerIds(pickerId);
const relatedTreePickers = getRelatedTreePickers(pickerId);
const p = {
+ cascadeSelection: this.props.cascadeSelection,
includeCollections: this.props.includeCollections,
includeDirectories: this.props.includeDirectories,
includeFiles: this.props.includeFiles,
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { PickerIdProp } from 'store/tree-picker/picker-id';
-import { getFileOperationLocation } from "store/tree-picker/tree-picker-actions";
+import { FileOperationLocation, getFileOperationLocation } from "store/tree-picker/tree-picker-actions";
+import { connect } from "react-redux";
+import { Dispatch } from "redux";
export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
<div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
+ cascadeSelection={false}
options={{ showOnlyOwned: false, showOnlyWritable: true }} />
{props.meta.dirty && props.meta.error &&
<Typography variant='caption' color='error'>
<ProjectsTreePicker
pickerId={props.pickerId}
toggleItemActive={handleChange(props)}
+ cascadeSelection={false}
options={{ showOnlyOwned: false, showOnlyWritable: true }}
includeCollections />
{props.meta.dirty && props.meta.error &&
</div>
</div>;
-const handleDirectoryChange = (props: WrappedFieldProps) =>
- (_: any, { data }: TreeItem<ProjectsTreePickerItem>) => {
- props.input.onChange(getFileOperationLocation(data) || '');
- }
+type ProjectsTreePickerActionProps = {
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
-export const DirectoryTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
- <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
- <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
- <ProjectsTreePicker
- currentUuid={props.input.value.uuid}
- pickerId={props.pickerId}
- toggleItemActive={handleDirectoryChange(props)}
- options={{ showOnlyOwned: false, showOnlyWritable: true }}
- includeCollections
- includeDirectories />
- {props.meta.dirty && props.meta.error &&
- <Typography variant='caption' color='error'>
- {props.meta.error}
- </Typography>}
- </div>
- </div>;
+const projectsTreePickerMapDispatchToProps = (dispatch: Dispatch): ProjectsTreePickerActionProps => ({
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+type ProjectsTreePickerCombinedProps = ProjectsTreePickerActionProps & WrappedFieldProps & PickerIdProp;
+
+export const DirectoryTreePickerField = connect(null, projectsTreePickerMapDispatchToProps)(
+ class DirectoryTreePickerFieldComponent extends React.Component<ProjectsTreePickerCombinedProps> {
+
+ handleDirectoryChange = (props: WrappedFieldProps) =>
+ async (_: any, { data }: TreeItem<ProjectsTreePickerItem>) => {
+ const location = await this.props.getFileOperationLocation(data);
+ props.input.onChange(location || '');
+ }
+
+ render() {
+ return <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
+ <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+ <ProjectsTreePicker
+ currentUuids={[this.props.input.value.uuid]}
+ pickerId={this.props.pickerId}
+ toggleItemActive={this.handleDirectoryChange(this.props)}
+ cascadeSelection={false}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }}
+ includeCollections
+ includeDirectories />
+ {this.props.meta.dirty && this.props.meta.error &&
+ <Typography variant='caption' color='error'>
+ {this.props.meta.error}
+ </Typography>}
+ </div>
+ </div>;
+ }
+ });
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { compose } from 'redux';
-import {
- IconButton,
- Paper,
- StyleRulesCallback,
- withStyles,
- WithStyles,
- Tooltip,
- InputAdornment, Input,
-} from '@material-ui/core';
-import SearchIcon from '@material-ui/icons/Search';
-import ArrowDropDownIcon from '@material-ui/icons/ArrowDropDown';
-import { ArvadosTheme } from 'common/custom-theme';
-import { SearchView } from 'store/search-bar/search-bar-reducer';
-import {
- SearchBarBasicView,
- SearchBarBasicViewDataProps,
- SearchBarBasicViewActionProps
-} from 'views-components/search-bar/search-bar-basic-view';
+import React from "react";
+import { compose } from "redux";
+import { IconButton, Paper, StyleRulesCallback, withStyles, WithStyles, Tooltip, InputAdornment, Input } from "@material-ui/core";
+import SearchIcon from "@material-ui/icons/Search";
+import ArrowDropDownIcon from "@material-ui/icons/ArrowDropDown";
+import { ArvadosTheme } from "common/custom-theme";
+import { SearchView } from "store/search-bar/search-bar-reducer";
+import { SearchBarBasicView, SearchBarBasicViewDataProps, SearchBarBasicViewActionProps } from "views-components/search-bar/search-bar-basic-view";
import {
SearchBarAutocompleteView,
SearchBarAutocompleteViewDataProps,
- SearchBarAutocompleteViewActionProps
-} from 'views-components/search-bar/search-bar-autocomplete-view';
+ SearchBarAutocompleteViewActionProps,
+} from "views-components/search-bar/search-bar-autocomplete-view";
import {
SearchBarAdvancedView,
SearchBarAdvancedViewDataProps,
- SearchBarAdvancedViewActionProps
-} from 'views-components/search-bar/search-bar-advanced-view';
+ SearchBarAdvancedViewActionProps,
+} from "views-components/search-bar/search-bar-advanced-view";
import { KEY_CODE_DOWN, KEY_CODE_ESC, KEY_CODE_UP, KEY_ENTER } from "common/codes";
-import { debounce } from 'debounce';
-import { Vocabulary } from 'models/vocabulary';
-import { connectVocabulary } from '../resource-properties-form/property-field-common';
+import { debounce } from "debounce";
+import { Vocabulary } from "models/vocabulary";
+import { connectVocabulary } from "../resource-properties-form/property-field-common";
-type CssRules = 'container' | 'containerSearchViewOpened' | 'input' | 'view';
+type CssRules = "container" | "containerSearchViewOpened" | "input" | "view";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => {
return {
container: {
- position: 'relative',
- width: '100%',
+ position: "relative",
+ width: "100%",
borderRadius: theme.spacing.unit / 2,
zIndex: theme.zIndex.modal,
},
containerSearchViewOpened: {
- position: 'relative',
- width: '100%',
+ position: "relative",
+ width: "100%",
borderRadius: `${theme.spacing.unit / 2}px ${theme.spacing.unit / 2}px 0 0`,
zIndex: theme.zIndex.modal,
},
input: {
- border: 'none',
- padding: `0`
+ border: "none",
+ padding: `0`,
},
view: {
- position: 'absolute',
- width: '100%',
- zIndex: 1
- }
+ position: "absolute",
+ width: "100%",
+ zIndex: 1,
+ },
};
};
-export type SearchBarDataProps = SearchBarViewDataProps
- & SearchBarAutocompleteViewDataProps
- & SearchBarAdvancedViewDataProps
- & SearchBarBasicViewDataProps;
+export type SearchBarDataProps = SearchBarViewDataProps &
+ SearchBarAutocompleteViewDataProps &
+ SearchBarAdvancedViewDataProps &
+ SearchBarBasicViewDataProps;
interface SearchBarViewDataProps {
searchValue: string;
vocabulary?: Vocabulary;
}
-export type SearchBarActionProps = SearchBarViewActionProps
- & SearchBarAutocompleteViewActionProps
- & SearchBarAdvancedViewActionProps
- & SearchBarBasicViewActionProps;
+export type SearchBarActionProps = SearchBarViewActionProps &
+ SearchBarAutocompleteViewActionProps &
+ SearchBarAdvancedViewActionProps &
+ SearchBarBasicViewActionProps;
interface SearchBarViewActionProps {
onChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
}
};
-export const SearchBarView = compose(connectVocabulary, withStyles(styles))(
+export const SearchBarView = compose(
+ connectVocabulary,
+ withStyles(styles)
+)(
class extends React.Component<SearchBarViewProps> {
-
debouncedSearch = debounce(() => {
this.props.onSearch(this.props.searchValue);
}, 1000);
handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
this.debouncedSearch();
this.props.onChange(event);
- }
+ };
handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
this.debouncedSearch.clear();
this.props.onSubmit(event);
- }
+ };
componentWillUnmount() {
this.debouncedSearch.clear();
const { classes, isPopoverOpen } = this.props;
return (
<>
+ {isPopoverOpen && <Backdrop onClick={props.closeView} />}
- {isPopoverOpen &&
- <Backdrop onClick={props.closeView} />}
-
- <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container} >
- <form onSubmit={this.handleSubmit}>
+ <Paper className={isPopoverOpen ? classes.containerSearchViewOpened : classes.container}>
+ <form
+ data-cy="searchbar-parent-form"
+ onSubmit={this.handleSubmit}>
<Input
- data-cy='searchbar-input-field'
+ data-cy="searchbar-input-field"
className={classes.input}
onChange={this.handleChange}
placeholder="Search"
onKeyDown={e => handleKeyDown(e, props)}
startAdornment={
<InputAdornment position="start">
- <Tooltip title='Search'>
+ <Tooltip title="Search">
<IconButton type="submit">
<SearchIcon />
</IconButton>
}
endAdornment={
<InputAdornment position="end">
- <Tooltip title='Advanced search'>
+ <Tooltip title="Advanced search">
<IconButton onClick={e => handleDropdownClick(e, props)}>
<ArrowDropDownIcon />
</IconButton>
</Tooltip>
</InputAdornment>
- } />
+ }
+ />
</form>
- <div className={classes.view}>
- {isPopoverOpen && getView({ ...props })}
- </div>
- </Paper >
+ <div className={classes.view}>{isPopoverOpen && getView({ ...props })}</div>
+ </Paper>
</>
);
}
- });
+ }
+);
const getView = (props: SearchBarViewProps) => {
switch (props.currentView) {
case SearchView.AUTOCOMPLETE:
- return <SearchBarAutocompleteView
- navigateTo={props.navigateTo}
- searchResults={props.searchResults}
- searchValue={props.searchValue}
- selectedItem={props.selectedItem} />;
+ return (
+ <SearchBarAutocompleteView
+ navigateTo={props.navigateTo}
+ searchResults={props.searchResults}
+ searchValue={props.searchValue}
+ selectedItem={props.selectedItem}
+ />
+ );
case SearchView.ADVANCED:
- return <SearchBarAdvancedView
- closeAdvanceView={props.closeAdvanceView}
- tags={props.tags}
- saveQuery={props.saveQuery} />;
+ return (
+ <SearchBarAdvancedView
+ closeAdvanceView={props.closeAdvanceView}
+ tags={props.tags}
+ saveQuery={props.saveQuery}
+ />
+ );
default:
- return <SearchBarBasicView
- onSetView={props.onSetView}
- onSearch={props.onSearch}
- loadRecentQueries={props.loadRecentQueries}
- savedQueries={props.savedQueries}
- deleteSavedQuery={props.deleteSavedQuery}
- editSavedQuery={props.editSavedQuery}
- selectedItem={props.selectedItem} />;
+ return (
+ <SearchBarBasicView
+ onSetView={props.onSetView}
+ onSearch={props.onSearch}
+ loadRecentQueries={props.loadRecentQueries}
+ savedQueries={props.savedQueries}
+ deleteSavedQuery={props.deleteSavedQuery}
+ editSavedQuery={props.editSavedQuery}
+ selectedItem={props.selectedItem}
+ />
+ );
}
};
-const Backdrop = withStyles<'backdrop'>(theme => ({
+const Backdrop = withStyles<"backdrop">(theme => ({
backdrop: {
- position: 'fixed',
+ position: "fixed",
top: 0,
right: 0,
bottom: 0,
left: 0,
- zIndex: theme.zIndex.modal
- }
-}))(
- ({ classes, ...props }: WithStyles<'backdrop'> & React.HTMLProps<HTMLDivElement>) =>
- <div className={classes.backdrop} {...props} />);
+ zIndex: theme.zIndex.modal,
+ },
+}))(({ classes, ...props }: WithStyles<"backdrop"> & React.HTMLProps<HTMLDivElement>) => (
+ <div
+ className={classes.backdrop}
+ {...props}
+ />
+));
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
+import React from "react";
+import { StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
import { DataExplorer } from "views-components/data-explorer/data-explorer";
-import { connect, DispatchProp } from 'react-redux';
-import { DataColumns } from 'components/data-table/data-table';
-import { RouteComponentProps } from 'react-router';
-import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
-import { SortDirection } from 'components/data-table/data-column';
-import { ResourceKind } from 'models/resource';
-import { ArvadosTheme } from 'common/custom-theme';
-import { ALL_PROCESSES_PANEL_ID } from 'store/all-processes-panel/all-processes-panel-action';
+import { connect, DispatchProp } from "react-redux";
+import { DataColumns } from "components/data-table/data-table";
+import { RouteComponentProps } from "react-router";
+import { DataTableFilterItem } from "components/data-table-filters/data-table-filters";
+import { SortDirection } from "components/data-table/data-column";
+import { ResourceKind } from "models/resource";
+import { ArvadosTheme } from "common/custom-theme";
+import { ALL_PROCESSES_PANEL_ID } from "store/all-processes-panel/all-processes-panel-action";
import {
ProcessStatus,
ResourceName,
ResourceOwnerWithName,
ResourceType,
ContainerRunTime,
- ResourceCreatedAtDate
-} from 'views-components/data-explorer/renderers';
-import { ProcessIcon } from 'components/icon/icon';
-import { openProcessContextMenu } from 'store/context-menu/context-menu-actions';
-import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
-import { navigateTo } from 'store/navigation/navigation-action';
+ ResourceCreatedAtDate,
+} from "views-components/data-explorer/renderers";
+import { ProcessIcon } from "components/icon/icon";
+import { openProcessContextMenu } from "store/context-menu/context-menu-actions";
+import { loadDetailsPanel } from "store/details-panel/details-panel-action";
+import { navigateTo } from "store/navigation/navigation-action";
import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
-import { RootState } from 'store/store';
-import { createTree } from 'models/tree';
-import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from 'store/resource-type-filters/resource-type-filters';
-import { getProcess } from 'store/processes/process';
-import { ResourcesState } from 'store/resources/resources';
+import { RootState } from "store/store";
+import { createTree } from "models/tree";
+import { getInitialProcessStatusFilters, getInitialProcessTypeFilters } from "store/resource-type-filters/resource-type-filters";
+import { getProcess } from "store/processes/process";
+import { ResourcesState } from "store/resources/resources";
type CssRules = "toolbar" | "button" | "root";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
toolbar: {
paddingBottom: theme.spacing.unit * 3,
- textAlign: "right"
+ textAlign: "right",
},
button: {
- marginLeft: theme.spacing.unit
+ marginLeft: theme.spacing.unit,
},
root: {
- width: '100%',
- }
+ width: "100%",
+ },
});
export enum AllProcessesPanelColumnNames {
TYPE = "Type",
OWNER = "Owner",
CREATED_AT = "Created at",
- RUNTIME = "Run Time"
+ RUNTIME = "Run Time",
}
export interface AllProcessesPanelFilter extends DataTableFilterItem {
name: AllProcessesPanelColumnNames.NAME,
selected: true,
configurable: true,
- sort: {direction: SortDirection.NONE, field: "name"},
+ sort: { direction: SortDirection.NONE, field: "name" },
filters: createTree(),
- render: uuid => <ResourceName uuid={uuid} />
+ render: uuid => <ResourceName uuid={uuid} />,
},
{
name: AllProcessesPanelColumnNames.STATUS,
configurable: true,
mutuallyExclusiveFilters: true,
filters: getInitialProcessStatusFilters(),
- render: uuid => <ProcessStatus uuid={uuid} />
+ render: uuid => <ProcessStatus uuid={uuid} />,
},
{
name: AllProcessesPanelColumnNames.TYPE,
selected: true,
configurable: true,
filters: getInitialProcessTypeFilters(),
- render: uuid => <ResourceType uuid={uuid} />
+ render: uuid => <ResourceType uuid={uuid} />,
},
{
name: AllProcessesPanelColumnNames.OWNER,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceOwnerWithName uuid={uuid} />
+ render: uuid => <ResourceOwnerWithName uuid={uuid} />,
},
{
name: AllProcessesPanelColumnNames.CREATED_AT,
selected: true,
configurable: true,
- sort: {direction: SortDirection.DESC, field: "createdAt"},
+ sort: { direction: SortDirection.DESC, field: "createdAt" },
filters: createTree(),
- render: uuid => <ResourceCreatedAtDate uuid={uuid} />
+ render: uuid => <ResourceCreatedAtDate uuid={uuid} />,
},
{
name: AllProcessesPanelColumnNames.RUNTIME,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ContainerRunTime uuid={uuid} />
- }
+ render: uuid => <ContainerRunTime uuid={uuid} />,
+ },
];
interface AllProcessesPanelDataProps {
onDialogOpen: (ownerUuid: string) => void;
onItemDoubleClick: (item: string) => void;
}
-const mapStateToProps = (state : RootState): AllProcessesPanelDataProps => ({
- resources: state.resources
+const mapStateToProps = (state: RootState): AllProcessesPanelDataProps => ({
+ resources: state.resources,
});
-type AllProcessesPanelProps = AllProcessesPanelDataProps & AllProcessesPanelActionProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
+type AllProcessesPanelProps = AllProcessesPanelDataProps &
+ AllProcessesPanelActionProps &
+ DispatchProp &
+ WithStyles<CssRules> &
+ RouteComponentProps<{ id: string }>;
export const AllProcessesPanel = withStyles(styles)(
connect(mapStateToProps)(
this.props.dispatch<any>(openProcessContextMenu(event, process));
}
this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
- }
+ };
handleRowDoubleClick = (uuid: string) => {
this.props.dispatch<any>(navigateTo(uuid));
- }
+ };
handleRowClick = (uuid: string) => {
this.props.dispatch<any>(loadDetailsPanel(uuid));
- }
+ };
render() {
- return <div className={this.props.classes.root}><DataExplorer
- id={ALL_PROCESSES_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
- onContextMenu={this.handleContextMenu}
- contextMenuColumn={true}
- defaultViewIcon={ProcessIcon}
- defaultViewMessages={['Processes list empty.']} />
- </div>
+ return (
+ <div className={this.props.classes.root}>
+ <DataExplorer
+ id={ALL_PROCESSES_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={true}
+ defaultViewIcon={ProcessIcon}
+ defaultViewMessages={["Processes list empty."]}
+ />
+ </div>
+ );
}
}
)
//
// SPDX-License-Identifier: AGPL-3.0
-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,
Grid,
Chip,
CircularProgress,
-} from '@material-ui/core';
-import { ArvadosTheme } from 'common/custom-theme';
-import {
- CloseIcon,
- ImageIcon,
- InputIcon,
- ImageOffIcon,
- OutputIcon,
- MaximizeIcon,
- UnMaximizeIcon,
- InfoIcon
-} from 'components/icon/icon';
-import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+} from "@material-ui/core";
+import { ArvadosTheme } from "common/custom-theme";
+import { CloseIcon, ImageIcon, InputIcon, ImageOffIcon, OutputIcon, MaximizeIcon, UnMaximizeIcon, InfoIcon } from "components/icon/icon";
+import { MPVPanelProps } from "components/multi-panel-view/multi-panel-view";
import {
BooleanCommandInputParameter,
CommandInputParameter,
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"
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
card: {
- height: '100%'
+ height: "100%",
},
header: {
paddingTop: theme.spacing.unit,
paddingBottom: 0,
},
iconHeader: {
- fontSize: '1.875rem',
+ fontSize: "1.875rem",
color: theme.customs.colors.greyL,
},
avatar: {
- alignSelf: 'flex-start',
- paddingTop: theme.spacing.unit * 0.5
+ alignSelf: "flex-start",
+ paddingTop: theme.spacing.unit * 0.5,
},
content: {
height: `calc(100% - ${theme.spacing.unit * 7}px - ${theme.spacing.unit * 1.5}px)`,
padding: theme.spacing.unit * 1.0,
paddingTop: 0,
- '&:last-child': {
+ "&:last-child": {
paddingBottom: theme.spacing.unit * 1,
- }
+ },
},
title: {
- overflow: 'hidden',
+ overflow: "hidden",
paddingTop: theme.spacing.unit * 0.5,
color: theme.customs.colors.greyD,
- fontSize: '1.875rem'
+ fontSize: "1.875rem",
},
tableWrapper: {
- height: 'auto',
+ height: "auto",
maxHeight: `calc(100% - ${theme.spacing.unit * 4.5}px)`,
- overflow: 'auto',
+ overflow: "auto",
},
tableRoot: {
- width: '100%',
- '& thead th': {
- verticalAlign: 'bottom',
- paddingBottom: '10px',
+ width: "100%",
+ "& thead th": {
+ verticalAlign: "bottom",
+ paddingBottom: "10px",
+ },
+ "& td, & th": {
+ paddingRight: "25px",
},
- '& td, & th': {
- paddingRight: '25px',
- }
},
paramValue: {
- display: 'flex',
- alignItems: 'flex-start',
- flexDirection: 'column',
+ display: "flex",
+ alignItems: "flex-start",
+ flexDirection: "column",
},
keepLink: {
color: theme.palette.primary.main,
- textDecoration: 'none',
- overflowWrap: 'break-word',
- cursor: 'pointer',
+ textDecoration: "none",
+ overflowWrap: "break-word",
+ cursor: "pointer",
},
collectionLink: {
- margin: '10px',
- '& a': {
+ margin: "10px",
+ "& a": {
color: theme.palette.primary.main,
- textDecoration: 'none',
- overflowWrap: 'break-word',
- cursor: 'pointer',
- }
+ textDecoration: "none",
+ overflowWrap: "break-word",
+ cursor: "pointer",
+ },
},
imagePreview: {
- maxHeight: '15em',
- maxWidth: '15em',
+ maxHeight: "15em",
+ maxWidth: "15em",
marginBottom: theme.spacing.unit,
},
valArray: {
- display: 'flex',
- gap: '10px',
- flexWrap: 'wrap',
- '& span': {
- display: 'inline',
- }
+ display: "flex",
+ gap: "10px",
+ flexWrap: "wrap",
+ "& span": {
+ display: "inline",
+ },
},
secondaryVal: {
- paddingLeft: '20px',
+ paddingLeft: "20px",
},
secondaryRow: {
- height: '29px',
- verticalAlign: 'top',
- position: 'relative',
- top: '-9px',
+ height: "29px",
+ verticalAlign: "top",
+ position: "relative",
+ top: "-9px",
},
emptyValue: {
color: theme.customs.colors.grey700,
},
noBorderRow: {
- '& td': {
- borderBottom: 'none',
- }
+ "& td": {
+ borderBottom: "none",
+ },
},
symmetricTabs: {
- '& button': {
- flexBasis: '0',
- }
+ "& button": {
+ flexBasis: "0",
+ },
},
imagePlaceholder: {
- width: '60px',
- height: '60px',
- display: 'flex',
- alignItems: 'center',
- justifyContent: 'center',
- backgroundColor: '#cecece',
- borderRadius: '10px',
+ width: "60px",
+ height: "60px",
+ display: "flex",
+ alignItems: "center",
+ justifyContent: "center",
+ backgroundColor: "#cecece",
+ borderRadius: "10px",
},
rowWithPreview: {
- verticalAlign: 'bottom',
+ verticalAlign: "bottom",
},
labelColumn: {
- minWidth: '120px',
+ minWidth: "120px",
},
});
export enum ProcessIOCardType {
- INPUT = 'Inputs',
- OUTPUT = 'Outputs',
+ INPUT = "Inputs",
+ OUTPUT = "Outputs",
}
export interface ProcessIOCardDataProps {
process?: Process;
}
const mapDispatchToProps = (dispatch: Dispatch): ProcessIOCardActionProps => ({
- navigateTo: (uuid) => dispatch<any>(navigateTo(uuid)),
+ navigateTo: uuid => dispatch<any>(navigateTo(uuid)),
});
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"
+ >
+ <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 ? (
<>
- <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"
+ >
+ <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}
+ >
+ {/* 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
<>
- <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"
+ >
+ <CircularProgress />
+ </Grid>
+ )}
+ {!loading && (hasInputMounts || hasOutputCollecton || hasRaw) ? (
+ <>
+ <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>
+ );
+ }
+ )
+);
export type ProcessIOValue = {
display: ReactElement<any, any>;
imageUrl?: string;
collection?: ReactElement<any, any>;
secondary?: boolean;
-}
+};
export type ProcessIOParameter = {
id: string;
label: string;
value: ProcessIOValue[];
-}
+};
interface ProcessIOPreviewDataProps {
data: ProcessIOParameter[];
type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
-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={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>
+ );
+ })
+);
interface ProcessValuePreviewProps {
value: ProcessIOValue;
showImagePreview: boolean;
}
-const ProcessValuePreview = withStyles(styles)(
- ({ value, showImagePreview, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) =>
- <Typography className={classes.paramValue}>
- {value.imageUrl && showImagePreview ? <img className={classes.imagePreview} src={value.imageUrl} alt="Inline Preview" /> : ""}
- {value.imageUrl && !showImagePreview ? <ImagePlaceholder /> : ""}
- <span className={classNames(classes.valArray, value.secondary && classes.secondaryVal)}>
- {value.display}
- </span>
- </Typography>
-)
+const ProcessValuePreview = withStyles(styles)(({ value, showImagePreview, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) => (
+ <Typography className={classes.paramValue}>
+ {value.imageUrl && showImagePreview ? (
+ <img
+ className={classes.imagePreview}
+ src={value.imageUrl}
+ alt="Inline Preview"
+ />
+ ) : (
+ ""
+ )}
+ {value.imageUrl && !showImagePreview ? <ImagePlaceholder /> : ""}
+ <span className={classNames(classes.valArray, value.secondary && classes.secondaryVal)}>{value.display}</span>
+ </Typography>
+));
interface ProcessIORawDataProps {
data: ProcessIOParameter[];
}
-const ProcessIORaw = withStyles(styles)(
- ({ data }: ProcessIORawDataProps) =>
- <Paper elevation={0}>
- <DefaultCodeSnippet lines={[JSON.stringify(data, null, 2)]} linked />
- </Paper>
-);
+const ProcessIORaw = withStyles(styles)(({ data }: ProcessIORawDataProps) => (
+ <Paper elevation={0}>
+ <DefaultCodeSnippet
+ lines={[JSON.stringify(data, null, 2)]}
+ linked
+ />
+ </Paper>
+));
interface ProcessInputMountsDataProps {
mounts: InputCollectionMount[];
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"
+ >
+ <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}
+ >
+ {mount.pdh}
+ </RouterLink>
+ </TableCell>
+ </TableRow>
+ ))}
+ </TableBody>
+ </Table>
+ ))
+);
type FileWithSecondaryFiles = {
secondaryFiles: File[];
-}
+};
export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParameter | CommandOutputParameter, pdh?: string): ProcessIOValue[] => {
switch (true) {
case isPrimitiveOfType(input, CWLType.BOOLEAN):
const boolValue = (input as BooleanCommandInputParameter).value;
- return boolValue !== undefined &&
- !(Array.isArray(boolValue) && boolValue.length === 0) ?
- [{ display: renderPrimitiveValue(boolValue, false) }] :
- [{ display: <EmptyValue /> }];
+ return boolValue !== undefined && !(Array.isArray(boolValue) && boolValue.length === 0)
+ ? [{ display: renderPrimitiveValue(boolValue, false) }]
+ : [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.INT):
case isPrimitiveOfType(input, CWLType.LONG):
const intValue = (input as IntCommandInputParameter).value;
return intValue !== undefined &&
// Missing values are empty array
- !(Array.isArray(intValue) && intValue.length === 0) ?
- [{ display: renderPrimitiveValue(intValue, false) }]
+ !(Array.isArray(intValue) && intValue.length === 0)
+ ? [{ display: renderPrimitiveValue(intValue, false) }]
: [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.FLOAT):
case isPrimitiveOfType(input, CWLType.DOUBLE):
const floatValue = (input as FloatCommandInputParameter).value;
- return floatValue !== undefined &&
- !(Array.isArray(floatValue) && floatValue.length === 0) ?
- [{ display: renderPrimitiveValue(floatValue, false) }] :
- [{ display: <EmptyValue /> }];
+ return floatValue !== undefined && !(Array.isArray(floatValue) && floatValue.length === 0)
+ ? [{ display: renderPrimitiveValue(floatValue, false) }]
+ : [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.STRING):
const stringValue = (input as StringCommandInputParameter).value || undefined;
- return stringValue !== undefined &&
- !(Array.isArray(stringValue) && stringValue.length === 0) ?
- [{ display: renderPrimitiveValue(stringValue, false) }] :
- [{ display: <EmptyValue /> }];
+ return stringValue !== undefined && !(Array.isArray(stringValue) && stringValue.length === 0)
+ ? [{ display: renderPrimitiveValue(stringValue, false) }]
+ : [{ display: <EmptyValue /> }];
case isPrimitiveOfType(input, CWLType.FILE):
const mainFile = (input as FileCommandInputParameter).value;
// secondaryFiles: File[] is not part of CommandOutputParameter so we cast to access secondaryFiles
- const secondaryFiles = ((mainFile as unknown) as FileWithSecondaryFiles)?.secondaryFiles || [];
- const files = [
- ...(mainFile && !(Array.isArray(mainFile) && mainFile.length === 0) ? [mainFile] : []),
- ...secondaryFiles
- ];
+ const secondaryFiles = (mainFile as unknown as FileWithSecondaryFiles)?.secondaryFiles || [];
+ const files = [...(mainFile && !(Array.isArray(mainFile) && mainFile.length === 0) ? [mainFile] : []), ...secondaryFiles];
const mainFilePdhUrl = mainFile ? getResourcePdhUrl(mainFile, pdh) : "";
- return files.length ?
- files.map((file, i) => fileToProcessIOValue(file, (i > 0), auth, pdh, (i > 0 ? mainFilePdhUrl : ""))) :
- [{ display: <EmptyValue /> }];
+ return files.length
+ ? files.map((file, i) => fileToProcessIOValue(file, i > 0, auth, pdh, i > 0 ? mainFilePdhUrl : ""))
+ : [{ display: <EmptyValue /> }];
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 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 || [];
- return strArray.length ?
- [{ display: <>{strArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
- [{ display: <EmptyValue /> }];
+ return strArray.length ? [{ display: <>{strArray.map(val => renderPrimitiveValue(val, true))}</> }] : [{ display: <EmptyValue /> }];
case isArrayOfType(input, CWLType.INT):
case isArrayOfType(input, CWLType.LONG):
const intArray = (input as IntArrayCommandInputParameter).value || [];
- return intArray.length ?
- [{ display: <>{intArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
- [{ display: <EmptyValue /> }];
+ return intArray.length ? [{ display: <>{intArray.map(val => renderPrimitiveValue(val, true))}</> }] : [{ display: <EmptyValue /> }];
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 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 || [];
- return directories.length ?
- directories.map(directory => directoryToProcessIOValue(directory, auth, pdh)) :
- [{ display: <EmptyValue /> }];
+ return directories.length ? directories.map(directory => directoryToProcessIOValue(directory, auth, pdh)) : [{ display: <EmptyValue /> }];
default:
return [{ display: <UnsupportedValue /> }];
};
const renderPrimitiveValue = (value: any, asChip: boolean) => {
- const isObject = typeof value === 'object';
+ const isObject = typeof value === "object";
if (!isObject) {
- return asChip ? <Chip label={String(value)} /> : <pre>{String(value)}</pre>;
+ return asChip ? (
+ <Chip
+ key={value}
+ label={String(value)}
+ />
+ ) : (
+ <pre key={value}>{String(value)}</pre>
+ );
} else {
return asChip ? <UnsupportedValueChip /> : <UnsupportedValue />;
}
* @returns keep url without keep: prefix
*/
const getKeepUrl = (file: File | Directory, pdh?: string): string => {
- const isKeepUrl = file.location?.startsWith('keep:') || false;
- const keepUrl = isKeepUrl ?
- file.location?.replace('keep:', '') :
- pdh ? `${pdh}/${file.location}` : file.location;
- return keepUrl || '';
+ const isKeepUrl = file.location?.startsWith("keep:") || false;
+ const keepUrl = isKeepUrl ? file.location?.replace("keep:", "") : pdh ? `${pdh}/${file.location}` : file.location;
+ return keepUrl || "";
};
interface KeepUrlProps {
const getResourcePdhUrl = (res: File | Directory, pdh?: string): string => {
const keepUrl = getKeepUrl(res, pdh);
- return keepUrl ? keepUrl.split('/').slice(0, 1)[0] : '';
+ return keepUrl ? keepUrl.split("/").slice(0, 1)[0] : "";
};
const KeepUrlBase = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
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}
+ >
+ {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"
+ >
+ {keepUrlPath || "/"}
+ </a>
+ </Tooltip>
+ ) : (
+ <EmptyValue />
+ );
});
const getKeepNavUrl = (auth: AuthState, file: File | Directory, pdh?: string): string => {
let keepUrl = getKeepUrl(file, pdh);
- return (getInlineFileUrl(`${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`, auth.config.keepWebServiceUrl, auth.config.keepWebInlineServiceUrl));
+ return getInlineFileUrl(
+ `${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`,
+ auth.config.keepWebServiceUrl,
+ auth.config.keepWebInlineServiceUrl
+ );
};
const getImageUrl = (auth: AuthState, file: File, pdh?: string): string => {
const keepUrl = getKeepUrl(file, pdh);
- return getInlineFileUrl(`${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`, auth.config.keepWebServiceUrl, auth.config.keepWebInlineServiceUrl);
+ return getInlineFileUrl(
+ `${auth.config.keepWebServiceUrl}/c=${keepUrl}?api_token=${auth.apiToken}`,
+ auth.config.keepWebServiceUrl,
+ auth.config.keepWebInlineServiceUrl
+ );
};
const isFileImage = (basename?: string): boolean => {
- return basename ? (mime.getType(basename) || "").startsWith('image/') : false;
+ return basename ? (mime.getType(basename) || "").startsWith("image/") : false;
};
-const isFileUrl = (location?: string): boolean => (
- !!location && !KEEP_URL_REGEX.exec(location) &&
- (location.startsWith("http://") || location.startsWith("https://"))
-);
+const isFileUrl = (location?: string): boolean =>
+ !!location && !KEEP_URL_REGEX.exec(location) && (location.startsWith("http://") || location.startsWith("https://"));
const normalizeDirectoryLocation = (directory: Directory): Directory => {
if (!directory.location) {
}
return {
...directory,
- location: (directory.location || '').endsWith('/') ? directory.location : directory.location + '/',
+ location: (directory.location || "").endsWith("/") ? directory.location : directory.location + "/",
};
};
const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: string): ProcessIOValue => {
- if (isExternalValue(directory)) { return { display: <UnsupportedValue /> } }
+ if (isExternalValue(directory)) {
+ return { display: <UnsupportedValue /> };
+ }
const normalizedDirectory = normalizeDirectoryLocation(directory);
return {
- display: <KeepUrlPath auth={auth} res={normalizedDirectory} pdh={pdh} />,
- collection: <KeepUrlBase auth={auth} res={normalizedDirectory} pdh={pdh} />,
+ display: (
+ <KeepUrlPath
+ auth={auth}
+ res={normalizedDirectory}
+ pdh={pdh}
+ />
+ ),
+ collection: (
+ <KeepUrlBase
+ auth={auth}
+ res={normalizedDirectory}
+ pdh={pdh}
+ />
+ ),
};
};
const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => {
- if (isExternalValue(file)) { return { display: <UnsupportedValue /> } }
+ if (isExternalValue(file)) {
+ return { display: <UnsupportedValue /> };
+ }
if (isFileUrl(file.location)) {
return {
- display: <MuiLink href={file.location} target="_blank">{file.location}</MuiLink>,
+ display: (
+ <MuiLink
+ href={file.location}
+ target="_blank"
+ >
+ {file.location}
+ </MuiLink>
+ ),
secondary,
};
}
const resourcePdh = getResourcePdhUrl(file, pdh);
return {
- display: <KeepUrlPath auth={auth} res={file} pdh={pdh} />,
+ display: (
+ <KeepUrlPath
+ auth={auth}
+ res={file}
+ pdh={pdh}
+ />
+ ),
secondary,
imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
- collection: (resourcePdh !== mainFilePdh) ? <KeepUrlBase auth={auth} res={file} pdh={pdh} /> : <></>,
- }
+ collection:
+ resourcePdh !== mainFilePdh ? (
+ <KeepUrlBase
+ auth={auth}
+ res={file}
+ pdh={pdh}
+ />
+ ) : (
+ <></>
+ ),
+ };
};
-const isExternalValue = (val: any) =>
- Object.keys(val).includes('$import') ||
- Object.keys(val).includes('$include')
+const isExternalValue = (val: any) => Object.keys(val).includes("$import") || Object.keys(val).includes("$include");
-export const EmptyValue = withStyles(styles)(
- ({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>
-);
+export const EmptyValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>);
-const UnsupportedValue = withStyles(styles)(
- ({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>
-);
+const UnsupportedValue = withStyles(styles)(({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>);
-const UnsupportedValueChip = withStyles(styles)(
- ({ classes }: WithStyles<CssRules>) => <Chip icon={<InfoIcon />} label={"Cannot display value"} />
-);
+const UnsupportedValueChip = withStyles(styles)(({ classes }: WithStyles<CssRules>) => (
+ <Chip
+ icon={<InfoIcon />}
+ label={"Cannot display value"}
+ />
+));
-const ImagePlaceholder = withStyles(styles)(
- ({ classes }: WithStyles<CssRules>) => <span className={classes.imagePlaceholder}><ImageIcon /></span>
-);
+const ImagePlaceholder = withStyles(styles)(({ classes }: WithStyles<CssRules>) => (
+ <span className={classes.imagePlaceholder}>
+ <ImageIcon />
+ </span>
+));
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
-import { Grid, StyleRulesCallback, WithStyles, withStyles } from '@material-ui/core';
-import { DefaultView } from 'components/default-view/default-view';
-import { ProcessIcon } from 'components/icon/icon';
-import { Process } from 'store/processes/process';
-import { SubprocessPanel } from 'views/subprocess-panel/subprocess-panel';
-import { SubprocessFilterDataProps } from 'components/subprocess-filter/subprocess-filter';
-import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
-import { ArvadosTheme } from 'common/custom-theme';
-import { ProcessDetailsCard } from './process-details-card';
-import { ProcessIOCard, ProcessIOCardType, ProcessIOParameter } from './process-io-card';
-import { ProcessResourceCard } from './process-resource-card';
-import { getProcessPanelLogs, ProcessLogsPanel } from 'store/process-logs-panel/process-logs-panel';
-import { ProcessLogsCard } from './process-log-card';
-import { FilterOption } from 'views/process-panel/process-log-form';
-import { getInputCollectionMounts } from 'store/processes/processes-actions';
-import { WorkflowInputsData } from 'models/workflow';
-import { CommandOutputParameter } from 'cwlts/mappings/v1.0/CommandOutputParameter';
-import { AuthState } from 'store/auth/auth-reducer';
-import { ProcessCmdCard } from './process-cmd-card';
-import { ContainerRequestResource } from 'models/container-request';
-import { OutputDetails, NodeInstanceType } from 'store/process-panel/process-panel';
+import React from "react";
+import { Grid, StyleRulesCallback, WithStyles, withStyles } from "@material-ui/core";
+import { DefaultView } from "components/default-view/default-view";
+import { ProcessIcon } from "components/icon/icon";
+import { Process } from "store/processes/process";
+import { SubprocessPanel } from "views/subprocess-panel/subprocess-panel";
+import { SubprocessFilterDataProps } from "components/subprocess-filter/subprocess-filter";
+import { MPVContainer, MPVPanelContent, MPVPanelState } from "components/multi-panel-view/multi-panel-view";
+import { ArvadosTheme } from "common/custom-theme";
+import { ProcessDetailsCard } from "./process-details-card";
+import { ProcessIOCard, ProcessIOCardType, ProcessIOParameter } from "./process-io-card";
+import { ProcessResourceCard } from "./process-resource-card";
+import { getProcessPanelLogs, ProcessLogsPanel } from "store/process-logs-panel/process-logs-panel";
+import { ProcessLogsCard } from "./process-log-card";
+import { FilterOption } from "views/process-panel/process-log-form";
+import { getInputCollectionMounts } from "store/processes/processes-actions";
+import { WorkflowInputsData } from "models/workflow";
+import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
+import { AuthState } from "store/auth/auth-reducer";
+import { ProcessCmdCard } from "./process-cmd-card";
+import { ContainerRequestResource } from "models/container-request";
+import { OutputDetails, NodeInstanceType } from "store/process-panel/process-panel";
-type CssRules = 'root';
+type CssRules = "root";
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
- width: '100%',
+ width: "100%",
},
});
updateOutputParams,
...props
}: ProcessPanelRootProps) => {
-
const outputUuid = process?.containerRequest.outputUuid;
const containerRequest = process?.containerRequest;
const inputMounts = getInputCollectionMounts(process?.containerRequest);
updateOutputParams();
}, [outputRaw, outputDefinitions, updateOutputParams]);
- return process
- ? <MPVContainer className={props.classes.root} spacing={8} panelStates={panelsData} justify-content="flex-start" direction="column" wrap="nowrap">
- <MPVPanelContent forwardProps xs="auto" data-cy="process-details">
+ return process ? (
+ <MPVContainer
+ className={props.classes.root}
+ spacing={8}
+ panelStates={panelsData}
+ justify-content="flex-start"
+ direction="column"
+ wrap="nowrap">
+ <MPVPanelContent
+ forwardProps
+ xs="auto"
+ data-cy="process-details">
<ProcessDetailsCard
process={process}
onContextMenu={event => props.onContextMenu(event, process)}
resumeOnHoldWorkflow={props.resumeOnHoldWorkflow}
/>
</MPVPanelContent>
- <MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
+ <MPVPanelContent
+ forwardProps
+ xs="auto"
+ data-cy="process-cmd">
<ProcessCmdCard
onCopy={props.onCopyToClipboard}
- process={process} />
+ process={process}
+ />
</MPVPanelContent>
- <MPVPanelContent forwardProps xs minHeight='50%' data-cy="process-logs">
+ <MPVPanelContent
+ forwardProps
+ xs
+ minHeight="50%"
+ data-cy="process-logs">
<ProcessLogsCard
onCopy={props.onCopyToClipboard}
process={process}
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}
mounts={inputMounts}
/>
</MPVPanelContent>
- <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-outputs">
+ <MPVPanelContent
+ forwardProps
+ xs
+ maxHeight="50%"
+ data-cy="process-outputs">
<ProcessIOCard
label={ProcessIOCardType.OUTPUT}
process={process}
outputUuid={outputUuid || ""}
/>
</MPVPanelContent>
- <MPVPanelContent forwardProps xs data-cy="process-resources">
+ <MPVPanelContent
+ forwardProps
+ xs
+ data-cy="process-resources">
<ProcessResourceCard
process={process}
nodeInfo={nodeInfo}
/>
</MPVPanelContent>
- <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-children">
+ <MPVPanelContent
+ forwardProps
+ xs
+ maxHeight="50%"
+ data-cy="process-children">
<SubprocessPanel />
</MPVPanelContent>
</MPVContainer>
- : <Grid container
- alignItems='center'
- justify='center'
- style={{ minHeight: '100%' }}>
+ ) : (
+ <Grid
+ container
+ alignItems="center"
+ justify="center"
+ style={{ minHeight: "100%" }}>
<DefaultView
icon={ProcessIcon}
- messages={['Process not found']} />
- </Grid>;
+ messages={["Process not found"]}
+ />
+ </Grid>
+ );
}
);
//
// SPDX-License-Identifier: AGPL-3.0
-import { RootState } from 'store/store';
-import { connect } from 'react-redux';
-import { getProcess, getSubprocesses, Process, getProcessStatus } from 'store/processes/process';
-import { Dispatch } from 'redux';
-import { openProcessContextMenu } from 'store/context-menu/context-menu-actions';
-import {
- ProcessPanelRootDataProps,
- ProcessPanelRootActionProps,
- ProcessPanelRoot
-} from './process-panel-root';
-import {
- getProcessPanelCurrentUuid,
- ProcessPanel as ProcessPanelState
-} from 'store/process-panel/process-panel';
-import { groupBy } from 'lodash';
+import { RootState } from "store/store";
+import { connect } from "react-redux";
+import { getProcess, getSubprocesses, Process, getProcessStatus } from "store/processes/process";
+import { Dispatch } from "redux";
+import { openProcessContextMenu } from "store/context-menu/context-menu-actions";
+import { ProcessPanelRootDataProps, ProcessPanelRootActionProps, ProcessPanelRoot } from "./process-panel-root";
+import { getProcessPanelCurrentUuid, ProcessPanel as ProcessPanelState } from "store/process-panel/process-panel";
+import { groupBy } from "lodash";
import {
loadInputs,
loadOutputDefinitions,
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, 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),
const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
onCopyToClipboard: (message: string) => {
- dispatch<any>(snackbarActions.OPEN_SNACKBAR({
- message,
- hideDuration: 2000,
- kind: SnackbarKind.SUCCESS,
- }));
+ dispatch<any>(
+ snackbarActions.OPEN_SNACKBAR({
+ message,
+ hideDuration: 2000,
+ kind: SnackbarKind.SUCCESS,
+ })
+ );
},
onContextMenu: (event, process) => {
- dispatch<any>(openProcessContextMenu(event, process));
+ if (process) {
+ dispatch<any>(openProcessContextMenu(event, process));
+ }
},
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[]) => {
const grouppedProcesses = groupBy(processes, getProcessStatus);
- return Object
- .keys(processPanel.filters)
- .map(filter => ({
- label: filter,
- value: (grouppedProcesses[filter] || []).length,
- checked: processPanel.filters[filter],
- key: filter,
- }));
+ return Object.keys(processPanel.filters).map(filter => ({
+ label: filter,
+ value: (grouppedProcesses[filter] || []).length,
+ checked: processPanel.filters[filter],
+ key: filter,
+ }));
};
export const ProcessPanel = connect(mapStateToProps, mapDispatchToProps)(ProcessPanelRoot);
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import withStyles from "@material-ui/core/styles/withStyles";
+import withStyles from '@material-ui/core/styles/withStyles';
import { DispatchProp, connect } from 'react-redux';
import { RouteComponentProps } from 'react-router';
-import { StyleRulesCallback, WithStyles } from "@material-ui/core";
+import { StyleRulesCallback, WithStyles } from '@material-ui/core';
-import { DataExplorer } from "views-components/data-explorer/data-explorer";
+import { DataExplorer } from 'views-components/data-explorer/data-explorer';
import { DataColumns } from 'components/data-table/data-table';
import { RootState } from 'store/store';
import { DataTableFilterItem } from 'components/data-table-filters/data-table-filters';
ResourceDeleteDate,
} from 'views-components/data-explorer/renderers';
import { ProjectIcon } from 'components/icon/icon';
-import {
- ResourcesState,
- getResource
-} from 'store/resources/resources';
+import { ResourcesState, getResource } from 'store/resources/resources';
import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
-import {
- openContextMenu,
- resourceUuidToContextMenuKind
-} from 'store/context-menu/context-menu-actions';
+import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
import { navigateTo } from 'store/navigation/navigation-action';
import { getProperty } from 'store/properties/properties';
import { PROJECT_PANEL_CURRENT_UUID } from 'store/project-panel/project-panel-action';
-import { ArvadosTheme } from "common/custom-theme";
+import { ArvadosTheme } from 'common/custom-theme';
import { createTree } from 'models/tree';
-import {
- getInitialResourceTypeFilters,
- getInitialProcessStatusFilters
-} from 'store/resource-type-filters/resource-type-filters';
+import { getInitialResourceTypeFilters, getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
import { GroupContentsResource } from 'services/groups-service/groups-service';
import { GroupClass, GroupResource } from 'models/group';
import { CollectionResource } from 'models/collection';
import { resourceIsFrozen } from 'common/frozen-resources';
import { ProjectResource } from 'models/project';
-type CssRules = 'root' | "button";
+type CssRules = 'root' | 'button';
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
root: {
width: '100%',
},
button: {
- marginLeft: theme.spacing.unit
+ marginLeft: theme.spacing.unit,
},
});
export enum ProjectPanelColumnNames {
- NAME = "Name",
- STATUS = "Status",
- TYPE = "Type",
- OWNER = "Owner",
- PORTABLE_DATA_HASH = "Portable Data Hash",
- FILE_SIZE = "File Size",
- FILE_COUNT = "File Count",
- UUID = "UUID",
- CONTAINER_UUID = "Container UUID",
- RUNTIME = "Runtime",
- OUTPUT_UUID = "Output UUID",
- LOG_UUID = "Log UUID",
+ NAME = 'Name',
+ STATUS = 'Status',
+ TYPE = 'Type',
+ OWNER = 'Owner',
+ PORTABLE_DATA_HASH = 'Portable Data Hash',
+ FILE_SIZE = 'File Size',
+ FILE_COUNT = 'File Count',
+ UUID = 'UUID',
+ CONTAINER_UUID = 'Container UUID',
+ RUNTIME = 'Runtime',
+ OUTPUT_UUID = 'Output UUID',
+ LOG_UUID = 'Log UUID',
PARENT_PROCESS = 'Parent Process UUID',
MODIFIED_BY_USER_UUID = 'Modified by User UUID',
- VERSION = "Version",
- CREATED_AT = "Date Created",
- LAST_MODIFIED = "Last Modified",
- TRASH_AT = "Trash at",
- DELETE_AT = "Delete at",
+ VERSION = 'Version',
+ CREATED_AT = 'Date Created',
+ LAST_MODIFIED = 'Last Modified',
+ TRASH_AT = 'Trash at',
+ DELETE_AT = 'Delete at',
}
export interface ProjectPanelFilter extends DataTableFilterItem {
name: ProjectPanelColumnNames.NAME,
selected: true,
configurable: true,
- sort: {direction: SortDirection.NONE, field: "name"},
+ sort: { direction: SortDirection.NONE, field: 'name' },
filters: createTree(),
- render: uuid => <ResourceName uuid={uuid} />
+ render: (uuid) => <ResourceName uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.STATUS,
configurable: true,
mutuallyExclusiveFilters: true,
filters: getInitialProcessStatusFilters(),
- render: uuid => <ResourceStatus uuid={uuid} />,
+ render: (uuid) => <ResourceStatus uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.TYPE,
selected: true,
configurable: true,
filters: getInitialResourceTypeFilters(),
- render: uuid => <ResourceType uuid={uuid} />
+ render: (uuid) => <ResourceType uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.OWNER,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceOwnerWithName uuid={uuid} />
+ render: (uuid) => <ResourceOwnerWithName uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.PORTABLE_DATA_HASH,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourcePortableDataHash uuid={uuid} />
+ render: (uuid) => <ResourcePortableDataHash uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.FILE_SIZE,
selected: true,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceFileSize uuid={uuid} />
+ render: (uuid) => <ResourceFileSize uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.FILE_COUNT,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceFileCount uuid={uuid} />
+ render: (uuid) => <ResourceFileCount uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.UUID,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceUUID uuid={uuid} />
+ render: (uuid) => <ResourceUUID uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.CONTAINER_UUID,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceContainerUuid uuid={uuid} />
+ render: (uuid) => <ResourceContainerUuid uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.RUNTIME,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ContainerRunTime uuid={uuid} />
+ render: (uuid) => <ContainerRunTime uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.OUTPUT_UUID,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceOutputUuid uuid={uuid} />
+ render: (uuid) => <ResourceOutputUuid uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.LOG_UUID,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceLogUuid uuid={uuid} />
+ render: (uuid) => <ResourceLogUuid uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.PARENT_PROCESS,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceParentProcess uuid={uuid} />
+ render: (uuid) => <ResourceParentProcess uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.MODIFIED_BY_USER_UUID,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceModifiedByUserUuid uuid={uuid} />
+ render: (uuid) => <ResourceModifiedByUserUuid uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.VERSION,
selected: false,
configurable: true,
filters: createTree(),
- render: uuid => <ResourceVersion uuid={uuid} />
+ render: (uuid) => <ResourceVersion uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.CREATED_AT,
selected: false,
configurable: true,
- sort: {direction: SortDirection.NONE, field: "createdAt"},
+ sort: { direction: SortDirection.NONE, field: 'createdAt' },
filters: createTree(),
- render: uuid => <ResourceCreatedAtDate uuid={uuid} />
+ render: (uuid) => <ResourceCreatedAtDate uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.LAST_MODIFIED,
selected: true,
configurable: true,
- sort: {direction: SortDirection.DESC, field: "modifiedAt"},
+ sort: { direction: SortDirection.DESC, field: 'modifiedAt' },
filters: createTree(),
- render: uuid => <ResourceLastModifiedDate uuid={uuid} />
+ render: (uuid) => <ResourceLastModifiedDate uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.TRASH_AT,
selected: false,
configurable: true,
- sort: {direction: SortDirection.NONE, field: "trashAt"},
+ sort: { direction: SortDirection.NONE, field: 'trashAt' },
filters: createTree(),
- render: uuid => <ResourceTrashDate uuid={uuid} />
+ render: (uuid) => <ResourceTrashDate uuid={uuid} />,
},
{
name: ProjectPanelColumnNames.DELETE_AT,
selected: false,
configurable: true,
- sort: {direction: SortDirection.NONE, field: "deleteAt"},
+ sort: { direction: SortDirection.NONE, field: 'deleteAt' },
filters: createTree(),
- render: uuid => <ResourceDeleteDate uuid={uuid} />
+ render: (uuid) => <ResourceDeleteDate uuid={uuid} />,
},
-
];
-export const PROJECT_PANEL_ID = "projectPanel";
+export const PROJECT_PANEL_ID = 'projectPanel';
-const DEFAULT_VIEW_MESSAGES = [
- 'Your project is empty.',
- 'Please create a project or create a collection and upload a data.',
-];
+const DEFAULT_VIEW_MESSAGES = ['Your project is empty.', 'Please create a project or create a collection and upload a data.'];
interface ProjectPanelDataProps {
currentItemId: string;
dataExplorerItems: any;
}
-type ProjectPanelProps = ProjectPanelDataProps & DispatchProp
- & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
-
+type ProjectPanelProps = ProjectPanelDataProps & DispatchProp & WithStyles<CssRules> & RouteComponentProps<{ id: string }>;
export const ProjectPanel = withStyles(styles)(
connect((state: RootState) => ({
currentItemId: getProperty(PROJECT_PANEL_CURRENT_UUID)(state.properties),
resources: state.resources,
- userUuid: state.auth.user!.uuid
+ userUuid: state.auth.user!.uuid,
}))(
class extends React.Component<ProjectPanelProps> {
render() {
const { classes } = this.props;
- return <div data-cy='project-panel' className={classes.root}>
- <DataExplorer
- id={PROJECT_PANEL_ID}
- onRowClick={this.handleRowClick}
- onRowDoubleClick={this.handleRowDoubleClick}
- onContextMenu={this.handleContextMenu}
- contextMenuColumn={true}
- defaultViewIcon={ProjectIcon}
- defaultViewMessages={DEFAULT_VIEW_MESSAGES}
- />
- </div>;
+ return (
+ <div data-cy='project-panel' className={classes.root}>
+ <DataExplorer
+ id={PROJECT_PANEL_ID}
+ onRowClick={this.handleRowClick}
+ onRowDoubleClick={this.handleRowDoubleClick}
+ onContextMenu={this.handleContextMenu}
+ contextMenuColumn={true}
+ defaultViewIcon={ProjectIcon}
+ defaultViewMessages={DEFAULT_VIEW_MESSAGES}
+ />
+ </div>
+ );
}
isCurrentItemChild = (resource: Resource) => {
return resource.ownerUuid === this.props.currentItemId;
- }
+ };
handleContextMenu = (event: React.MouseEvent<HTMLElement>, resourceUuid: string) => {
const { resources, isAdmin } = this.props;
const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(resourceUuid, readonly));
if (menuKind && resource) {
- this.props.dispatch<any>(openContextMenu(event, {
- name: resource.name,
- uuid: resource.uuid,
- ownerUuid: resource.ownerUuid,
- isTrashed: ('isTrashed' in resource) ? resource.isTrashed : false,
- kind: resource.kind,
- menuKind,
- isAdmin,
- isFrozen: resourceIsFrozen(resource, resources),
- description: resource.description,
- storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
- properties: ('properties' in resource) ? resource.properties : {},
- }));
+ this.props.dispatch<any>(
+ openContextMenu(event, {
+ name: resource.name,
+ uuid: resource.uuid,
+ ownerUuid: resource.ownerUuid,
+ isTrashed: 'isTrashed' in resource ? resource.isTrashed : false,
+ kind: resource.kind,
+ menuKind,
+ isAdmin,
+ isFrozen: resourceIsFrozen(resource, resources),
+ description: resource.description,
+ storageClassesDesired: (resource as CollectionResource).storageClassesDesired,
+ properties: 'properties' in resource ? resource.properties : {},
+ })
+ );
}
this.props.dispatch<any>(loadDetailsPanel(resourceUuid));
- }
+ };
handleRowDoubleClick = (uuid: string) => {
this.props.dispatch<any>(navigateTo(uuid));
- }
+ };
handleRowClick = (uuid: string) => {
this.props.dispatch<any>(loadDetailsPanel(uuid));
- }
-
+ };
}
)
);
import { GenericInputProps, GenericInput } from './generic-input';
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
import { connect, DispatchProp } from 'react-redux';
-import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, getAllNodes } from 'store/tree-picker/tree-picker-actions';
+import { initProjectsTreePicker, getSelectedNodes, treePickerActions, getProjectsTreePickerIds, FileOperationLocation, getFileOperationLocation, fileOperationLocationToPickerId } from 'store/tree-picker/tree-picker-actions';
import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
import { createSelector, createStructuredSelector } from 'reselect';
import { ChipsInput } from 'components/chips-input/chips-input';
import { Chips } from 'components/chips/chips';
import withStyles, { StyleRulesCallback } from '@material-ui/core/styles/withStyles';
import { CollectionResource } from 'models/collection';
-import { ResourceKind } from 'models/resource';
+import { PORTABLE_DATA_HASH_PATTERN, ResourceKind } from 'models/resource';
+import { Dispatch } from 'redux';
+import { CollectionDirectory, CollectionFileType } from 'models/collection-file';
+const LOCATION_REGEX = new RegExp("^(?:keep:)?(" + PORTABLE_DATA_HASH_PATTERN + ")(/.*)?$");
export interface DirectoryArrayInputProps {
input: DirectoryArrayCommandInputParameter;
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
interface FormattedDirectory {
name: string;
portableDataHash: string;
+ subpath: string;
}
-const parseDirectories = (directories: CollectionResource[] | string) =>
+const parseDirectories = (directories: FileOperationLocation[] | string) =>
typeof directories === 'string'
? undefined
: directories.map(parse);
-const parse = (directory: CollectionResource): Directory => ({
+const parse = (directory: FileOperationLocation): Directory => ({
class: CWLType.DIRECTORY,
basename: directory.name,
- location: `keep:${directory.portableDataHash}`,
+ location: `keep:${directory.pdh}${directory.subpath}`,
});
-const formatDirectories = (directories: Directory[] = []) =>
- directories ? directories.map(format) : [];
+const formatDirectories = (directories: Directory[] = []): FormattedDirectory[] =>
+ directories ? directories.map(format).filter((dir): dir is FormattedDirectory => Boolean(dir)) : [];
-const format = ({ location = '', basename = '' }: Directory): FormattedDirectory => ({
- portableDataHash: location.replace('keep:', ''),
- name: basename,
-});
+const format = ({ location = '', basename = '' }: Directory): FormattedDirectory | undefined => {
+ const match = LOCATION_REGEX.exec(location);
+
+ if (match) {
+ return {
+ portableDataHash: match[1],
+ subpath: match[2],
+ name: basename,
+ };
+ }
+ return undefined;
+};
const validationSelector = createSelector(
isRequiredInput,
: ERROR_MESSAGE;
interface DirectoryArrayInputComponentState {
open: boolean;
- directories: CollectionResource[];
- prevDirectories: CollectionResource[];
+ directories: FileOperationLocation[];
}
-interface DirectoryArrayInputComponentProps {
+interface DirectoryArrayInputDataProps {
treePickerState: TreePicker;
}
treePickerState: treePickerSelector,
});
-const DirectoryArrayInputComponent = connect(mapStateToProps)(
- class DirectoryArrayInputComponent extends React.Component<DirectoryArrayInputComponentProps & GenericInputProps & DispatchProp & {
+interface DirectoryArrayInputActionProps {
+ initProjectsTreePicker: (pickerId: string) => void;
+ selectTreePickerNode: (pickerId: string, id: string | string[]) => void;
+ deselectTreePickerNode: (pickerId: string, id: string | string[]) => void;
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DirectoryArrayInputActionProps => ({
+ initProjectsTreePicker: (pickerId: string) => dispatch<any>(initProjectsTreePicker(pickerId)),
+ selectTreePickerNode: (pickerId: string, id: string | string[]) =>
+ dispatch<any>(treePickerActions.SELECT_TREE_PICKER_NODE({
+ pickerId, id, cascade: false
+ })),
+ deselectTreePickerNode: (pickerId: string, id: string | string[]) =>
+ dispatch<any>(treePickerActions.DESELECT_TREE_PICKER_NODE({
+ pickerId, id, cascade: false
+ })),
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+const DirectoryArrayInputComponent = connect(mapStateToProps, mapDispatchToProps)(
+ class DirectoryArrayInputComponent extends React.Component<GenericInputProps & DirectoryArrayInputDataProps & DirectoryArrayInputActionProps & DispatchProp & {
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}, DirectoryArrayInputComponentState> {
state: DirectoryArrayInputComponentState = {
open: false,
directories: [],
- prevDirectories: [],
};
directoryRefreshTimeout = -1;
componentDidMount() {
- this.props.dispatch<any>(
- initProjectsTreePicker(this.props.commandInput.id));
+ this.props.initProjectsTreePicker(this.props.commandInput.id);
}
render() {
}
openDialog = () => {
- this.setDirectoriesFromProps(this.props.input.value);
this.setState({ open: true });
}
this.props.input.onChange(this.state.directories);
}
- setDirectories = (directories: CollectionResource[]) => {
+ setDirectoriesFromResources = async (directories: (CollectionResource | CollectionDirectory)[]) => {
+ const locations = (await Promise.all(
+ directories.map(directory => (this.props.getFileOperationLocation(directory)))
+ )).filter((location): location is FileOperationLocation => (
+ location !== undefined
+ ));
- const deletedDirectories = this.state.directories
- .reduce((deletedDirectories, directory) =>
- directories.some(({ uuid }) => uuid === directory.uuid)
- ? deletedDirectories
- : [...deletedDirectories, directory]
- , []);
-
- this.setState({ directories });
-
- const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
- ids.forEach(pickerId => {
- this.props.dispatch(
- treePickerActions.DESELECT_TREE_PICKER_NODE({
- pickerId, id: deletedDirectories.map(({ uuid }) => uuid),
- })
- );
- });
+ this.setDirectories(locations);
+ }
+ refreshDirectories = () => {
+ clearTimeout(this.directoryRefreshTimeout);
+ this.directoryRefreshTimeout = window.setTimeout(this.setDirectoriesFromTree);
}
- setDirectoriesFromProps = (formattedDirectories: FormattedDirectory[]) => {
- const nodes = getAllNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
- const initialDirectories: CollectionResource[] = [];
+ setDirectoriesFromTree = () => {
+ const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
+ const initialDirectories: (CollectionResource | CollectionDirectory)[] = [];
const directories = nodes
.reduce((directories, { value }) =>
- 'kind' in value &&
- value.kind === ResourceKind.COLLECTION &&
- formattedDirectories.find(({ portableDataHash, name }) => value.portableDataHash === portableDataHash && value.name === name)
+ (('kind' in value && value.kind === ResourceKind.COLLECTION) ||
+ ('type' in value && value.type === CollectionFileType.DIRECTORY))
? directories.concat(value)
: directories, initialDirectories);
+ this.setDirectoriesFromResources(directories);
+ }
+
+ setDirectories = (locations: FileOperationLocation[]) => {
+ const deletedDirectories = this.state.directories
+ .reduce((deletedDirectories, directory) =>
+ locations.some(({ uuid, subpath }) => uuid === directory.uuid && subpath === directory.subpath)
+ ? deletedDirectories
+ : [...deletedDirectories, directory]
+ , [] as FileOperationLocation[]);
- const addedDirectories = directories
- .reduce((addedDirectories, directory) =>
- this.state.directories.find(({ uuid }) =>
- uuid === directory.uuid)
- ? addedDirectories
- : [...addedDirectories, directory]
- , []);
+ this.setState({ directories: locations });
const ids = values(getProjectsTreePickerIds(this.props.commandInput.id));
ids.forEach(pickerId => {
- this.props.dispatch(
- treePickerActions.SELECT_TREE_PICKER_NODE({
- pickerId, id: addedDirectories.map(({ uuid }) => uuid),
- })
+ this.props.deselectTreePickerNode(
+ pickerId,
+ deletedDirectories.map(fileOperationLocationToPickerId)
);
});
+ };
- const orderedDirectories = formattedDirectories.reduce((dirs, formattedDir) => {
- const dir = directories.find(({ portableDataHash, name }) => portableDataHash === formattedDir.portableDataHash && name === formattedDir.name);
- return dir
- ? [...dirs, dir]
- : dirs;
- }, []);
-
- this.setDirectories(orderedDirectories);
-
- }
-
- refreshDirectories = () => {
- clearTimeout(this.directoryRefreshTimeout);
- this.directoryRefreshTimeout = window.setTimeout(this.setSelectedFiles);
- }
-
- setSelectedFiles = () => {
- const nodes = getSelectedNodes<ProjectsTreePickerItem>(this.props.commandInput.id)(this.props.treePickerState);
- const initialDirectories: CollectionResource[] = [];
- const directories = nodes
- .reduce((directories, { value }) =>
- 'kind' in value && value.kind === ResourceKind.COLLECTION
- ? directories.concat(value)
- : directories, initialDirectories);
- this.setDirectories(directories);
- }
input = () =>
<GenericInput
component={this.chipsInput}
onClose={this.closeDialog}
fullWidth
maxWidth='md' >
- <DialogTitle>Choose collections</DialogTitle>
+ <DialogTitle>Choose directories</DialogTitle>
<DialogContent className={classes.root}>
<div className={classes.pickerWrapper}>
<div className={classes.tree}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
+ currentUuids={this.state.directories.map(dir => fileOperationLocationToPickerId(dir))}
includeCollections
+ includeDirectories
showSelection
+ cascadeSelection={false}
options={this.props.options}
toggleItemSelection={this.refreshDirectories} />
</div>
} from 'models/workflow';
import { GenericInputProps, GenericInput } from './generic-input';
import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
-import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { FileOperationLocation, getFileOperationLocation, initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
import { TreeItem } from 'components/tree/tree';
import { ProjectsTreePickerItem } from 'store/tree-picker/tree-picker-middleware';
-import { CollectionResource } from 'models/collection';
-import { ResourceKind } from 'models/resource';
import { ERROR_MESSAGE } from 'validators/require';
+import { Dispatch } from 'redux';
export interface DirectoryInputProps {
input: DirectoryCommandInputParameter;
const format = (value?: Directory) => value ? value.basename : '';
-const parse = (directory: CollectionResource): Directory => ({
+const parse = (directory: FileOperationLocation): Directory => ({
class: CWLType.DIRECTORY,
- location: `keep:${directory.portableDataHash}`,
+ location: `keep:${directory.pdh}${directory.subpath}`,
basename: directory.name,
});
interface DirectoryInputComponentState {
open: boolean;
- directory?: CollectionResource;
+ directory?: FileOperationLocation;
}
-const DirectoryInputComponent = connect()(
- class FileInputComponent extends React.Component<GenericInputProps & DispatchProp & {
+interface DirectoryInputActionProps {
+ initProjectsTreePicker: (pickerId: string) => void;
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => Promise<FileOperationLocation | undefined>;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): DirectoryInputActionProps => ({
+ initProjectsTreePicker: (pickerId: string) => dispatch<any>(initProjectsTreePicker(pickerId)),
+ getFileOperationLocation: (item: ProjectsTreePickerItem) => dispatch<any>(getFileOperationLocation(item)),
+});
+
+const DirectoryInputComponent = connect(null, mapDispatchToProps)(
+ class FileInputComponent extends React.Component<GenericInputProps & DirectoryInputActionProps & DispatchProp & {
options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
}, DirectoryInputComponentState> {
state: DirectoryInputComponentState = {
};
componentDidMount() {
- this.props.dispatch<any>(
- initProjectsTreePicker(this.props.commandInput.id));
+ this.props.initProjectsTreePicker(this.props.commandInput.id);
}
render() {
this.props.input.onChange(this.state.directory);
}
- setDirectory = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
- if ('kind' in data && data.kind === ResourceKind.COLLECTION) {
- this.setState({ directory: data });
- } else {
- this.setState({ directory: undefined });
- }
+ setDirectory = async (_: {}, { data: item }: TreeItem<ProjectsTreePickerItem>) => {
+ const location = await this.props.getFileOperationLocation(item);
+ this.setState({ directory: location });
}
renderInput() {
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
includeCollections
+ includeDirectories
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setDirectory} />
</div>
ids.forEach(pickerId => {
this.props.dispatch(
treePickerActions.DESELECT_TREE_PICKER_NODE({
- pickerId, id: deletedFiles.map(({ id }) => id),
+ pickerId,
+ id: deletedFiles.map(({ id }) => id),
+ cascade: true,
})
);
});
ids.forEach(pickerId => {
this.props.dispatch(
treePickerActions.SELECT_TREE_PICKER_NODE({
- pickerId, id: addedFiles.map(({ id }) => id),
+ pickerId,
+ id: addedFiles.map(({ id }) => id),
+ cascade: true,
})
);
});
includeDirectories
includeFiles
showSelection
+ cascadeSelection={true}
options={this.props.options}
toggleItemSelection={this.refreshFiles} />
</div>
includeCollections
includeDirectories
includeFiles
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setFile} />
</div>
<div className={classes.pickerWrapper}>
<ProjectsTreePicker
pickerId={this.props.commandInput.id}
+ cascadeSelection={false}
options={this.props.options}
toggleItemActive={this.setProject} />
</div>
import { Dispatch } from "redux";
import { connect } from "react-redux";
-import { openProcessContextMenu } from 'store/context-menu/context-menu-actions';
-import { SubprocessPanelRoot, SubprocessPanelActionProps, SubprocessPanelDataProps } from 'views/subprocess-panel/subprocess-panel-root';
+import { openProcessContextMenu } from "store/context-menu/context-menu-actions";
+import { SubprocessPanelRoot, SubprocessPanelActionProps, SubprocessPanelDataProps } from "views/subprocess-panel/subprocess-panel-root";
import { RootState } from "store/store";
import { navigateTo } from "store/navigation/navigation-action";
import { loadDetailsPanel } from "store/details-panel/details-panel-action";
},
onItemDoubleClick: uuid => {
dispatch<any>(navigateTo(uuid));
- }
+ },
});
const mapStateToProps = (state: RootState): SubprocessPanelDataProps => ({
- resources: state.resources
+ resources: state.resources,
});
-export const SubprocessPanel = connect(mapStateToProps, mapDispatchToProps)(SubprocessPanelRoot);
\ No newline at end of file
+export const SubprocessPanel = connect(mapStateToProps, mapDispatchToProps)(SubprocessPanelRoot);
//
// 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 { 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 { 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: {
paddingTop: theme.spacing.unit * 7,
- background: theme.palette.background.default
+ background: theme.palette.background.default,
},
container: {
- position: 'relative'
+ position: "relative",
},
splitter: {
- '& > .layout-splitter': {
- width: '2px',
+ "& > .layout-splitter": {
+ width: "2px",
+ },
+ "& > .layout-splitter-disabled": {
+ pointerEvents: "none",
+ cursor: "pointer",
},
- '& > .layout-splitter-disabled': {
- pointerEvents: 'none',
- cursor: 'pointer'
- }
},
asidePanel: {
paddingTop: theme.spacing.unit,
- height: '100%'
+ height: "100%",
},
contentWrapper: {
paddingTop: theme.spacing.unit,
- minWidth: 0
+ minWidth: 0,
},
content: {
minWidth: 0,
paddingRight: theme.spacing.unit * 3,
// Reserve vertical space for app bar + MainContentBar
minHeight: `calc(100vh - ${theme.spacing.unit * 16}px)`,
- display: 'flex',
- }
+ display: "flex",
+ },
});
interface WorkbenchDataProps {
const defaultSplitterSize = 90;
const getSplitterInitialSize = () => {
- const splitterSize = localStorage.getItem('splitterSize');
+ const splitterSize = localStorage.getItem("splitterSize");
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}
+ >
{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}
+ >
+ <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
+ >
{props.isNotLinking && <MainContentBar />}
</Grid>
- <Grid item xs className={props.classes.content}>
+ <Grid
+ item
+ xs
+ 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>
<ChangeWorkflowDialog />
<ContextMenu />
<CopyCollectionDialog />
+ <CopyMultiCollectionDialog />
<CopyProcessDialog />
<CreateCollectionDialog />
<CreateProjectDialog />
<Banner />
{React.createElement(React.Fragment, null, pluginConfig.dialogs)}
</Grid>
- }
);
+});
//
// SPDX-License-Identifier: AGPL-3.0
-import { RootStore } from 'store/store';
-import { AuthService } from 'services/auth-service/auth-service';
-import { Config } from 'common/config';
-import { WebSocketService } from './websocket-service';
-import { ResourceEventMessage } from './resource-event-message';
-import { ResourceKind } from 'models/resource';
-import { loadProcess } from 'store/processes/processes-actions';
-import { getProcess, getSubprocesses } from 'store/processes/process';
-import { LogEventType } from 'models/log';
+import { RootStore } from "store/store";
+import { AuthService } from "services/auth-service/auth-service";
+import { Config } from "common/config";
+import { WebSocketService } from "./websocket-service";
+import { ResourceEventMessage } from "./resource-event-message";
+import { ResourceKind } from "models/resource";
+import { loadProcess } from "store/processes/processes-actions";
+import { getProcess, getSubprocesses } from "store/processes/process";
+import { LogEventType } from "models/log";
import { subprocessPanelActions } from "store/subprocess-panel/subprocess-panel-actions";
-import { projectPanelActions } from "store/project-panel/project-panel-action";
-import { getProjectPanelCurrentUuid } from 'store/project-panel/project-panel-action';
-import { allProcessesPanelActions } from 'store/all-processes-panel/all-processes-panel-action';
-import { loadCollection } from 'store/workbench/workbench-actions';
-import { matchAllProcessesRoute, matchProjectRoute, matchProcessRoute } from 'routes/routes';
+import { projectPanelActions } from "store/project-panel/project-panel-action-bind";
+import { getProjectPanelCurrentUuid } from "store/project-panel/project-panel-action";
+import { allProcessesPanelActions } from "store/all-processes-panel/all-processes-panel-action";
+import { loadCollection } from "store/workbench/workbench-actions";
+import { matchAllProcessesRoute, matchProjectRoute, matchProcessRoute } from "routes/routes";
export const initWebSocket = (config: Config, authService: AuthService, store: RootStore) => {
if (config.websocketUrl) {
const messageListener = (store: RootStore) => (message: ResourceEventMessage) => {
if (message.eventType === LogEventType.CREATE || message.eventType === LogEventType.UPDATE) {
const state = store.getState();
- const location = state.router.location ? state.router.location.pathname : '';
+ const location = state.router.location ? state.router.location.pathname : "";
switch (message.objectKind) {
case ResourceKind.COLLECTION:
const currentCollection = state.collectionPanel.item;
}
const proc = getProcess(state.processPanel.containerRequestUuid)(state.resources);
if (proc && proc.container && proc.container.uuid === message.properties["new_attributes"]["requesting_container_uuid"]) {
- store.dispatch(subprocessPanelActions.REQUEST_ITEMS());
+ store.dispatch(subprocessPanelActions.REQUEST_ITEMS(false, true));
return;
}
}
const subproc = getSubprocesses(state.processPanel.containerRequestUuid)(state.resources);
for (const sb of subproc) {
if (sb.containerRequest.uuid === message.objectUuid || (sb.container && sb.container.uuid === message.objectUuid)) {
- store.dispatch(subprocessPanelActions.REQUEST_ITEMS());
+ store.dispatch(subprocessPanelActions.REQUEST_ITEMS(false, true));
break;
}
}
}
if (matchAllProcessesRoute(location)) {
- store.dispatch(allProcessesPanelActions.REQUEST_ITEMS());
+ store.dispatch(allProcessesPanelActions.REQUEST_ITEMS(false, true));
}
if (matchProjectRoute(location) && message.objectOwnerUuid === getProjectPanelCurrentUuid(state)) {
- store.dispatch(projectPanelActions.REQUEST_ITEMS());
+ store.dispatch(projectPanelActions.REQUEST_ITEMS(false, true));
}
return;
default: