Merge branch 'main' of git.arvados.org:arvados-workbench2 into 16073-process-io-panels
authorStephen Smith <stephen@curii.com>
Tue, 30 Aug 2022 22:04:05 +0000 (18:04 -0400)
committerStephen Smith <stephen@curii.com>
Tue, 30 Aug 2022 22:04:05 +0000 (18:04 -0400)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

1  2 
package.json
src/store/processes/processes-actions.ts
src/views/process-panel/process-panel-root.tsx
src/views/process-panel/process-panel.tsx
yarn.lock

diff --combined package.json
index 110d0e70ca367e8eeeb784202439aad120cb6d4c,9e663ca6ac5a440946e91f1f3a50ce030db355e0..347ca0f40a652aa499c8c04c5b5fd9b8d1a5afb3
@@@ -22,7 -22,7 +22,7 @@@
      "@types/react-virtualized-auto-sizer": "1.0.0",
      "@types/react-window": "1.8.2",
      "@types/redux-form": "7.4.12",
-     "@types/shell-quote": "1.6.0",
+     "@types/shell-escape": "^0.2.0",
      "axios": "^0.21.1",
      "babel-core": "6.26.3",
      "babel-runtime": "6.26.0",
@@@ -44,7 -44,6 +44,7 @@@
      "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",
@@@ -72,7 -71,7 +72,7 @@@
      "redux-thunk": "2.3.0",
      "reselect": "4.0.0",
      "set-value": "2.0.1",
-     "shell-quote": "1.6.1",
+     "shell-escape": "^0.2.0",
      "sinon": "7.3",
      "tslint": "5.20.0",
      "tslint-etc": "1.6.0",
index 77f0eca8095d4d0cd0072adf605112382a89fff5,c4d421ac09d9b5719a9f8d1b8f9a00833b7cf662..dbca03ab6387c069993f80c3423f2d5881ac3507
@@@ -17,21 -17,15 +17,21 @@@ 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 { 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]));
          return { containerRequest };
      };
  
- export const loadContainers = (filters: string) =>
+ export const loadContainers = (filters: string, loadMounts: boolean = true) =>
      async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-         const { items } = await services.containerService.list({ filters });
+         let args: any = { filters };
+         if (!loadMounts) {
+             args.select = containerFieldsNoMounts;
+         }
+         const { items } = await services.containerService.list(args);
          dispatch<any>(updateResources(items));
          return items;
      };
  
+ // Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+ const containerFieldsNoMounts = [
+     "auth_uuid",
+     "command",
+     "container_image",
+     "created_at",
+     "cwd",
+     "environment",
+     "etag",
+     "exit_code",
+     "finished_at",
+     "gateway_address",
+     "href",
+     "interactive_session_started",
+     "kind",
+     "lock_count",
+     "locked_by_uuid",
+     "log",
+     "modified_at",
+     "modified_by_client_uuid",
+     "modified_by_user_uuid",
+     "output_path",
+     "output_properties",
+     "output_storage_classes",
+     "output",
+     "owner_uuid",
+     "priority",
+     "progress",
+     "runtime_auth_scopes",
+     "runtime_constraints",
+     "runtime_status",
+     "runtime_user_uuid",
+     "scheduling_parameters",
+     "started_at",
+     "state",
+     "uuid",
+ ]
  export const cancelRunningWorkflow = (uuid: string) =>
      async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
          try {
@@@ -91,7 -127,7 +133,7 @@@ export const reRunProcess = (processUui
          }
      };
  
 -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
              }
          )
index 22643e8588c279277295ecd537e29b0e31570ac4,f8ff84304dcb3fb4acc7554ef0f26882ef9cc6d6..1cf6030015426d87a9742790db6aa40bc30913bc
@@@ -2,7 -2,7 +2,7 @@@
  //
  // 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';
@@@ -12,15 -12,10 +12,16 @@@ import { SubprocessFilterDataProps } fr
  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';
  
@@@ -35,7 -30,6 +36,7 @@@ export interface ProcessPanelRootDataPr
      subprocesses: Array<Process>;
      filters: Array<SubprocessFilterDataProps>;
      processLogsPanel: ProcessLogsPanel;
 +    auth: AuthState;
  }
  
  export interface ProcessPanelRootActionProps {
      cancelProcess: (uuid: string) => void;
      onLogFilterChange: (filter: FilterOption) => void;
      navigateToLog: (uuid: string) => void;
-     onLogCopyToClipboard: (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
                      cancelProcess={props.cancelProcess}
                  />
              </MPVPanelContent>
+             <MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
+                 <ProcessCmdCard
+                     onCopy={props.onCopyToClipboard}
+                     process={process} />
+             </MPVPanelContent>
              <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-logs">
                  <ProcessLogsCard
-                     onCopy={props.onLogCopyToClipboard}
+                     onCopy={props.onCopyToClipboard}
                      process={process}
                      lines={getProcessPanelLogs(processLogsPanel)}
                      selectedFilter={{
                      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)
 +        };
 +    });
 +};
