src/lib/cwl-svg/*
tools/arvados_config.yml
cypress/fixtures/files/5mb.bin
+cypress/fixtures/files/cat.png
+cypress/fixtures/webdav-propfind-outputs.xml
.yarn/releases/*
package.json
yarn.lock
--- /dev/null
+<?xml version="1.0" encoding="UTF-8"?>
+<D:multistatus xmlns:D="DAV:">
+ <D:response>
+ <D:href>/c=zzzzz-4zz18-zzzzzzzzzzzzzzz/</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:resourcetype>
+ <D:collection xmlns:D="DAV:" />
+ </D:resourcetype>
+ <D:getlastmodified>Mon, 11 Jul 2022 21:54:20 GMT</D:getlastmodified>
+ <D:supportedlock>
+ <D:lockentry xmlns:D="DAV:">
+ <D:lockscope>
+ <D:exclusive />
+ </D:lockscope>
+ <D:locktype>
+ <D:write />
+ </D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ <D:displayname></D:displayname>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+ <D:response>
+ <D:href>/c=zzzzz-4zz18-zzzzzzzzzzzzzzz/cwl.output.json</D:href>
+ <D:propstat>
+ <D:prop>
+ <D:displayname>cwl.output.json</D:displayname>
+ <D:getcontentlength>141</D:getcontentlength>
+ <D:getlastmodified>Mon, 11 Jul 2022 21:54:20 GMT</D:getlastmodified>
+ <D:supportedlock>
+ <D:lockentry xmlns:D="DAV:">
+ <D:lockscope>
+ <D:exclusive />
+ </D:lockscope>
+ <D:locktype>
+ <D:write />
+ </D:locktype>
+ </D:lockentry>
+ </D:supportedlock>
+ <D:resourcetype></D:resourcetype>
+ <D:getcontenttype>application/json</D:getcontenttype>
+ <D:getetag>"000000000000000000"</D:getetag>
+ </D:prop>
+ <D:status>HTTP/1.1 200 OK</D:status>
+ </D:propstat>
+ </D:response>
+</D:multistatus>
// 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", false);
+ 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 !== ""
.contains(fileName).rightclick();
cy.get('[data-cy=context-menu]')
.should('contain', 'Download')
- .and('not.contain', 'Open in new tab')
+ .and('contain', 'Open in new tab')
.and('contain', 'Copy to clipboard')
.and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
.and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
.contains(subDirName).rightclick();
cy.get('[data-cy=context-menu]')
.should('not.contain', 'Download')
- .and('not.contain', 'Open in new tab')
+ .and('contain', 'Open in new tab')
.and('contain', 'Copy to clipboard')
.and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
.and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
.should('contain', 'Process retried 2 times');
});
});
+
+
+ const testInputs = [
+ {
+ definition: {
+ "id": "#main/input_file",
+ "label": "Label Description",
+ "type": "File"
+ },
+ input: {
+ "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-3.txt",
+ "class": "File",
+ "location": "keep:00000000000000000000000000000000+01/input1-3.txt"
+ },
+ {
+ "basename": "input1-4.txt",
+ "class": "File",
+ "location": "keep:00000000000000000000000000000000+01/input1-4.txt"
+ }
+ ]
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_dir",
+ "doc": "Doc Description",
+ "type": "Directory"
+ },
+ input: {
+ "input_dir": {
+ "basename": "11111111111111111111111111111111+01",
+ "class": "Directory",
+ "location": "keep:11111111111111111111111111111111+01"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_bool",
+ "doc": ["Doc desc 1", "Doc desc 2"],
+ "type": "boolean"
+ },
+ input: {
+ "input_bool": true,
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_int",
+ "type": "int"
+ },
+ input: {
+ "input_int": 1,
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_long",
+ "type": "long"
+ },
+ input: {
+ "input_long" : 1,
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_float",
+ "type": "float"
+ },
+ input: {
+ "input_float": 1.5,
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_double",
+ "type": "double"
+ },
+ input: {
+ "input_double": 1.3,
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_string",
+ "type": "string"
+ },
+ input: {
+ "input_string": "Hello World",
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_file_array",
+ "type": {
+ "items": "File",
+ "type": "array"
+ }
+ },
+ input: {
+ "input_file_array": [
+ {
+ "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-2.txt",
+ "class": "File",
+ "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
+ }
+ ]
+ }
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_dir_array",
+ "type": {
+ "items": "Directory",
+ "type": "array"
+ }
+ },
+ input: {
+ "input_dir_array": [
+ {
+ "basename": "11111111111111111111111111111111+02",
+ "class": "Directory",
+ "location": "keep:11111111111111111111111111111111+02"
+ },
+ {
+ "basename": "11111111111111111111111111111111+03",
+ "class": "Directory",
+ "location": "keep:11111111111111111111111111111111+03"
+ }
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_int_array",
+ "type": {
+ "items": "int",
+ "type": "array"
+ }
+ },
+ input: {
+ "input_int_array": [
+ 1,
+ 3,
+ 5
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_long_array",
+ "type": {
+ "items": "long",
+ "type": "array"
+ }
+ },
+ input: {
+ "input_long_array": [
+ 10,
+ 20
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_float_array",
+ "type": {
+ "items": "float",
+ "type": "array"
+ }
+ },
+ input: {
+ "input_float_array": [
+ 10.2,
+ 10.4,
+ 10.6
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_double_array",
+ "type": {
+ "items": "double",
+ "type": "array"
+ }
+ },
+ input: {
+ "input_double_array": [
+ 20.1,
+ 20.2,
+ 20.3
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_string_array",
+ "type": {
+ "items": "string",
+ "type": "array"
+ }
+ },
+ input: {
+ "input_string_array": [
+ "Hello",
+ "World",
+ "!"
+ ]
+ }
+ }
+ ];
+
+ const testOutputs = [
+ {
+ definition: {
+ "id": "#main/output_file",
+ "label": "Label Description",
+ "type": "File"
+ },
+ output: {
+ "output_file": {
+ "basename": "cat.png",
+ "class": "File",
+ "location": "cat.png"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_file_with_secondary",
+ "doc": "Doc Description",
+ "type": "File"
+ },
+ output: {
+ "output_file_with_secondary": {
+ "basename": "main.dat",
+ "class": "File",
+ "location": "main.dat",
+ "secondaryFiles": [
+ {
+ "basename": "secondary.dat",
+ "class": "File",
+ "location": "secondary.dat"
+ },
+ {
+ "basename": "secondary2.dat",
+ "class": "File",
+ "location": "secondary2.dat"
+ }
+ ]
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_dir",
+ "doc": ["Doc desc 1", "Doc desc 2"],
+ "type": "Directory"
+ },
+ output: {
+ "output_dir": {
+ "basename": "outdir1",
+ "class": "Directory",
+ "location": "outdir1"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_bool",
+ "type": "boolean"
+ },
+ output: {
+ "output_bool": true
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_int",
+ "type": "int"
+ },
+ output: {
+ "output_int": 1
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_long",
+ "type": "long"
+ },
+ output: {
+ "output_long": 1
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_float",
+ "type": "float"
+ },
+ output: {
+ "output_float": 100.5
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_double",
+ "type": "double"
+ },
+ output: {
+ "output_double": 100.3
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_string",
+ "type": "string"
+ },
+ output: {
+ "output_string": "Hello output"
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_file_array",
+ "type": {
+ "items": "File",
+ "type": "array"
+ }
+ },
+ output: {
+ "output_file_array": [
+ {
+ "basename": "output2.tar",
+ "class": "File",
+ "location": "output2.tar"
+ },
+ {
+ "basename": "output3.tar",
+ "class": "File",
+ "location": "output3.tar"
+ }
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_dir_array",
+ "type": {
+ "items": "Directory",
+ "type": "array"
+ }
+ },
+ output: {
+ "output_dir_array": [
+ {
+ "basename": "outdir2",
+ "class": "Directory",
+ "location": "outdir2"
+ },
+ {
+ "basename": "outdir3",
+ "class": "Directory",
+ "location": "outdir3"
+ }
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_int_array",
+ "type": {
+ "items": "int",
+ "type": "array"
+ }
+ },
+ output: {
+ "output_int_array": [
+ 10,
+ 11,
+ 12
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_long_array",
+ "type": {
+ "items": "long",
+ "type": "array"
+ }
+ },
+ output: {
+ "output_long_array": [
+ 51,
+ 52
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_float_array",
+ "type": {
+ "items": "float",
+ "type": "array"
+ }
+ },
+ output: {
+ "output_float_array": [
+ 100.2,
+ 100.4,
+ 100.6
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_double_array",
+ "type": {
+ "items": "double",
+ "type": "array"
+ }
+ },
+ output: {
+ "output_double_array": [
+ 100.1,
+ 100.2,
+ 100.3
+ ]
+ }
+ },
+ {
+ definition: {
+ "id": "#main/output_string_array",
+ "type": {
+ "items": "string",
+ "type": "array"
+ }
+ },
+ 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);
+ }
+ }
+ 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);
+ }
+ }
+
+
+ });
+ };
+
+ 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);
+ })
+ });
+ };
+
+ 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);
+
+ cy.goToPath(`/collections/${testOutputCollection.uuid}`);
+
+ 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.getCollection(activeUser.token, testOutputCollection.uuid).as('testOutputCollection');
+ });
+
+ // Get updated collection pdh
+ 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) => {
+ 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)), {})
+ };
+ res.body.mounts["/var/lib/cwl/workflow.json"] = {
+ content: {
+ $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,
+ }
+ });
+
+ // 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)), {})
+ });
+
+ // Stub webdav response, points to output json
+ cy.intercept({method: 'PROPFIND', url: '*'}, {
+ fixture: 'webdav-propfind-outputs.xml',
+ });
+ });
+
+ createContainerRequest(
+ activeUser,
+ 'test_container_request',
+ 'arvados/jobs',
+ ['echo', 'hello world'],
+ false, 'Committed')
+ .as('containerRequest');
+
+ cy.getAll('@containerRequest', '@testOutputCollection').then(function([containerRequest, testOutputCollection]) {
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get('[data-cy=process-io-card] h6').contains('Inputs')
+ .parents('[data-cy=process-io-card]').within(() => {
+ verifyIOParameter('input_file', null, "Label Description", 'input1.tar', '00000000000000000000000000000000+01');
+ verifyIOParameter('input_file', null, "Label Description", 'input1-2.txt', undefined, true);
+ verifyIOParameter('input_file', null, "Label Description", 'input1-3.txt', undefined, true);
+ verifyIOParameter('input_file', null, "Label Description", 'input1-4.txt', undefined, true);
+ verifyIOParameter('input_dir', null, "Doc Description", '/', '11111111111111111111111111111111+01');
+ verifyIOParameter('input_bool', null, "Doc desc 1, Doc desc 2", 'true');
+ verifyIOParameter('input_int', null, null, '1');
+ verifyIOParameter('input_long', null, null, '1');
+ verifyIOParameter('input_float', null, null, '1.5');
+ verifyIOParameter('input_double', null, null, '1.3');
+ verifyIOParameter('input_string', null, null, 'Hello World');
+ verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
+ verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
+ verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
+ verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
+ verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
+ verifyIOParameter('input_int_array', null, null, ["1", "3", "5"]);
+ verifyIOParameter('input_long_array', null, null, ["10", "20"]);
+ verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6"]);
+ verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3"]);
+ verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!"]);
+ });
+ 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();
+ const outPdh = testOutputCollection.portable_data_hash;
+
+ verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
+ verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
+ verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'main.dat', `${outPdh}`);
+ verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary.dat', undefined, true);
+ verifyIOParameter('output_file_with_secondary', null, "Doc Description", 'secondary2.dat', undefined, true);
+ verifyIOParameter('output_dir', null, "Doc desc 1, Doc desc 2", 'outdir1', `${outPdh}`);
+ verifyIOParameter('output_bool', null, null, 'true');
+ verifyIOParameter('output_int', null, null, '1');
+ verifyIOParameter('output_long', null, null, '1');
+ verifyIOParameter('output_float', null, null, '100.5');
+ verifyIOParameter('output_double', null, null, '100.3');
+ verifyIOParameter('output_string', null, null, 'Hello output');
+ verifyIOParameter('output_file_array', null, null, 'output2.tar', `${outPdh}`);
+ verifyIOParameter('output_file_array', null, null, 'output3.tar', undefined, true);
+ verifyIOParameter('output_dir_array', null, null, 'outdir2', `${outPdh}`);
+ verifyIOParameter('output_dir_array', null, null, 'outdir3', undefined, true);
+ verifyIOParameter('output_int_array', null, null, ["10", "11", "12"]);
+ verifyIOParameter('output_long_array', null, null, ["51", "52"]);
+ verifyIOParameter('output_float_array', null, null, ["100.2", "100.4", "100.6"]);
+ verifyIOParameter('output_double_array', null, null, ["100.1", "100.2", "100.3"]);
+ verifyIOParameter('output_string_array', null, null, ["Hello", "Output", "!"]);
+ });
+ });
+ });
+
+ it('displays IO parameters with no value', function() {
+
+ const fakeOutputUUID = 'zzzzz-4zz18-abcdefghijklmno';
+ const fakeOutputPDH = '11111111111111111111111111111111+99/';
+
+ cy.loginAs(activeUser);
+
+ // Add output uuid and inputs to container request
+ cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
+ req.reply((res) => {
+ res.body.output_uuid = fakeOutputUUID;
+ res.body.mounts["/var/lib/cwl/cwl.input.json"] = {
+ content: {}
+ };
+ res.body.mounts["/var/lib/cwl/workflow.json"] = {
+ content: {
+ $graph: [{
+ id: "#main",
+ inputs: testInputs.map((input) => (input.definition)),
+ outputs: testOutputs.map((output) => (output.definition))
+ }]
+ }
+ };
+ });
+ });
+
+ // Stub fake output collection
+ cy.intercept({method: 'GET', url: `**/arvados/v1/collections/${fakeOutputUUID}*`}, {
+ statusCode: 200,
+ body: {
+ uuid: fakeOutputUUID,
+ portable_data_hash: fakeOutputPDH,
+ }
+ });
+
+ // Stub fake output json
+ cy.intercept({method: 'GET', url: `**/c%3D${fakeOutputUUID}/cwl.output.json`}, {
+ statusCode: 200,
+ body: {}
+ });
+
+ cy.readFile('cypress/fixtures/webdav-propfind-outputs.xml').then((data) => {
+ // Stub webdav response, points to output json
+ cy.intercept({method: 'PROPFIND', url: '*'}, {
+ statusCode: 200,
+ body: data.replace(/zzzzz-4zz18-zzzzzzzzzzzzzzz/g, fakeOutputUUID)
+ });
+ });
+
+ createContainerRequest(
+ activeUser,
+ 'test_container_request',
+ 'arvados/jobs',
+ ['echo', 'hello world'],
+ false, 'Committed')
+ .as('containerRequest');
+
+ cy.getAll('@containerRequest').then(function([containerRequest]) {
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get('[data-cy=process-io-card] h6').contains('Inputs')
+ .parents('[data-cy=process-io-card]').within(() => {
+ cy.wait(2000);
+ cy.waitForDom();
+ cy.get('tbody tr').each((item) => {
+ cy.wrap(item).contains('No value');
+ });
+ });
+ cy.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');
+ });
+ });
+ });
+ });
+
});
"lodash.template": "4.5.0",
"material-ui-pickers": "^2.2.4",
"mem": "4.0.0",
+ "mime": "^3.0.0",
"moment": "2.29.1",
"parse-duration": "0.4.4",
"prop-types": "15.7.2",
data
})
+ get = (url: string, config: WebDAVRequestConfig = {}) =>
+ this.request({
+ ...config, url,
+ method: 'GET'
+ })
+
upload = (url: string, files: File[], config: WebDAVRequestConfig = {}) => {
return Promise.all(
files.map(file => this.request({
Object.assign(window, { cancelTokens: {} });
}
- (window as any).cancelTokens[config.url] = () => {
+ (window as any).cancelTokens[config.url] = () => {
resolve(r);
r.abort();
}
headers?: { [key: string]: string };
data?: any;
onUploadProgress?: (event: ProgressEvent) => void;
-}
\ No newline at end of file
+}
marginTop: '-15px',
},
pathPanel: {
- padding: '1rem',
- marginBottom: '1rem',
+ 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%)',
},
},
leftPanel: {
flex: 0,
- padding: '1rem',
+ padding: '0 1rem 1rem',
marginRight: '1rem',
whiteSpace: 'nowrap',
position: 'relative',
rightPanel: {
flex: '50%',
padding: '1rem',
- paddingTop: '2rem',
- marginTop: '-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%)',
messages: string[];
filtersApplied?: boolean;
classMessage?: string;
- icon: IconType;
+ icon?: IconType;
classIcon?: string;
}
export const DefaultView = withStyles(styles)(
({ classes, classRoot, messages, classMessage, icon: Icon, classIcon }: DefaultViewProps) =>
<Typography className={classnames([classes.root, classRoot])} component="div">
- <Icon className={classnames([classes.icon, classIcon])} />
+ {Icon && <Icon className={classnames([classes.icon, classIcon])} />}
{messages.map((msg: string, index: number) => {
return <Typography key={index}
className={classnames([classes.message, classMessage])}>{msg}</Typography>;
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { Badge, Tooltip } from '@material-ui/core';
+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 HelpOutline from '@material-ui/icons/HelpOutline';
import History from '@material-ui/icons/History';
import Inbox from '@material-ui/icons/Inbox';
+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 Menu from '@material-ui/icons/Menu';
import MoreVert from '@material-ui/icons/MoreVert';
import Mail from '@material-ui/icons/Mail';
-import MoveToInbox from '@material-ui/icons/MoveToInbox';
import Notifications from '@material-ui/icons/Notifications';
import OpenInNew from '@material-ui/icons/OpenInNew';
import People from '@material-ui/icons/People';
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 FontAwesome icons
import { library } from '@fortawesome/fontawesome-svg-core';
</Badge>
</Tooltip>;
+// https://materialdesignicons.com/icon/image-off
+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>;
+
+// https://materialdesignicons.com/icon/inbox-arrow-up
+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>;
+
export type IconType = React.SFC<{ className?: string, style?: object }>;
export const AddIcon: IconType = (props) => <Add {...props} />;
export const HelpOutlineIcon: IconType = (props) => <HelpOutline {...props} />;
export const ImportContactsIcon: IconType = (props) => <ImportContacts {...props} />;
export const InfoIcon: IconType = (props) => <Info {...props} />;
-export const InputIcon: IconType = (props) => <InsertDriveFile {...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 NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
export const NotificationIcon: IconType = (props) => <Notifications {...props} />;
export const OpenIcon: IconType = (props) => <OpenInNew {...props} />;
-export const OutputIcon: IconType = (props) => <MoveToInbox {...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 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} />;
import { Resource, ResourceKind } from "./resource";
import { safeLoad } from 'js-yaml';
+import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
export interface WorkflowResource extends Resource {
kind: ResourceKind.WORKFLOW;
: undefined;
};
+export const getWorkflowOutputs = (workflowDefinition: WorkflowResourceDefinition) => {
+ if (!workflowDefinition) { return undefined; }
+ return getWorkflow(workflowDefinition)
+ ? getWorkflow(workflowDefinition)!.outputs
+ : undefined;
+};
+
export const getInputLabel = (input: CommandInputParameter) => {
return `${input.label || input.id.split('/').pop()}`;
};
+export const getIOParamId = (input: CommandInputParameter | CommandOutputParameter) => {
+ return `${input.id.split('/').pop()}`;
+};
+
export const isRequiredInput = ({ type }: CommandInputParameter) => {
if (type instanceof Array) {
for (const t of type) {
};
}
+ async getFileContents(file: CollectionFile) {
+ return (await this.webdavClient.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('//', '/');
const requestConfig = {
// SPDX-License-Identifier: AGPL-3.0
import { unionize, ofType, UnionOf } from "common/unionize";
-import { loadProcess } from 'store/processes/processes-actions';
+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 { showWorkflowDetails } from 'store/workflow-panel/workflow-panel-actions';
import { loadSubprocessPanel } 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 } from 'models/workflow';
+import { getIOParamDisplayValue, ProcessIOParameter } from "views/process-panel/process-io-card";
+import { OutputDetails } from "./process-panel";
+import { AuthState } from "store/auth/auth-reducer";
export const processPanelActions = unionize({
+ RESET_PROCESS_PANEL: ofType<{}>(),
SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID: ofType<string>(),
SET_PROCESS_PANEL_FILTERS: ofType<string[]>(),
TOGGLE_PROCESS_PANEL_FILTER: ofType<string>(),
+ SET_INPUT_RAW: ofType<CommandInputParameter[] | null>(),
+ SET_INPUT_PARAMS: ofType<ProcessIOParameter[] | null>(),
+ SET_OUTPUT_RAW: ofType<OutputDetails | null>(),
+ SET_OUTPUT_DEFINITIONS: ofType<CommandOutputParameter[]>(),
+ SET_OUTPUT_PARAMS: ofType<ProcessIOParameter[] | null>(),
});
export type ProcessPanelAction = UnionOf<typeof processPanelActions>;
export const loadProcessPanel = (uuid: string) =>
async (dispatch: Dispatch) => {
+ 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));
}
};
+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) => {
+ const noOutputs = {rawOutputs: {}};
+ if (!containerRequest.outputUuid) {
+ dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW(noOutputs));
+ return;
+ };
+ try {
+ const propsOutputs = getRawOutputs(containerRequest);
+ const filesPromise = services.collectionService.files(containerRequest.outputUuid);
+ const collectionPromise = services.collectionService.get(containerRequest.outputUuid);
+ const [files, collection] = await Promise.all([filesPromise, collectionPromise]);
+
+ // If has propsOutput, skip fetching cwl.output.json
+ if (propsOutputs !== undefined) {
+ 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;
+ 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,
+ }));
+ } else {
+ dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW(noOutputs));
+ }
+ }
+ } catch {
+ dispatch<ProcessPanelAction>(processPanelActions.SET_OUTPUT_RAW(noOutputs));
+ }
+ };
+
+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;
+
+ if (outputRaw !== null && 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);
ProcessStatus.WARNING,
ProcessStatus.CANCELLED
]);
+
+const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
+ return inputs.map(input => {
+ return {
+ id: getIOParamId(input),
+ label: input.label || "",
+ value: getIOParamDisplayValue(auth, input)
+ };
+ });
+};
+
+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)
+ };
+ });
+};
const initialState: ProcessPanel = {
containerRequestUuid: "",
- filters: {}
+ filters: {},
+ inputRaw: null,
+ inputParams: null,
+ outputRaw: null,
+ outputDefinitions: [],
+ outputParams: null,
};
export const processPanelReducer = (state = initialState, action: ProcessPanelAction): ProcessPanel =>
processPanelActions.match(action, {
+ RESET_PROCESS_PANEL: () => initialState,
SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID: containerRequestUuid => ({
...state, containerRequestUuid
}),
const filters = { ...state.filters, [status]: !state.filters[status] };
return { ...state, filters };
},
+ SET_INPUT_RAW: inputRaw => {
+ // Since mounts can disappear and reappear, only set inputs
+ // if current state is null or new inputs has content
+ if (state.inputRaw === null || (inputRaw && inputRaw.length)) {
+ return { ...state, inputRaw };
+ } else {
+ return state;
+ }
+ },
+ SET_INPUT_PARAMS: inputParams => {
+ // Since mounts can disappear and reappear, only set inputs
+ // if current state is null or new inputs has content
+ if (state.inputParams === null || (inputParams && inputParams.length)) {
+ return { ...state, inputParams };
+ } else {
+ return state;
+ }
+ },
+ SET_OUTPUT_RAW: outputRaw => {
+ return { ...state, outputRaw };
+ },
+ SET_OUTPUT_DEFINITIONS: outputDefinitions => {
+ // Set output definitions is only additive to avoid clearing when mounts go temporarily missing
+ if (outputDefinitions.length) {
+ return { ...state, outputDefinitions }
+ } else {
+ return state;
+ }
+ },
+ SET_OUTPUT_PARAMS: outputParams => {
+ return { ...state, outputParams };
+ },
default: () => state,
});
//
// SPDX-License-Identifier: AGPL-3.0
+import { CommandInputParameter } from 'models/workflow';
import { RouterState } from "react-router-redux";
import { matchProcessRoute } from "routes/routes";
+import { ProcessIOParameter } from "views/process-panel/process-io-card";
+import { CommandOutputParameter } from 'cwlts/mappings/v1.0/CommandOutputParameter';
+
+export type OutputDetails = {
+ rawOutputs?: any;
+ pdh?: string;
+}
export interface ProcessPanel {
containerRequestUuid: string;
filters: { [status: string]: boolean };
+ inputRaw: CommandInputParameter[] | null;
+ inputParams: ProcessIOParameter[] | null;
+ outputRaw: OutputDetails | null;
+ outputDefinitions: CommandOutputParameter[];
+ outputParams: ProcessIOParameter[] | null;
}
export const getProcessPanelCurrentUuid = (router: RouterState) => {
const pathname = router.location ? router.location.pathname : '';
const match = matchProcessRoute(pathname);
return match ? match.params.id : undefined;
-};
\ No newline at end of file
+};
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 { getWorkflow, getWorkflowInputs } from "models/workflow";
+import { CommandInputParameter, getWorkflow, getWorkflowInputs, getWorkflowOutputs } from "models/workflow";
import { ProjectResource } from "models/project";
import { UserResource } from "models/user";
+import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
export const loadProcess = (containerRequestUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
const containerRequest = await services.containerRequestService.get(containerRequestUuid);
dispatch<any>(updateResources([containerRequest]));
+ if (containerRequest.outputUuid) {
+ const collection = await services.collectionService.get(containerRequest.outputUuid);
+ dispatch<any>(updateResources([collection]));
+ }
+
if (containerRequest.containerUuid) {
const container = await services.containerService.get(containerRequest.containerUuid);
dispatch<any>(updateResources([container]));
}
};
-const getInputs = (data: any) => {
+/*
+ * Fetches raw inputs from containerRequest mounts with fallback to properties
+ * Returns undefined if containerRequest not loaded
+ * Returns [] if inputs not found in mounts or props
+ */
+export const getRawInputs = (data: any): CommandInputParameter[] | 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);
+}
+
+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 []; }
+ const content = getRawInputs(data) as any;
+ 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: data.mounts[MOUNT_PATH_CWL_INPUT].content[it.id],
+ default: content[it.id],
+ value: content[it.id.split('/').pop()] || [],
+ doc: it.doc
+ }
+ )
+ ) : [];
+};
+
+/*
+ * Fetches raw outputs from containerRequest properties
+ * 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);
+}
+
+export type InputCollectionMount = {
+ path: string;
+ pdh: string;
+}
+
+export const getInputCollectionMounts = (data: any): InputCollectionMount[] => {
+ 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)
+ .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 []; }
+ 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
}
)
: <Typography>head version</Typography>
);
+export const CollectionName = connect((state: RootState, props: { uuid: string, className?: string }) => {
+ return {
+ collection: getResource<CollectionResource>(props.uuid)(state.resources),
+ uuid: props.uuid,
+ className: props.className,
+ };
+})((props: { collection: CollectionResource, uuid: string, className?: string }) =>
+ <Typography className={props.className}>{props.collection?.name || props.uuid}</Typography>
+);
+
export const ProcessStatus = compose(
connect((state: RootState, props: { uuid: string }) => {
return { process: getProcess(props.uuid)(state.resources) };
import { ProjectResource } from "models/project";
import { treePickerActions } from "store/tree-picker/tree-picker-actions";
import { ListItemTextIcon } from "components/list-item-text-icon/list-item-text-icon";
-import { ProjectIcon, InputIcon, IconType, CollectionIcon } from 'components/icon/icon';
+import { ProjectIcon, FileInputIcon, IconType, CollectionIcon } from 'components/icon/icon';
import { loadProject, loadCollection } from 'store/tree-picker/tree-picker-actions';
import { GroupContentsResource } from 'services/groups-service/groups-service';
import { CollectionDirectory, CollectionFile, CollectionFileType } from 'models/collection-file';
} else if ('type' in data) {
switch (data.type) {
case CollectionFileType.FILE:
- return InputIcon;
+ return FileInputIcon;
default:
return ProjectIcon;
}
import { resourceLabel } from "common/labels";
import { DetailsAttribute } from "components/details-attribute/details-attribute";
import { ResourceKind } from "models/resource";
-import { ContainerRunTime, ResourceWithName } from "views-components/data-explorer/renderers";
+import { CollectionName, ContainerRunTime, ResourceWithName } from "views-components/data-explorer/renderers";
import { getProcess, getProcessStatus } from "store/processes/process";
import { RootState } from "store/store";
import { connect } from "react-redux";
import { ProcessResource } from "models/process";
import { ContainerResource } from "models/container";
-import { openProcessInputDialog } from "store/processes/process-input-actions";
import { navigateToOutput, openWorkflow } from "store/process-panel/process-panel-actions";
import { ArvadosTheme } from "common/custom-theme";
import { ProcessRuntimeStatus } from "views-components/process-runtime-status/process-runtime-status";
};
interface ProcessDetailsAttributesActionProps {
- openProcessInputDialog: (uuid: string) => void;
navigateToOutput: (uuid: string) => void;
openWorkflow: (uuid: string) => void;
}
const mapDispatchToProps = (dispatch: Dispatch): ProcessDetailsAttributesActionProps => ({
- openProcessInputDialog: (uuid) => dispatch<any>(openProcessInputDialog(uuid)),
navigateToOutput: (uuid) => dispatch<any>(navigateToOutput(uuid)),
openWorkflow: (uuid) => dispatch<any>(openWorkflow(uuid)),
});
const container = props.container;
const classes = props.classes;
const mdSize = props.twoCol ? 6 : 12;
+ const filteredPropertyKeys = Object.keys(containerRequest.properties)
+ .filter(k => (typeof containerRequest.properties[k] !== 'object'));
return <Grid container>
<Grid item xs={12}>
<ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} containerCount={containerRequest.containerCount} />
<DetailsAttribute label='Requesting Container UUID' value={containerRequest.requestingContainerUuid || "(none)"} />
</Grid>
<Grid item xs={6}>
- <span onClick={() => props.navigateToOutput(containerRequest.outputUuid!)}>
- <DetailsAttribute classLabel={classes.link} label='Outputs' />
- </span>
- <span onClick={() => props.openProcessInputDialog(containerRequest.uuid)}>
- <DetailsAttribute classLabel={classes.link} label='Inputs' />
- </span>
+ <DetailsAttribute label='Output Collection' />
+ {containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest.outputUuid!)}>
+ <CollectionName className={classes.link} uuid={containerRequest.outputUuid} />
+ </span>}
</Grid>
{containerRequest.properties.template_uuid &&
<Grid item xs={12} md={mdSize}>
*/}
<Grid item xs={12} md={12}>
<DetailsAttribute label='Properties' />
- {Object.keys(containerRequest.properties).length > 0
- ? Object.keys(containerRequest.properties).map(k =>
+ {filteredPropertyKeys.length > 0
+ ? filteredPropertyKeys.map(k =>
Array.isArray(containerRequest.properties[k])
? containerRequest.properties[k].map((v: string) =>
getPropertyChip(k, v, undefined, classes.propertyTag))
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { ReactElement, useState } from 'react';
+import { Dispatch } from 'redux';
+import {
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ Card,
+ CardHeader,
+ IconButton,
+ CardContent,
+ Tooltip,
+ Typography,
+ Tabs,
+ Tab,
+ Table,
+ TableHead,
+ TableBody,
+ TableRow,
+ TableCell,
+ Paper,
+ Grid,
+ Chip,
+ CircularProgress,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon, ImageIcon, InputIcon, ImageOffIcon, OutputIcon, MaximizeIcon } from 'components/icon/icon';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import {
+ BooleanCommandInputParameter,
+ CommandInputParameter,
+ CWLType,
+ Directory,
+ DirectoryArrayCommandInputParameter,
+ DirectoryCommandInputParameter,
+ EnumCommandInputParameter,
+ FileArrayCommandInputParameter,
+ FileCommandInputParameter,
+ FloatArrayCommandInputParameter,
+ FloatCommandInputParameter,
+ IntArrayCommandInputParameter,
+ IntCommandInputParameter,
+ isArrayOfType,
+ isPrimitiveOfType,
+ StringArrayCommandInputParameter,
+ StringCommandInputParameter,
+} 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';
+
+type CssRules =
+ | "card"
+ | "content"
+ | "title"
+ | "header"
+ | "avatar"
+ | "iconHeader"
+ | "tableWrapper"
+ | "tableRoot"
+ | "paramValue"
+ | "keepLink"
+ | "collectionLink"
+ | "imagePreview"
+ | "valArray"
+ | "secondaryVal"
+ | "secondaryRow"
+ | "emptyValue"
+ | "noBorderRow"
+ | "symmetricTabs"
+ | "imagePlaceholder"
+ | "rowWithPreview"
+ | "labelColumn";
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ card: {
+ height: '100%'
+ },
+ header: {
+ paddingTop: theme.spacing.unit,
+ paddingBottom: 0,
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700,
+ },
+ avatar: {
+ 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': {
+ paddingBottom: theme.spacing.unit * 1,
+ }
+ },
+ title: {
+ overflow: 'hidden',
+ paddingTop: theme.spacing.unit * 0.5
+ },
+ tableWrapper: {
+ height: `calc(100% - ${theme.spacing.unit * 6}px)`,
+ overflow: 'auto',
+ },
+ tableRoot: {
+ width: '100%',
+ '& thead th': {
+ verticalAlign: 'bottom',
+ paddingBottom: '10px',
+ },
+ '& td, & th': {
+ paddingRight: '25px',
+ }
+ },
+ paramValue: {
+ display: 'flex',
+ alignItems: 'flex-start',
+ flexDirection: 'column',
+ },
+ keepLink: {
+ color: theme.palette.primary.main,
+ textDecoration: 'none',
+ overflowWrap: 'break-word',
+ cursor: 'pointer',
+ },
+ collectionLink: {
+ margin: '10px',
+ '& a': {
+ color: theme.palette.primary.main,
+ textDecoration: 'none',
+ overflowWrap: 'break-word',
+ cursor: 'pointer',
+ }
+ },
+ imagePreview: {
+ maxHeight: '15em',
+ maxWidth: '15em',
+ marginBottom: theme.spacing.unit,
+ },
+ valArray: {
+ display: 'flex',
+ gap: '10px',
+ flexWrap: 'wrap',
+ '& span': {
+ display: 'inline',
+ }
+ },
+ secondaryVal: {
+ paddingLeft: '20px',
+ },
+ secondaryRow: {
+ height: '29px',
+ verticalAlign: 'top',
+ position: 'relative',
+ top: '-9px',
+ },
+ emptyValue: {
+ color: theme.customs.colors.grey500,
+ },
+ noBorderRow: {
+ '& td': {
+ borderBottom: 'none',
+ }
+ },
+ symmetricTabs: {
+ '& button': {
+ flexBasis: '0',
+ }
+ },
+ imagePlaceholder: {
+ width: '60px',
+ height: '60px',
+ display: 'flex',
+ alignItems: 'center',
+ justifyContent: 'center',
+ backgroundColor: '#cecece',
+ borderRadius: '10px',
+ },
+ rowWithPreview: {
+ verticalAlign: 'bottom',
+ },
+ labelColumn: {
+ minWidth: '120px',
+ },
+});
+
+export enum ProcessIOCardType {
+ INPUT = 'Inputs',
+ OUTPUT = 'Outputs',
+}
+export interface ProcessIOCardDataProps {
+ process: Process;
+ label: ProcessIOCardType;
+ params: ProcessIOParameter[] | null;
+ raw: any;
+ mounts?: InputCollectionMount[];
+ outputUuid?: string;
+}
+
+export interface ProcessIOCardActionProps {
+ navigateTo: (uuid: string) => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch): ProcessIOCardActionProps => ({
+ 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, panelMaximized, panelName, process, navigateTo }: ProcessIOCardProps) => {
+ const [mainProcTabState, setMainProcTabState] = useState(0);
+ const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ setMainProcTabState(value);
+ }
+
+ const [showImagePreview, setShowImagePreview] = useState(false);
+
+ const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
+ const mainProcess = !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);
+
+ 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> }
+ { doMaximizePanel && !panelMaximized &&
+ <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
+ </Tooltip> }
+ { doHidePanel &&
+ <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+ </Tooltip> }
+ </div>
+ } />
+ <CardContent className={classes.content}>
+ {mainProcess ?
+ (<>
+ {/* 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" />}
+ <Tab label="JSON" />
+ </Tabs>
+ {(mainProcTabState === 0 && params && hasParams) && <div className={classes.tableWrapper}>
+ <ProcessIOPreview data={params} showImagePreview={showImagePreview} />
+ </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
+ (<>
+ {((mounts && mounts.length) || outputUuid) ?
+ <>
+ <Tabs value={0} variant="fullWidth" className={classes.symmetricTabs}>
+ {label === ProcessIOCardType.INPUT && <Tab label="Collections" />}
+ {label === ProcessIOCardType.OUTPUT && <Tab label="Collection" />}
+ </Tabs>
+ <div className={classes.tableWrapper}>
+ {label === ProcessIOCardType.INPUT && <ProcessInputMounts mounts={mounts || []} />}
+ {label === ProcessIOCardType.OUTPUT && <>
+ {outputUuid && <Typography className={classes.collectionLink}>
+ Output Collection: <MuiLink className={classes.keepLink} onClick={() => {navigateTo(outputUuid || "")}}>
+ {outputUuid}
+ </MuiLink></Typography>}
+ <ProcessOutputCollectionFiles isWritable={false} currentItemUuid={outputUuid} />
+ </>}
+ </div>
+ </> :
+ <Grid container item alignItems='center' justify='center'>
+ <DefaultView messages={["No collection(s) found"]} />
+ </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[];
+ showImagePreview: boolean;
+}
+
+type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
+
+const ProcessIOPreview = withStyles(styles)(
+ ({ classes, data, showImagePreview }: 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>Value</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 <>
+ <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)}>
+ <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>
+ })}
+ </>;
+ })}
+ </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>
+)
+
+interface ProcessIORawDataProps {
+ data: ProcessIOParameter[];
+}
+
+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>
+ </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: <pre>{String(boolValue)}</pre> }] :
+ [{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: <pre>{String(intValue)}</pre> }]
+ : [{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: <pre>{String(floatValue)}</pre> }]:
+ [{display: <EmptyValue />}];
+
+ case isPrimitiveOfType(input, CWLType.STRING):
+ const stringValue = (input as StringCommandInputParameter).value || undefined;
+
+ return stringValue !== undefined &&
+ !(Array.isArray(stringValue) && stringValue.length === 0) ?
+ [{display: <pre>{stringValue}</pre> }] :
+ [{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 mainFilePdhUrl = mainFile ? getResourcePdhUrl(mainFile, pdh) : "";
+
+ 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 />}];
+
+ case typeof input.type === 'object' &&
+ !(input.type instanceof Array) &&
+ input.type.type === 'enum':
+ const enumValue = (input as EnumCommandInputParameter).value;
+
+ return enumValue !== undefined ?
+ [{ display: <pre>{(input as EnumCommandInputParameter).value || ''}</pre> }] :
+ [{display: <EmptyValue />}];
+
+ case isArrayOfType(input, CWLType.STRING):
+ const strArray = (input as StringArrayCommandInputParameter).value || [];
+ return strArray.length ?
+ [{ display: <>{((input as StringArrayCommandInputParameter).value || []).map((val) => <Chip label={val} />)}</> }] :
+ [{display: <EmptyValue />}];
+
+ case isArrayOfType(input, CWLType.INT):
+ case isArrayOfType(input, CWLType.LONG):
+ const intArray = (input as IntArrayCommandInputParameter).value || [];
+
+ return intArray.length ?
+ [{ display: <>{((input as IntArrayCommandInputParameter).value || []).map((val) => <Chip label={val} />)}</> }] :
+ [{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) => <Chip label={val} />)}</> }] :
+ [{display: <EmptyValue />}];
+
+ case isArrayOfType(input, CWLType.FILE):
+ const fileArrayMainFiles = ((input as FileArrayCommandInputParameter).value || []);
+ const firstMainFilePdh = (fileArrayMainFiles.length > 0 && fileArrayMainFiles[0]) ? getResourcePdhUrl(fileArrayMainFiles[0], pdh) : "";
+
+ // Convert each main file into separate arrays of ProcessIOValue to preserve secondaryFile grouping
+ const fileArrayValues = fileArrayMainFiles.map((mainFile: File, i): ProcessIOValue[] => {
+ const secondaryFiles = ((mainFile as unknown) as FileWithSecondaryFiles)?.secondaryFiles || [];
+ return [
+ // Pass firstMainFilePdh to secondary files and every main file besides the first to hide pdh if equal
+ ...(mainFile ? [fileToProcessIOValue(mainFile, false, auth, pdh, i > 0 ? firstMainFilePdh : "")] : []),
+ ...(secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh)))
+ ];
+ // Reduce each mainFile/secondaryFile group into single array preserving ordering
+ }).reduce((acc: ProcessIOValue[], mainFile: ProcessIOValue[]) => (acc.concat(mainFile)), []);
+
+ 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 />}];
+
+ default:
+ return [];
+ }
+};
+
+/*
+ * @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 || '';
+};
+
+interface KeepUrlProps {
+ auth: AuthState;
+ res: File | Directory;
+ pdh?: string;
+}
+
+const getResourcePdhUrl = (res: File | Directory, pdh?: string): string => {
+ const keepUrl = getKeepUrl(res, pdh);
+ 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> :
+ <></>;
+});
+
+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 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 />;
+});
+
+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));
+};
+
+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);
+};
+
+const isFileImage = (basename?: string): boolean => {
+ return basename ? (mime.getType(basename) || "").startsWith('image/') : false;
+};
+
+const normalizeDirectoryLocation = (directory: Directory): Directory => {
+ if (!directory.location) {
+ return directory;
+ }
+ return {
+ ...directory,
+ location: (directory.location || '').endsWith('/') ? directory.location : directory.location + '/',
+ };
+};
+
+const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: string): ProcessIOValue => {
+ const normalizedDirectory = normalizeDirectoryLocation(directory);
+ return {
+ 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 => {
+ const resourcePdh = getResourcePdhUrl(file, pdh);
+ return {
+ 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}/> : <></>,
+ }
+};
+
+const EmptyValue = withStyles(styles)(
+ ({classes}: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>
+);
+
+const ImagePlaceholder = withStyles(styles)(
+ ({classes}: WithStyles<CssRules>) => <span className={classes.imagePlaceholder}><ImageIcon /></span>
+);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import {
+ CollectionPanelFiles as Component,
+ CollectionPanelFilesProps
+} from "components/collection-panel-files/collection-panel-files";
+import { Dispatch } from "redux";
+import { collectionPanelFilesAction } from "store/collection-panel/collection-panel-files/collection-panel-files-actions";
+import { ContextMenuKind } from "views-components/context-menu/context-menu";
+import { openContextMenu, openCollectionFilesContextMenu } from 'store/context-menu/context-menu-actions';
+import { openUploadCollectionFilesDialog } from 'store/collections/collection-upload-actions';
+import { ResourceKind } from "models/resource";
+import { openDetailsPanel } from 'store/details-panel/details-panel-action';
+
+const mapDispatchToProps = (dispatch: Dispatch): Pick<CollectionPanelFilesProps, 'onSearchChange' | 'onFileClick' | 'onUploadDataClick' | 'onCollapseToggle' | 'onSelectionToggle' | 'onItemMenuOpen' | 'onOptionsMenuOpen'> => ({
+ onUploadDataClick: (targetLocation?: string) => {
+ dispatch<any>(openUploadCollectionFilesDialog(targetLocation));
+ },
+ onCollapseToggle: (id) => {
+ dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_COLLAPSE({ id }));
+ },
+ onSelectionToggle: (event, item) => {
+ dispatch(collectionPanelFilesAction.TOGGLE_COLLECTION_FILE_SELECTION({ id: item.id }));
+ },
+ onItemMenuOpen: (event, item, isWritable) => {
+ const isDirectory = item.data.type === 'directory';
+ dispatch<any>(openContextMenu(
+ event,
+ {
+ menuKind: isWritable
+ ? isDirectory
+ ? ContextMenuKind.COLLECTION_DIRECTORY_ITEM
+ : ContextMenuKind.COLLECTION_FILE_ITEM
+ : isDirectory
+ ? ContextMenuKind.READONLY_COLLECTION_DIRECTORY_ITEM
+ : ContextMenuKind.READONLY_COLLECTION_FILE_ITEM,
+ kind: ResourceKind.COLLECTION,
+ name: item.data.name,
+ uuid: item.id,
+ ownerUuid: ''
+ }
+ ));
+ },
+ onSearchChange: (searchValue: string) => {
+ dispatch(collectionPanelFilesAction.ON_SEARCH_CHANGE(searchValue));
+ },
+ onOptionsMenuOpen: (event, isWritable) => {
+ dispatch<any>(openCollectionFilesContextMenu(event, isWritable));
+ },
+ onFileClick: (id) => {
+ dispatch<any>(openDetailsPanel(id));
+ },
+});
+
+export const ProcessOutputCollectionFiles = connect(null, mapDispatchToProps)(Component);
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 { 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 { CommandInputParameter } 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 } from 'store/process-panel/process-panel';
type CssRules = 'root';
subprocesses: Array<Process>;
filters: Array<SubprocessFilterDataProps>;
processLogsPanel: ProcessLogsPanel;
+ auth: AuthState;
+ inputRaw: CommandInputParameter[] | null;
+ inputParams: ProcessIOParameter[] | null;
+ outputRaw: OutputDetails | null;
+ outputDefinitions: CommandOutputParameter[];
+ outputParams: ProcessIOParameter[] | null;
}
export interface ProcessPanelRootActionProps {
onLogFilterChange: (filter: FilterOption) => void;
navigateToLog: (uuid: string) => void;
onCopyToClipboard: (uuid: string) => void;
+ loadInputs: (containerRequest: ContainerRequestResource) => void;
+ loadOutputs: (containerRequest: ContainerRequestResource) => void;
+ loadOutputDefinitions: (containerRequest: ContainerRequestResource) => void;
+ updateOutputParams: () => void;
}
export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
{name: "Details"},
{name: "Command"},
{name: "Logs", visible: true},
+ {name: "Inputs"},
+ {name: "Outputs"},
{name: "Subprocesses"},
];
export const ProcessPanelRoot = withStyles(styles)(
- ({ process, processLogsPanel, ...props }: ProcessPanelRootProps) =>
- process
+ ({
+ process,
+ auth,
+ processLogsPanel,
+ inputRaw,
+ inputParams,
+ outputRaw,
+ outputDefinitions,
+ outputParams,
+ loadInputs,
+ loadOutputs,
+ loadOutputDefinitions,
+ updateOutputParams,
+ ...props
+ }: ProcessPanelRootProps) => {
+
+ const outputUuid = process?.containerRequest.outputUuid;
+ const containerRequest = process?.containerRequest;
+ const inputMounts = getInputCollectionMounts(process?.containerRequest);
+
+ React.useEffect(() => {
+ if (containerRequest) {
+ // Load inputs from mounts or props
+ loadInputs(containerRequest);
+ // Fetch raw output (loads from props or keep)
+ loadOutputs(containerRequest);
+ // Loads output definitions from mounts into store
+ loadOutputDefinitions(containerRequest);
+ }
+ }, [containerRequest, loadInputs, loadOutputs, loadOutputDefinitions]);
+
+ // Trigger processing output params when raw or definitions change
+ React.useEffect(() => {
+ 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">
<ProcessDetailsCard
navigateToLog={props.navigateToLog}
/>
</MPVPanelContent>
+ <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-inputs">
+ <ProcessIOCard
+ label={ProcessIOCardType.INPUT}
+ process={process}
+ params={inputParams}
+ raw={inputRaw}
+ mounts={inputMounts}
+ />
+ </MPVPanelContent>
+ <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-outputs">
+ <ProcessIOCard
+ label={ProcessIOCardType.OUTPUT}
+ process={process}
+ params={outputParams}
+ raw={outputRaw?.rawOutputs}
+ outputUuid={outputUuid || ""}
+ />
+ </MPVPanelContent>
<MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-children">
<SubprocessPanel />
</MPVPanelContent>
<DefaultView
icon={ProcessIcon}
messages={['Process not found']} />
- </Grid>);
+ </Grid>;
+ }
+);
} from 'store/process-panel/process-panel';
import { groupBy } from 'lodash';
import {
+ loadInputs,
+ loadOutputDefinitions,
+ loadOutputs,
toggleProcessPanelFilter,
+ updateOutputParams,
} from 'store/process-panel/process-panel-actions';
import { cancelRunningWorkflow } from 'store/processes/processes-actions';
import { navigateToLogCollection, setProcessLogsPanelFilter } from 'store/process-logs-panel/process-logs-panel-actions';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
-const mapStateToProps = ({ router, resources, processPanel, processLogsPanel }: RootState): ProcessPanelRootDataProps => {
+const mapStateToProps = ({ router, auth, resources, processPanel, processLogsPanel }: RootState): ProcessPanelRootDataProps => {
const uuid = getProcessPanelCurrentUuid(router) || '';
const subprocesses = getSubprocesses(uuid)(resources);
return {
subprocesses: subprocesses.filter(subprocess => processPanel.filters[getProcessStatus(subprocess)]),
filters: getFilters(processPanel, subprocesses),
processLogsPanel: processLogsPanel,
+ auth: auth,
+ inputRaw: processPanel.inputRaw,
+ inputParams: processPanel.inputParams,
+ outputRaw: processPanel.outputRaw,
+ outputDefinitions: processPanel.outputDefinitions,
+ outputParams: processPanel.outputParams,
};
};
cancelProcess: (uuid) => dispatch<any>(cancelRunningWorkflow(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())
});
const getFilters = (processPanel: ProcessPanelState, processes: Process[]) => {
CollectionVersioning: true
PreserveVersionIfIdle: -1s
BlobSigningKey: zfhgfenhffzltr9dixws36j1yhksjoll2grmku38mi7yxd66h5j4q9w4jzanezacp8s6q0ro3hxakfye02152hncy6zml2ed0uc
- TrustAllContent: false
+ TrustAllContent: true
ForwardSlashNameSubstitution: /
ManagedProperties:
original_owner_uuid: {Function: original_owner, Protected: true}
lodash.template: 4.5.0
material-ui-pickers: ^2.2.4
mem: 4.0.0
+ mime: ^3.0.0
moment: 2.29.1
node-sass: ^4.9.4
node-sass-chokidar: 1.5.0
languageName: node
linkType: hard
+"mime@npm:^3.0.0":
+ version: 3.0.0
+ resolution: "mime@npm:3.0.0"
+ bin:
+ mime: cli.js
+ checksum: f43f9b7bfa64534e6b05bd6062961681aeb406a5b53673b53b683f27fcc4e739989941836a355eef831f4478923651ecc739f4a5f6e20a76487b432bfd4db928
+ languageName: node
+ linkType: hard
+
"mimic-fn@npm:^1.0.0":
version: 1.2.0
resolution: "mimic-fn@npm:1.2.0"