[arvados-workbench2] created: 2.4.0-156-g55acac75
git repository hosting
git at public.arvados.org
Tue Jul 12 19:35:49 UTC 2022
at 55acac755ba2516c63e902a11e90c6e3754e5c48 (commit)
commit 55acac755ba2516c63e902a11e90c6e3754e5c48
Author: Stephen Smith <stephen at curii.com>
Date: Tue Jul 12 15:13:59 2022 -0400
16073: Add process IO panels with image preview and raw view
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>
diff --git a/package.json b/package.json
index a8b3ee81..110d0e70 100644
--- a/package.json
+++ b/package.json
@@ -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",
diff --git a/src/common/webdav.ts b/src/common/webdav.ts
index 93ec21cb..d4f904ae 100644
--- a/src/common/webdav.ts
+++ b/src/common/webdav.ts
@@ -30,6 +30,12 @@ export class WebDAV {
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({
@@ -88,7 +94,7 @@ export class WebDAV {
Object.assign(window, { cancelTokens: {} });
}
- (window as any).cancelTokens[config.url] = () => {
+ (window as any).cancelTokens[config.url] = () => {
resolve(r);
r.abort();
}
@@ -138,4 +144,4 @@ interface RequestConfig {
headers?: { [key: string]: string };
data?: any;
onUploadProgress?: (event: ProgressEvent) => void;
-}
\ No newline at end of file
+}
diff --git a/src/models/workflow.ts b/src/models/workflow.ts
index 6d21dbc7..12f253ac 100644
--- a/src/models/workflow.ts
+++ b/src/models/workflow.ts
@@ -156,6 +156,10 @@ export const getInputLabel = (input: CommandInputParameter) => {
return `${input.label || input.id.split('/').pop()}`;
};
+export const getInputId = (input: CommandInputParameter) => {
+ return `${input.id.split('/').pop()}`;
+};
+
export const isRequiredInput = ({ type }: CommandInputParameter) => {
if (type instanceof Array) {
for (const t of type) {
diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts
index 92e4dfba..e2c420d8 100644
--- a/src/services/collection-service/collection-service.ts
+++ b/src/services/collection-service/collection-service.ts
@@ -107,6 +107,10 @@ export class CollectionService extends TrashableResourceService<CollectionResour
};
}
+ 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 = {
diff --git a/src/store/process-panel/process-panel-actions.ts b/src/store/process-panel/process-panel-actions.ts
index e77c300d..d21b9b83 100644
--- a/src/store/process-panel/process-panel-actions.ts
+++ b/src/store/process-panel/process-panel-actions.ts
@@ -14,6 +14,7 @@ import { SnackbarKind } from '../snackbar/snackbar-actions';
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>(),
@@ -45,6 +46,26 @@ export const navigateToOutput = (uuid: 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);
diff --git a/src/store/processes/processes-actions.ts b/src/store/processes/processes-actions.ts
index 213e292b..ddf71e77 100644
--- a/src/store/processes/processes-actions.ts
+++ b/src/store/processes/processes-actions.ts
@@ -17,7 +17,7 @@ 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 } from "models/workflow";
import { ProjectResource } from "models/project";
import { UserResource } from "models/user";
@@ -85,7 +85,7 @@ export const reRunProcess = (processUuid: string, workflowUuid: string) =>
}
};
-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(
@@ -95,6 +95,7 @@ const getInputs = (data: any) => {
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
}
)
diff --git a/src/views/process-panel/process-io-card.tsx b/src/views/process-panel/process-io-card.tsx
new file mode 100644
index 00000000..5540caed
--- /dev/null
+++ b/src/views/process-panel/process-io-card.tsx
@@ -0,0 +1,307 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useState } from 'react';
+import {
+ StyleRulesCallback,
+ WithStyles,
+ withStyles,
+ Card,
+ CardHeader,
+ IconButton,
+ CardContent,
+ Tooltip,
+ Typography,
+ Tabs,
+ Tab,
+ Table,
+ TableHead,
+ TableBody,
+ TableRow,
+ TableCell,
+ Paper,
+ Link,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon, 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';
+
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'avatar' | 'iconHeader' | 'tableWrapper' | 'tableRoot' | 'paramValue' | 'keepLink' | 'imagePreview';
+
+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: 'center',
+ },
+ keepLink: {
+ cursor: 'pointer',
+ },
+ imagePreview: {
+ maxHeight: '15em',
+ },
+});
+
+export interface ProcessIOCardDataProps {
+ label: string;
+ params: ProcessIOParameter[];
+ raw?: any;
+}
+
+type ProcessIOCardProps = ProcessIOCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessIOCard = withStyles(styles)(
+ ({ classes, label, params, raw, doHidePanel, panelName }: ProcessIOCardProps) => {
+ const [tabState, setTabState] = useState(0);
+ const handleChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
+ setTabState(value);
+ }
+
+ return <Card className={classes.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}>
+ <Tabs value={tabState} onChange={handleChange} variant="fullWidth">
+ <Tab label="Preview" />
+ <Tab label="Raw" />
+ </Tabs>
+ {tabState === 0 && <div className={classes.tableWrapper}>
+ <ProcessIOPreview data={params} />
+ </div>}
+ {tabState === 1 && <div className={classes.tableWrapper}>
+ <ProcessIORaw data={raw || params} />
+ </div>}
+ </CardContent>
+ </Card>;
+ }
+);
+
+export type ProcessIOValue = {
+ display: string;
+ nav?: string;
+ 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="simple table">
+ <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" /> : ""}
+ {val.nav ? <Link className={classes.keepLink} onClick={() => handleClick(val.nav)}>{val.display}</Link> : val.display}
+ </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>
+);
+
+// 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[] => {
+ switch (true) {
+ case isPrimitiveOfType(input, CWLType.BOOLEAN):
+ return [{display: String((input as BooleanCommandInputParameter).value)}];
+
+ case isPrimitiveOfType(input, CWLType.INT):
+ case isPrimitiveOfType(input, CWLType.LONG):
+ return [{display: String((input as IntCommandInputParameter).value)}];
+
+ case isPrimitiveOfType(input, CWLType.FLOAT):
+ case isPrimitiveOfType(input, CWLType.DOUBLE):
+ return [{display: String((input as FloatCommandInputParameter).value)}];
+
+ case isPrimitiveOfType(input, CWLType.STRING):
+ return [{display: (input as StringCommandInputParameter).value || ""}];
+
+ case isPrimitiveOfType(input, CWLType.FILE):
+ const mainFile = (input as FileCommandInputParameter).value;
+ const files = [
+ ...(mainFile ? [mainFile] : []),
+ ...secondaryFiles
+ ];
+
+ return files.map(file => ({
+ display: getKeepUrl(file, pdh),
+ nav: getNavUrl(auth, file, pdh),
+ imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
+ }));
+
+ case isPrimitiveOfType(input, CWLType.DIRECTORY):
+ const directory = (input as DirectoryCommandInputParameter).value;
+ return directory ? [{
+ display: getKeepUrl(directory, pdh),
+ nav: getNavUrl(auth, directory, pdh),
+ }] : [];
+
+ case typeof input.type === 'object' &&
+ !(input.type instanceof Array) &&
+ input.type.type === 'enum':
+ return [{ display: (input as EnumCommandInputParameter).value || '' }];
+
+ case isArrayOfType(input, CWLType.STRING):
+ return [{ display: ((input as StringArrayCommandInputParameter).value || []).join(', ') }];
+
+ case isArrayOfType(input, CWLType.INT):
+ case isArrayOfType(input, CWLType.LONG):
+ return [{ display: ((input as IntArrayCommandInputParameter).value || []).join(', ') }];
+
+ case isArrayOfType(input, CWLType.FLOAT):
+ case isArrayOfType(input, CWLType.DOUBLE):
+ return [{ display: ((input as FloatArrayCommandInputParameter).value || []).join(', ') }];
+
+ case isArrayOfType(input, CWLType.FILE):
+ return ((input as FileArrayCommandInputParameter).value || []).map(file => ({
+ display: getKeepUrl(file, pdh),
+ nav: getNavUrl(auth, file, pdh),
+ imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
+ }));
+
+ case isArrayOfType(input, CWLType.DIRECTORY):
+ const directories = (input as DirectoryArrayCommandInputParameter).value || [];
+ return directories.map(directory => ({
+ display: getKeepUrl(directory, pdh),
+ nav: getNavUrl(auth, directory, pdh),
+ }));
+
+ default:
+ return [{display: ''}];
+ }
+};
+
+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 || '';
+}
+
+const getNavUrl = (auth: AuthState, file: File | Directory, pdh?: string): string => {
+ let keepUrl = getKeepUrl(file, pdh).replace('keep:', '');
+ // Directory urls lack a trailing slash
+ if (!keepUrl.endsWith('/')) {
+ keepUrl += '/';
+ }
+ 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;
+}
diff --git a/src/views/process-panel/process-panel-root.tsx b/src/views/process-panel/process-panel-root.tsx
index 4f95d0d8..e130054b 100644
--- a/src/views/process-panel/process-panel-root.tsx
+++ b/src/views/process-panel/process-panel-root.tsx
@@ -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,9 +12,15 @@ import { SubprocessFilterDataProps } from 'components/subprocess-filter/subproce
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 { getInputDisplayValue, ProcessIOCard, 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 } from 'store/processes/processes-actions';
+import { CommandInputParameter, getInputId } from 'models/workflow';
+import { CommandOutputParameter } from 'cwlts/mappings/v1.0/CommandOutputParameter';
+import { AuthState } from 'store/auth/auth-reducer';
type CssRules = 'root';
@@ -29,6 +35,7 @@ export interface ProcessPanelRootDataProps {
subprocesses: Array<Process>;
filters: Array<SubprocessFilterDataProps>;
processLogsPanel: ProcessLogsPanel;
+ auth: AuthState;
}
export interface ProcessPanelRootActionProps {
@@ -38,19 +45,58 @@ export interface ProcessPanelRootActionProps {
onLogFilterChange: (filter: FilterOption) => void;
navigateToLog: (uuid: string) => void;
onLogCopyToClipboard: (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: "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;
+
+ React.useEffect(() => {
+ if (outputUuid) {
+ fetchOutputs(outputUuid, setOutputs);
+ }
+ }, [outputUuid, fetchOutputs]);
+
+ React.useEffect(() => {
+ if (outputDetails.rawOutputs) {
+ setProcessedOutputs(formatOutputData(outputDetails.rawOutputs, outputDetails.pdh, auth));
+ }
+ }, [outputDetails, auth]);
+
+ 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
@@ -75,6 +121,20 @@ export const ProcessPanelRoot = withStyles(styles)(
navigateToLog={props.navigateToLog}
/>
</MPVPanelContent>
+ <MPVPanelContent forwardProps xs="auto" data-cy="process-inputs">
+ <ProcessIOCard
+ label="Inputs"
+ params={processedInputs}
+ raw={rawInputs}
+ />
+ </MPVPanelContent>
+ <MPVPanelContent forwardProps xs="auto" data-cy="process-outputs">
+ <ProcessIOCard
+ label="Outputs"
+ params={processedOutputs}
+ raw={outputDetails.rawOutputs}
+ />
+ </MPVPanelContent>
<MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-children">
<SubprocessPanel />
</MPVPanelContent>
@@ -86,4 +146,39 @@ export const ProcessPanelRoot = withStyles(styles)(
<DefaultView
icon={ProcessIcon}
messages={['Process not found']} />
- </Grid>);
+ </Grid>;
+ }
+);
+
+const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
+ return inputs.map(input => {
+ return {
+ id: getInputId(input),
+ doc: input.label || "",
+ 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]];
+ 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)), [])
+ };
+ });
+};
diff --git a/src/views/process-panel/process-panel.tsx b/src/views/process-panel/process-panel.tsx
index de6b13b3..348222f6 100644
--- a/src/views/process-panel/process-panel.tsx
+++ b/src/views/process-panel/process-panel.tsx
@@ -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 {
@@ -32,6 +33,7 @@ const mapStateToProps = ({ router, resources, processPanel, processLogsPanel }:
subprocesses: subprocesses.filter(subprocess => processPanel.filters[getProcessStatus(subprocess)]),
filters: getFilters(processPanel, subprocesses),
processLogsPanel: processLogsPanel,
+ auth: auth,
};
};
@@ -52,6 +54,7 @@ const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps =>
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 --git a/yarn.lock b/yarn.lock
index 13ea553a..7fa499fd 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3798,6 +3798,7 @@ __metadata:
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
@@ -12019,6 +12020,15 @@ __metadata:
languageName: node
linkType: hard
+"mime at npm:^3.0.0":
+ version: 3.0.0
+ resolution: "mime at npm:3.0.0"
+ bin:
+ mime: cli.js
+ checksum: f43f9b7bfa64534e6b05bd6062961681aeb406a5b53673b53b683f27fcc4e739989941836a355eef831f4478923651ecc739f4a5f6e20a76487b432bfd4db928
+ languageName: node
+ linkType: hard
+
"mimic-fn at npm:^1.0.0":
version: 1.2.0
resolution: "mimic-fn at npm:1.2.0"
-----------------------------------------------------------------------
hooks/post-receive
--
More information about the arvados-commits
mailing list