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, doc, val) => {
+ cy.get('table tr').contains(name).parents('tr').within(() => {
+ doc && cy.contains(doc);
+ if (val) {
+ if (Array.isArray(val)) {
+ val.forEach(v => cy.contains(v));
+ } else {
+ cy.contains(val);
+ }
+ }
+ });
+ };
+
+ 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(() => {
+ cy.wait(2000);
+ cy.waitForDom();
+ verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1.tar');
+ verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1-2.txt');
+ verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1-3.txt');
+ verifyIOParameter('input_file', "Label Description", 'keep:00000000000000000000000000000000+01/input1-4.txt');
+ verifyIOParameter('input_dir', "Doc Description", 'keep:11111111111111111111111111111111+01/');
+ verifyIOParameter('input_bool', "Doc desc 1, Doc desc 2", 'true');
+ verifyIOParameter('input_int', null, '1');
+ verifyIOParameter('input_long', null, '1');
+ verifyIOParameter('input_float', null, '1.5');
+ verifyIOParameter('input_double', null, '1.3');
+ verifyIOParameter('input_string', null, 'Hello World');
+ verifyIOParameter('input_file_array', null, 'keep:00000000000000000000000000000000+02/input2.tar');
+ verifyIOParameter('input_file_array', null, 'keep:00000000000000000000000000000000+03/input3.tar');
+ verifyIOParameter('input_file_array', null, 'keep:00000000000000000000000000000000+03/input3-2.txt');
+ verifyIOParameter('input_dir_array', null, 'keep:11111111111111111111111111111111+02/');
+ verifyIOParameter('input_dir_array', null, 'keep:11111111111111111111111111111111+03/');
+ verifyIOParameter('input_int_array', null, ["1", "3", "5"]);
+ verifyIOParameter('input_long_array', null, ["10", "20"]);
+ verifyIOParameter('input_float_array', null, ["10.2", "10.4", "10.6"]);
+ verifyIOParameter('input_double_array', null, ["20.1", "20.2", "20.3"]);
+ verifyIOParameter('input_string_array', 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();
+ const outPdh = testOutputCollection.portable_data_hash;
+
+ verifyIOParameter('output_file', "Label Description", `keep:${outPdh}/cat.png`);
+ verifyIOParameterImage('output_file', `/c=${outPdh}/cat.png`);
+ verifyIOParameter('output_file_with_secondary', "Doc Description", `keep:${outPdh}/main.dat`);
+ verifyIOParameter('output_file_with_secondary', "Doc Description", `keep:${outPdh}/secondary.dat`);
+ verifyIOParameter('output_file_with_secondary', "Doc Description", `keep:${outPdh}/secondary2.dat`);
+ verifyIOParameter('output_dir', "Doc desc 1, Doc desc 2", `keep:${outPdh}/outdir1`);
+ verifyIOParameter('output_bool', null, 'true');
+ verifyIOParameter('output_int', null, '1');
+ verifyIOParameter('output_long', null, '1');
+ verifyIOParameter('output_float', null, '100.5');
+ verifyIOParameter('output_double', null, '100.3');
+ verifyIOParameter('output_string', null, 'Hello output');
+ verifyIOParameter('output_file_array', null, `keep:${outPdh}/output2.tar`);
+ verifyIOParameter('output_file_array', null, `keep:${outPdh}/output3.tar`);
+ verifyIOParameter('output_dir_array', null, `keep:${outPdh}/outdir2`);
+ verifyIOParameter('output_dir_array', null, `keep:${outPdh}/outdir3`);
+ verifyIOParameter('output_int_array', null, ["10", "11", "12"]);
+ verifyIOParameter('output_long_array', null, ["51", "52"]);
+ verifyIOParameter('output_float_array', null, ["100.2", "100.4", "100.6"]);
+ verifyIOParameter('output_double_array', null, ["100.1", "100.2", "100.3"]);
+ verifyIOParameter('output_string_array', 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
+}
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 = {
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";
export const processPanelActions = unionize({
SET_PROCESS_PANEL_CONTAINER_REQUEST_UUID: ofType<string>(),
}
};
+export const loadOutputs = (uuid: string, setOutputs) =>
+ async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const files = await services.collectionService.files(uuid);
+ const collection = await services.collectionService.get(uuid);
+ const outputFile = files.find((file) => file.name === 'cwl.output.json') as CollectionFile | undefined;
+ let outputData = outputFile ? await services.collectionService.getFileContents(outputFile) : undefined;
+ if ((outputData = JSON.parse(outputData)) && collection.portableDataHash) {
+ setOutputs({
+ rawOutputs: outputData,
+ pdh: collection.portableDataHash,
+ });
+ } else {
+ setOutputs({});
+ }
+ } catch {
+ setOutputs({});
+ }
+ };
+
export const openWorkflow = (uuid: string) =>
(dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
dispatch<any>(navigateToWorkflows);
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) => {
+export const getInputs = (data: any): CommandInputParameter[] => {
if (!data || !data.mounts || !data.mounts[MOUNT_PATH_CWL_WORKFLOW]) { return []; }
const inputs = getWorkflowInputs(data.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
return inputs ? inputs.map(
id: it.id,
label: it.label,
default: data.mounts[MOUNT_PATH_CWL_INPUT].content[it.id],
+ value: data.mounts[MOUNT_PATH_CWL_INPUT].content[it.id.split('/').pop()] || [],
+ doc: it.doc
+ }
+ )
+ ) : [];
+};
+
+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 { 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)),
});
<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}>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { ReactElement, useState } from 'react';
+import {
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ Card,
+ CardHeader,
+ IconButton,
+ CardContent,
+ Tooltip,
+ Typography,
+ Tabs,
+ Tab,
+ Table,
+ TableHead,
+ TableBody,
+ TableRow,
+ TableCell,
+ Paper,
+ Grid,
+ Chip,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon, InfoIcon, ProcessIcon } 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';
+
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'avatar' | 'iconHeader' | 'tableWrapper' | 'tableRoot' | 'paramValue' | 'keepLink' | 'imagePreview' | 'valArray' | 'emptyValue';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ card: {
+ height: '100%'
+ },
+ header: {
+ paddingTop: theme.spacing.unit,
+ paddingBottom: theme.spacing.unit,
+ },
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700,
+ },
+ avatar: {
+ alignSelf: 'flex-start',
+ paddingTop: theme.spacing.unit * 0.5
+ },
+ content: {
+ padding: theme.spacing.unit * 1.0,
+ paddingTop: theme.spacing.unit * 0.5,
+ '&:last-child': {
+ paddingBottom: theme.spacing.unit * 1,
+ }
+ },
+ title: {
+ overflow: 'hidden',
+ paddingTop: theme.spacing.unit * 0.5
+ },
+ tableWrapper: {
+ overflow: 'auto',
+ },
+ tableRoot: {
+ width: '100%',
+ },
+ paramValue: {
+ display: 'flex',
+ alignItems: 'flex-start',
+ flexDirection: 'column',
+ },
+ keepLink: {
+ 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',
+ }
+ },
+ emptyValue: {
+ color: theme.customs.colors.grey500,
+ },
+});
+
+export enum ProcessIOCardType {
+ INPUT = 'Inputs',
+ OUTPUT = 'Outputs',
+}
+export interface ProcessIOCardDataProps {
+ label: ProcessIOCardType;
+ params: ProcessIOParameter[];
+ raw?: any;
+ mounts?: InputCollectionMount[];
+ outputUuid?: string;
+}
+
+type ProcessIOCardProps = ProcessIOCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessIOCard = withStyles(styles)(
+ ({ classes, label, params, raw, mounts, outputUuid, doHidePanel, panelName }: ProcessIOCardProps) => {
+ const [tabState, setTabState] = useState(0);
+ const handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ setTabState(value);
+ }
+
+ return <Card className={classes.card} data-cy="process-io-card">
+ <CardHeader
+ className={classes.header}
+ classes={{
+ content: classes.title,
+ avatar: classes.avatar,
+ }}
+ avatar={<ProcessIcon className={classes.iconHeader} />}
+ title={
+ <Typography noWrap variant='h6' color='inherit'>
+ {label}
+ </Typography>
+ }
+ action={
+ <div>
+ { doHidePanel &&
+ <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+ <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+ </Tooltip> }
+ </div>
+ } />
+ <CardContent className={classes.content}>
+ <div>
+ <Tabs value={tabState} onChange={handleChange} variant="fullWidth">
+ <Tab label="Preview" />
+ <Tab label="Raw" />
+ {label === ProcessIOCardType.INPUT && <Tab label="Input Mounts" />}
+ {label === ProcessIOCardType.OUTPUT && <Tab label="Output Collection" />}
+ </Tabs>
+ {tabState === 0 && <div className={classes.tableWrapper}>
+ {params.length ?
+ <ProcessIOPreview data={params} /> :
+ <Grid container item alignItems='center' justify='center'>
+ <DefaultView messages={["No parameters found"]} icon={InfoIcon} />
+ </Grid>}
+ </div>}
+ {tabState === 1 && <div className={classes.tableWrapper}>
+ {params.length ?
+ <ProcessIORaw data={raw || params} /> :
+ <Grid container item alignItems='center' justify='center'>
+ <DefaultView messages={["No parameters found"]} icon={InfoIcon} />
+ </Grid>}
+ </div>}
+ {tabState === 2 && <div className={classes.tableWrapper}>
+ {label === ProcessIOCardType.INPUT && <ProcessInputMounts mounts={mounts || []} />}
+ {label === ProcessIOCardType.OUTPUT && <ProcessOutputCollectionFiles isWritable={false} currentItemUuid={outputUuid} />}
+ </div>}
+ </div>
+ </CardContent>
+ </Card>;
+ }
+);
+
+export type ProcessIOValue = {
+ display: ReactElement<any, any>;
+ imageUrl?: string;
+}
+
+export type ProcessIOParameter = {
+ id: string;
+ doc: string;
+ value: ProcessIOValue[];
+}
+
+interface ProcessIOPreviewDataProps {
+ data: ProcessIOParameter[];
+}
+
+type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
+
+const ProcessIOPreview = withStyles(styles)(
+ ({ classes, data }: ProcessIOPreviewProps) =>
+ <Table className={classes.tableRoot} aria-label="Process IO Preview">
+ <TableHead>
+ <TableRow>
+ <TableCell>Label</TableCell>
+ <TableCell>Description</TableCell>
+ <TableCell>Value</TableCell>
+ </TableRow>
+ </TableHead>
+ <TableBody>
+ {data.map((param: ProcessIOParameter) => {
+ return <TableRow key={param.id}>
+ <TableCell component="th" scope="row">
+ {param.id}
+ </TableCell>
+ <TableCell>{param.doc}</TableCell>
+ <TableCell>{param.value.map(val => (
+ <Typography className={classes.paramValue}>
+ {val.imageUrl ? <img className={classes.imagePreview} src={val.imageUrl} alt="Inline Preview" /> : ""}
+ <span className={classes.valArray}>
+ {val.display}
+ </span>
+ </Typography>
+ ))}</TableCell>
+ </TableRow>;
+ })}
+ </TableBody>
+ </Table>
+);
+
+const handleClick = (url) => {
+ window.open(url, '_blank');
+}
+
+const ProcessIORaw = withStyles(styles)(
+ ({ data }: ProcessIOPreviewProps) =>
+ <Paper elevation={0}>
+ <pre>
+ {JSON.stringify(data, null, 2)}
+ </pre>
+ </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
+ ];
+
+ return files.length ?
+ files.map(file => fileToProcessIOValue(file, auth, pdh)) :
+ [{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 fileArrayMainFile = ((input as FileArrayCommandInputParameter).value || []);
+ const fileArraySecondaryFiles = fileArrayMainFile.map((file) => (
+ ((file as unknown) as FileWithSecondaryFiles)?.secondaryFiles || []
+ )).reduce((acc: File[], params: File[]) => (acc.concat(params)), []);
+
+ const fileArrayFiles = [
+ ...fileArrayMainFile,
+ ...fileArraySecondaryFiles
+ ];
+
+ return fileArrayFiles.length ?
+ fileArrayFiles.map(file => fileToProcessIOValue(file, auth, pdh)) :
+ [{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 [];
+ }
+};
+
+const getKeepUrl = (file: File | Directory, pdh?: string): string => {
+ const isKeepUrl = file.location?.startsWith('keep:') || false;
+ const keepUrl = isKeepUrl ? file.location : pdh ? `keep:${pdh}/${file.location}` : file.location;
+ return keepUrl || '';
+};
+
+interface KeepUrlProps {
+ auth: AuthState;
+ res: File | Directory;
+ pdh?: string;
+}
+
+const KeepUrlBase = withStyles(styles)(({auth, res, pdh, classes}: KeepUrlProps & WithStyles<CssRules>) => {
+ const keepUrl = getKeepUrl(res, pdh);
+ const pdhUrl = keepUrl ? keepUrl.split('/').slice(0, 1)[0] : '';
+ // Passing a pdh always returns a relative wb2 collection url
+ const pdhWbPath = getNavUrl(pdhUrl.replace('keep:', ''), auth);
+ return pdhUrl && pdhWbPath ?
+ <RouterLink to={pdhWbPath} className={classes.keepLink}>{pdhUrl}</RouterLink> :
+ <></>;
+});
+
+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 keepUrlPath && keepUrlPathNav ?
+ <MuiLink className={classes.keepLink} onClick={() => handleClick(keepUrlPathNav)}>{keepUrlPath}</MuiLink> :
+ <></>;
+});
+
+const getKeepNavUrl = (auth: AuthState, file: File | Directory, pdh?: string): string => {
+ let keepUrl = getKeepUrl(file, pdh).replace('keep:', '');
+ 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).replace('keep:', '');
+ 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: <span>
+ <KeepUrlBase auth={auth} res={normalizedDirectory} pdh={pdh}/>/<KeepUrlPath auth={auth} res={normalizedDirectory} pdh={pdh}/>
+ </span>,
+ };
+};
+
+const fileToProcessIOValue = (file: File, auth: AuthState, pdh?: string): ProcessIOValue => {
+ return {
+ display: <span>
+ <KeepUrlBase auth={auth} res={file} pdh={pdh}/>/<KeepUrlPath auth={auth} res={file} pdh={pdh}/>
+ </span>,
+ imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
+ }
+};
+
+const EmptyValue = withStyles(styles)(
+ ({classes}: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</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);
//
// SPDX-License-Identifier: AGPL-3.0
-import React from 'react';
+import React, { useState } 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 { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
import { ArvadosTheme } from 'common/custom-theme';
import { ProcessDetailsCard } from './process-details-card';
+import { getIOParamDisplayValue, 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 { getInputs, getInputCollectionMounts, getOutputParameters } from 'store/processes/processes-actions';
+import { CommandInputParameter, getIOParamId } from 'models/workflow';
+import { CommandOutputParameter } from 'cwlts/mappings/v1.0/CommandOutputParameter';
+import { AuthState } from 'store/auth/auth-reducer';
import { ProcessCmdCard } from './process-cmd-card';
type CssRules = 'root';
subprocesses: Array<Process>;
filters: Array<SubprocessFilterDataProps>;
processLogsPanel: ProcessLogsPanel;
+ auth: AuthState;
}
export interface ProcessPanelRootActionProps {
onLogFilterChange: (filter: FilterOption) => void;
navigateToLog: (uuid: string) => void;
onCopyToClipboard: (uuid: string) => void;
+ fetchOutputs: (uuid: string, fetchOutputs) => void;
}
export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
+type OutputDetails = {
+ rawOutputs?: any;
+ pdh?: string;
+}
+
const panelsData: MPVPanelState[] = [
{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, fetchOutputs, ...props }: ProcessPanelRootProps) => {
+
+ const [outputDetails, setOutputs] = useState<OutputDetails>({});
+ const [rawInputs, setInputs] = useState<CommandInputParameter[]>([]);
+
+
+ const [processedOutputs, setProcessedOutputs] = useState<ProcessIOParameter[]>([]);
+ const [processedInputs, setProcessedInputs] = useState<ProcessIOParameter[]>([]);
+
+ const outputUuid = process?.containerRequest.outputUuid;
+ const requestUuid = process?.containerRequest.uuid;
+
+ const inputMounts = getInputCollectionMounts(process?.containerRequest);
+
+ React.useEffect(() => {
+ if (outputUuid) {
+ fetchOutputs(outputUuid, setOutputs);
+ }
+ }, [outputUuid, fetchOutputs]);
+
+ React.useEffect(() => {
+ if (outputDetails.rawOutputs && process) {
+ const outputDefinitions = getOutputParameters(process.containerRequest);
+ setProcessedOutputs(formatOutputData(outputDefinitions, outputDetails.rawOutputs, outputDetails.pdh, auth));
+ } else {
+ setProcessedOutputs([]);
+ }
+ }, [outputDetails, auth, process]);
+
+ React.useEffect(() => {
+ if (process) {
+ const rawInputs = getInputs(process.containerRequest);
+ setInputs(rawInputs);
+ setProcessedInputs(formatInputData(rawInputs, auth));
+ }
+ }, [requestUuid, auth, process]);
+
+ 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="auto" data-cy="process-inputs">
+ <ProcessIOCard
+ label={ProcessIOCardType.INPUT}
+ params={processedInputs}
+ raw={rawInputs}
+ mounts={inputMounts}
+ />
+ </MPVPanelContent>
+ <MPVPanelContent forwardProps xs="auto" data-cy="process-outputs">
+ <ProcessIOCard
+ label={ProcessIOCardType.OUTPUT}
+ params={processedOutputs}
+ raw={outputDetails.rawOutputs}
+ outputUuid={outputUuid || ""}
+ />
+ </MPVPanelContent>
<MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-children">
<SubprocessPanel />
</MPVPanelContent>
<DefaultView
icon={ProcessIcon}
messages={['Process not found']} />
- </Grid>);
+ </Grid>;
+ }
+);
+
+const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
+ return inputs.map(input => {
+ const doc = Array.isArray(input.doc) ? input.doc.join(', ') : input.doc;
+ return {
+ id: getIOParamId(input),
+ doc: input.label || doc || "",
+ value: getIOParamDisplayValue(auth, input)
+ };
+ });
+};
+
+const formatOutputData = (definitions: CommandOutputParameter[], values: any, pdh: string | undefined, auth: AuthState): ProcessIOParameter[] => {
+ return definitions.map(output => {
+ const doc = Array.isArray(output.doc) ? output.doc.join(', ') : output.doc;
+ return {
+ id: getIOParamId(output),
+ doc: output.label || doc || "",
+ value: getIOParamDisplayValue(auth, Object.assign(output, { value: values[getIOParamId(output)] || [] }), pdh)
+ };
+ });
+};
} from 'store/process-panel/process-panel';
import { groupBy } from 'lodash';
import {
+ loadOutputs,
toggleProcessPanelFilter,
} 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,
};
};
cancelProcess: (uuid) => dispatch<any>(cancelRunningWorkflow(uuid)),
onLogFilterChange: (filter) => dispatch(setProcessLogsPanelFilter(filter.value)),
navigateToLog: (uuid) => dispatch<any>(navigateToLogCollection(uuid)),
+ fetchOutputs: (uuid, setOutputs) => dispatch<any>(loadOutputs(uuid, setOutputs)),
});
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"