index 348222f6d7bddcafa1222046c56ce6166d1b7dd6,7afaa04d94b95f90e47181f2a449985c0879fd62..8adec3bd9f78cdce8558f5759f56ad54b37a46ee
@@@ -18,14 -18,13 +18,14 @@@ import 
  } 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,
      };
  };
  
  const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
-     onLogCopyToClipboard: (message: string) => {
+     onCopyToClipboard: (message: string) => {
          dispatch<any>(snackbarActions.OPEN_SNACKBAR({
              message,
              hideDuration: 2000,
@@@ -54,7 -52,6 +54,7 @@@
      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[]) => {
diff --combined yarn.lock
index 7fa499fdc4f332e8158d1378d33faf526970be36,6dfb5b18b8aae518b209c72c9888a5fbb65d00cc..64f25cc2e921d34644d2fad2d8b9a3a3cb10ec92
+++ b/yarn.lock
@@@ -2817,10 -2817,10 +2817,10 @@@ __metadata
    languageName: node
    linkType: hard
  
- "@types/shell-quote@npm:1.6.0":
-   version: 1.6.0
-   resolution: "@types/shell-quote@npm:1.6.0"
-   checksum: 5d9f4e35c8df32d9994f8ae2f1a1fe8a6b7ee96794f803e0904ceae7ad7255a214954e85cd75bd847fe77458d3746430522e87237438f223b7d72a23c4928c0e
+ "@types/shell-escape@npm:^0.2.0":
+   version: 0.2.0
+   resolution: "@types/shell-escape@npm:0.2.0"
+   checksum: 020696ed313eeb02deb2abcc581e8b570be6f9ee662892339965b524bb4fbdc9a97b6520d914117740ec11147b0b1aa52358b8e03fa214c2da99743adb196853
    languageName: node
    linkType: hard
  
    languageName: node
    linkType: hard
  
- "array-filter@npm:~0.0.0":
-   version: 0.0.1
-   resolution: "array-filter@npm:0.0.1"
-   checksum: 0e9afdf5e248c45821c6fe1232071a13a3811e1902c2c2a39d12e4495e8b0b25739fd95bffbbf9884b9693629621f6077b4ae16207b8f23d17710fc2465cebbb
-   languageName: node
-   linkType: hard
  "array-find-index@npm:^1.0.1":
    version: 1.0.2
    resolution: "array-find-index@npm:1.0.2"
    languageName: node
    linkType: hard
  
- "array-map@npm:~0.0.0":
-   version: 0.0.0
-   resolution: "array-map@npm:0.0.0"
-   checksum: 30d73fdc99956c8bd70daea40db5a7d78c5c2c75a03c64fc77904885e79adf7d5a0595076534f4e58962d89435f0687182ac929e65634e3d19931698cbac8149
-   languageName: node
-   linkType: hard
- "array-reduce@npm:~0.0.0":
-   version: 0.0.0
-   resolution: "array-reduce@npm:0.0.0"
-   checksum: d6226325271f477e3dd65b4d40db8597735b8d08bebcca4972e52d3c173d6c697533664fa8865789ea2d076bdaf1989bab5bdfbb61598be92074a67f13057c3a
-   languageName: node
-   linkType: hard
  "array-union@npm:^1.0.1":
    version: 1.0.2
    resolution: "array-union@npm:1.0.2"
      "@types/redux-devtools": 3.0.44
      "@types/redux-form": 7.4.12
      "@types/redux-mock-store": 1.0.2
-     "@types/shell-quote": 1.6.0
+     "@types/shell-escape": ^0.2.0
      "@types/sinon": 7.5
      "@types/uuid": 3.4.4
      axios: ^0.21.1
      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
      redux-thunk: 2.3.0
      reselect: 4.0.0
      set-value: 2.0.1
-     shell-quote: 1.6.1
+     shell-escape: ^0.2.0
      sinon: 7.3
      ts-mock-imports: 1.3.7
      tslint: 5.20.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"
    languageName: node
    linkType: hard
  
- "shell-quote@npm:1.6.1":
-   version: 1.6.1
-   resolution: "shell-quote@npm:1.6.1"
-   dependencies:
-     array-filter: ~0.0.0
-     array-map: ~0.0.0
-     array-reduce: ~0.0.0
-     jsonify: ~0.0.0
-   checksum: 982a4fdf2d474f0dc40885de4222f100ba457d7c75d46b532bf23b01774b8617bc62522c6825cb1fa7dd4c54c18e9dcbae7df2ca8983101841b6f2e6a7cacd2f
+ "shell-escape@npm:^0.2.0":
+   version: 0.2.0
+   resolution: "shell-escape@npm:0.2.0"
+   checksum: 0d87f1ae22ad22a74e148348ceaf64721e3024f83c90afcfb527318ce10ece654dd62e103dd89a242f2f4e4ce3cecdef63e3d148c40e5fabca8ba6c508f97d9f
    languageName: node
    linkType: hard