16073: Use workflow output definition fields for output type and doc info
authorStephen Smith <stephen@curii.com>
Tue, 19 Jul 2022 14:04:09 +0000 (10:04 -0400)
committerStephen Smith <stephen@curii.com>
Tue, 19 Jul 2022 14:04:09 +0000 (10:04 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

src/models/workflow.ts
src/store/processes/processes-actions.ts
src/views/process-panel/process-io-card.tsx
src/views/process-panel/process-panel-root.tsx

index 12f253acfe47ec80e50dc65450b84962e1a02f58..e85dce7a6a02e697fe9b674630b530b9599d09cc 100644 (file)
@@ -4,6 +4,7 @@
 
 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;
@@ -152,11 +153,18 @@ export const getWorkflowInputs = (workflowDefinition: WorkflowResourceDefinition
         : 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 getInputId = (input: CommandInputParameter) => {
+export const getIOParamId = (input: CommandInputParameter | CommandOutputParameter) => {
     return `${input.id.split('/').pop()}`;
 };
 
index ddf71e77d644290af6d43dc2277afc51fffed686..3a4c60632a719facb0a0be3c0066a0f39ea9080d 100644 (file)
@@ -17,9 +17,10 @@ import { initialize } from "redux-form";
 import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from "views/run-process-panel/run-process-basic-form";
 import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from "views/run-process-panel/run-process-advanced-form";
 import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from 'models/process';
-import { CommandInputParameter, 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> => {
@@ -102,6 +103,21 @@ export const getInputs = (data: any): CommandInputParameter[] => {
     ) : [];
 };
 
+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
+            }
+        )
+    ) : [];
+};
+
 export const openRemoveProcessDialog = (uuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         dispatch(dialogActions.OPEN_DIALOG({
index 6f932187168430c43a890ddc15ae90622ad6e5e1..6426f2bd28517374490631b3ee472d66182aaab8 100644 (file)
@@ -115,7 +115,7 @@ export const ProcessIOCard = withStyles(styles)(
             setTabState(value);
         }
 
-        return <Card className={classes.card}>
+        return <Card className={classes.card} data-cy="process-io-card">
             <CardHeader
                 className={classes.header}
                 classes={{
@@ -217,8 +217,11 @@ const ProcessIORaw = withStyles(styles)(
         </Paper>
 );
 
-// secondaryFiles File[] is not part of CommandOutputParameter so we pass in an extra param
-export const getInputDisplayValue = (auth: AuthState, input: CommandInputParameter | CommandOutputParameter, pdh?: string, secondaryFiles: File[] = []): ProcessIOValue[] => {
+type FileWithSecondaryFiles = {
+    secondaryFiles: File[];
+}
+
+export const getInputDisplayValue = (auth: AuthState, input: CommandInputParameter | CommandOutputParameter, pdh?: string): ProcessIOValue[] => {
     switch (true) {
         case isPrimitiveOfType(input, CWLType.BOOLEAN):
             return [{display: String((input as BooleanCommandInputParameter).value)}];
@@ -236,6 +239,8 @@ export const getInputDisplayValue = (auth: AuthState, input: CommandInputParamet
 
         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 ? [mainFile] : []),
                 ...secondaryFiles
@@ -263,7 +268,17 @@ export const getInputDisplayValue = (auth: AuthState, input: CommandInputParamet
             return [{ display: ((input as FloatArrayCommandInputParameter).value || []).join(', ') }];
 
         case isArrayOfType(input, CWLType.FILE):
-            return ((input as FileArrayCommandInputParameter).value || [])
+            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
                 .map(file => fileToProcessIOValue(file, auth, pdh));
 
         case isArrayOfType(input, CWLType.DIRECTORY):
index 3447dc2a897e6bf331ad2a85c84f336e61389023..950418fb1589064324b5230756dea12297aa3488 100644 (file)
@@ -17,8 +17,8 @@ import { getInputDisplayValue, ProcessIOCard, ProcessIOParameter } from './proce
 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 } from 'store/processes/processes-actions';
-import { CommandInputParameter, getInputId } from 'models/workflow';
+import { getInputs, 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';
 
@@ -83,12 +83,13 @@ export const ProcessPanelRoot = withStyles(styles)(
     }, [outputUuid, fetchOutputs]);
 
     React.useEffect(() => {
-        if (outputDetails.rawOutputs) {
-            setProcessedOutputs(formatOutputData(outputDetails.rawOutputs, outputDetails.pdh, auth));
+        if (outputDetails.rawOutputs && process) {
+            const outputDefinitions = getOutputParameters(process.containerRequest);
+            setProcessedOutputs(formatOutputData(outputDefinitions, outputDetails.rawOutputs, outputDetails.pdh, auth));
         } else {
             setProcessedOutputs([]);
         }
-    }, [outputDetails, auth]);
+    }, [outputDetails, auth, process]);
 
     React.useEffect(() => {
         if (process) {
@@ -154,33 +155,22 @@ export const ProcessPanelRoot = withStyles(styles)(
 
 const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
     return inputs.map(input => {
+        const doc = Array.isArray(input.doc) ? input.doc.join(', ') : input.doc;
         return {
-            id: getInputId(input),
-            doc: input.label || "",
+            id: getIOParamId(input),
+            doc: input.label || doc || "",
             value: getInputDisplayValue(auth, input)
         };
     });
 };
 
-const formatOutputData = (rawData: any, pdh: string | undefined, auth: AuthState): ProcessIOParameter[] => {
-    if (!rawData) { return []; }
-    return Object.keys(rawData).map((id): ProcessIOParameter => {
-        const multiple = rawData[id].length > 0;
-        const outputArray = multiple ? rawData[id] : [rawData[id]];
+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,
-            doc: outputArray.map((outputParam: CommandOutputParameter) => (outputParam.doc))
-                        // Doc can be string or string[], concat conveniently works with both
-                        .reduce((acc: string[], input: string | string[]) => (acc.concat(input)), [])
-                        // Remove undefined and empty doc strings
-                        .filter(str => str)
-                        .join(", "),
-            value: outputArray.map(outputParam => getInputDisplayValue(auth, {
-                    type: outputParam.class,
-                    value: outputParam,
-                    ...outputParam
-                }, pdh, outputParam.secondaryFiles))
-                .reduce((acc: ProcessIOParameter[], params: ProcessIOParameter[]) => (acc.concat(params)), [])
+            id: getIOParamId(output),
+            doc: output.label || doc || "",
+            value: getInputDisplayValue(auth, Object.assign(output, { value: values[getIOParamId(output)] }), pdh)
         };
     });
 };