},
event_type: 'stdout'
}).then(function(log) {
- cy.get('[data-cy=process-logs]')
+ cy.get('[data-cy=process-logs]', {timeout: 7000})
.should('not.contain', 'No logs yet')
.and('contain', 'hello world');
})
});
});
+ it('shows process details', function() {
+ createContainerRequest(
+ activeUser,
+ `test_container_request ${Math.floor(Math.random() * 999999)}`,
+ 'arvados/jobs',
+ ['echo', 'hello world'],
+ false, 'Committed')
+ .then(function(containerRequest) {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
+ cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
+ cy.get('[data-cy=process-details-attributes-runtime-user]').should('not.exist');
+ });
+
+ // Fake submitted by another user
+ cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
+ req.reply((res) => {
+ res.body.modified_by_user_uuid = 'zzzzz-tpzed-000000000000000';
+ });
+ });
+
+ createContainerRequest(
+ activeUser,
+ `test_container_request ${Math.floor(Math.random() * 999999)}`,
+ 'arvados/jobs',
+ ['echo', 'hello world'],
+ false, 'Committed')
+ .then(function(containerRequest) {
+ cy.loginAs(activeUser);
+ cy.goToPath(`/processes/${containerRequest.uuid}`);
+ cy.get('[data-cy=process-details]').should('contain', containerRequest.name);
+ cy.get('[data-cy=process-details-attributes-modifiedby-user]').contains(`zzzzz-tpzed-000000000000000`);
+ cy.get('[data-cy=process-details-attributes-runtime-user]').contains(`Active User (${activeUser.user.uuid})`);
+ });
+ });
+
it('filters process logs by event type', function() {
const nodeInfoLogs = [
'Host Information',
cy.loginAs(activeUser);
cy.goToPath(`/processes/${containerRequest.uuid}`);
// Should show main logs by default
- cy.get('[data-cy=process-logs-filter]').should('contain', 'Main logs');
+ cy.get('[data-cy=process-logs-filter]', {timeout: 7000}).should('contain', 'Main logs');
cy.get('[data-cy=process-logs]')
.should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
.and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
cy.getAll('@containerRequest').then(function([containerRequest]) {
cy.goToPath(`/processes/${containerRequest.uuid}`);
- cy.get('[data-cy=process-runtime-status-retry-warning]')
+ cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
.should('contain', 'Process retried 1 time');
});
cy.getAll('@containerRequest').then(function([containerRequest]) {
containerCount = 3;
cy.goToPath(`/processes/${containerRequest.uuid}`);
- cy.get('[data-cy=process-runtime-status-retry-warning]')
+ cy.get('[data-cy=process-runtime-status-retry-warning]', {timeout: 7000})
.should('contain', 'Process retried 2 times');
});
});
"location": "keep:00000000000000000000000000000000+03/input3-2.txt"
}
]
+ },
+ {
+ "$import": "import_path"
}
]
}
"basename": "11111111111111111111111111111111+03",
"class": "Directory",
"location": "keep:11111111111111111111111111111111+03"
+ },
+ {
+ "$import": "import_path"
}
]
}
"input_int_array": [
1,
3,
- 5
+ 5,
+ {
+ "$import": "import_path"
+ }
]
}
},
input: {
"input_long_array": [
10,
- 20
+ 20,
+ {
+ "$import": "import_path"
+ }
]
}
},
"input_float_array": [
10.2,
10.4,
- 10.6
+ 10.6,
+ {
+ "$import": "import_path"
+ }
]
}
},
"input_double_array": [
20.1,
20.2,
- 20.3
+ 20.3,
+ {
+ "$import": "import_path"
+ }
]
}
},
"input_string_array": [
"Hello",
"World",
- "!"
+ "!",
+ {
+ "$import": "import_path"
+ }
]
}
+ },
+ {
+ definition: {
+ "id": "#main/input_bool_include",
+ "type": "boolean"
+ },
+ input: {
+ "input_bool_include": {
+ "$include": "include_path"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_int_include",
+ "type": "int"
+ },
+ input: {
+ "input_int_include": {
+ "$include": "include_path"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_float_include",
+ "type": "float"
+ },
+ input: {
+ "input_float_include": {
+ "$include": "include_path"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_string_include",
+ "type": "string"
+ },
+ input: {
+ "input_string_include": {
+ "$include": "include_path"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_file_include",
+ "type": "File"
+ },
+ input: {
+ "input_file_include": {
+ "$include": "include_path"
+ }
+ }
+ },
+ {
+ definition: {
+ "id": "#main/input_directory_include",
+ "type": "Directory"
+ },
+ input: {
+ "input_directory_include": {
+ "$include": "include_path"
+ }
+ }
}
];
verifyIOParameter('input_file_array', null, null, 'input2.tar', '00000000000000000000000000000000+02');
verifyIOParameter('input_file_array', null, null, 'input3.tar', undefined, true);
verifyIOParameter('input_file_array', null, null, 'input3-2.txt', undefined, true);
+ verifyIOParameter('input_file_array', null, null, 'Cannot display value', undefined, true);
verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+02');
verifyIOParameter('input_dir_array', null, null, '/', '11111111111111111111111111111111+03', true);
- verifyIOParameter('input_int_array', null, null, ["1", "3", "5"]);
- verifyIOParameter('input_long_array', null, null, ["10", "20"]);
- verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6"]);
- verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3"]);
- verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!"]);
+ verifyIOParameter('input_dir_array', null, null, 'Cannot display value', undefined, true);
+ verifyIOParameter('input_int_array', null, null, ["1", "3", "5", "Cannot display value"]);
+ verifyIOParameter('input_long_array', null, null, ["10", "20", "Cannot display value"]);
+ verifyIOParameter('input_float_array', null, null, ["10.2", "10.4", "10.6", "Cannot display value"]);
+ verifyIOParameter('input_double_array', null, null, ["20.1", "20.2", "20.3", "Cannot display value"]);
+ verifyIOParameter('input_string_array', null, null, ["Hello", "World", "!", "Cannot display value"]);
+ verifyIOParameter('input_bool_include', null, null, "Cannot display value");
+ verifyIOParameter('input_int_include', null, null, "Cannot display value");
+ verifyIOParameter('input_float_include', null, null, "Cannot display value");
+ verifyIOParameter('input_string_include', null, null, "Cannot display value");
+ verifyIOParameter('input_file_include', null, null, "Cannot display value");
+ verifyIOParameter('input_directory_include', null, null, "Cannot display value");
});
cy.get('[data-cy=process-io-card] h6').contains('Outputs')
.parents('[data-cy=process-io-card]').within((ctx) => {
//
// SPDX-License-Identifier: AGPL-3.0
-import { formatUploadSpeed } from "./formatters";
+import { formatUploadSpeed, formatContainerCost } from "./formatters";
describe('formatUploadSpeed', () => {
it('should show speed less than 1MB/s', () => {
// then
expect(result).toBe('5.23 MB/s');
- });
-});
\ No newline at end of file
+ });
+});
+
+describe('formatContainerCost', () => {
+ it('should correctly round to tenth of a cent', () => {
+ expect(formatContainerCost(0.0)).toBe('$0');
+ expect(formatContainerCost(0.125)).toBe('$0.125');
+ expect(formatContainerCost(0.1254)).toBe('$0.125');
+ expect(formatContainerCost(0.1255)).toBe('$0.126');
+ });
+
+ it('should round up any smaller value to 0.001', () => {
+ expect(formatContainerCost(0.0)).toBe('$0');
+ expect(formatContainerCost(0.001)).toBe('$0.001');
+ expect(formatContainerCost(0.0001)).toBe('$0.001');
+ expect(formatContainerCost(0.00001)).toBe('$0.001');
+ });
+});
}
return "";
};
+
+export const formatContainerCost = (cost: number): string => {
+ const decimalPlaces = 3;
+
+ const factor = Math.pow(10, decimalPlaces);
+ const rounded = Math.round(cost*factor)/factor;
+ if (cost > 0 && rounded === 0) {
+ // Display min value of 0.001
+ return `$${1/factor}`;
+ } else {
+ // Otherwise use rounded value to proper decimal places
+ return `$${rounded}`;
+ }
+};
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
searchBox: {
- paddingBottom: theme.spacing.unit * 2
+ paddingBottom: 0,
},
toolbar: {
- paddingTop: theme.spacing.unit,
- paddingRight: theme.spacing.unit * 2,
+ paddingTop: 0,
+ paddingRight: theme.spacing.unit,
},
footer: {
overflow: 'auto'
},
title: {
display: 'inline-block',
- paddingLeft: theme.spacing.unit * 3,
- paddingTop: theme.spacing.unit * 3,
+ paddingLeft: theme.spacing.unit * 2,
+ paddingTop: theme.spacing.unit * 2,
fontSize: '18px'
},
dataTable: {
},
headerMenu: {
float: 'right',
- display: 'inline-block'
+ display: 'inline-block',
}
});
(!hideColumnSelector || !hideSearchInput || !!actions) &&
<Grid className={classes.headerMenu} item xs>
<Toolbar className={classes.toolbar}>
- <Grid container justify="space-between" wrap="nowrap" alignItems="center">
- {!hideSearchInput && <div className={classes.searchBox}>
- {!hideSearchInput && <SearchInput
- label={searchLabel}
- value={searchValue}
- selfClearProp={currentItemUuid}
- onSearch={onSearch} />}
- </div>}
- {actions}
- {!hideColumnSelector && <ColumnSelector
- columns={columns}
- onColumnToggle={onColumnToggle} />}
- </Grid>
+ {!hideSearchInput && <div className={classes.searchBox}>
+ {!hideSearchInput && <SearchInput
+ label={searchLabel}
+ value={searchValue}
+ selfClearProp={currentItemUuid}
+ onSearch={onSearch} />}
+ </div>}
+ {actions}
+ {!hideColumnSelector && <ColumnSelector
+ columns={columns}
+ onColumnToggle={onColumnToggle} />}
{ doUnMaximizePanel && panelMaximized &&
<Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
<IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
panelRef?: MutableRefObject<any>;
forwardProps?: boolean;
maxHeight?: string;
+ minHeight?: string;
}
interface MPVPanelActionProps {
// Grid item compatible component for layout and MPV props passing
export const MPVPanelContent = ({doHidePanel, doMaximizePanel, doUnMaximizePanel, panelName,
- panelMaximized, panelIlluminated, panelRef, forwardProps, maxHeight,
+ panelMaximized, panelIlluminated, panelRef, forwardProps, maxHeight, minHeight,
...props}: MPVPanelContentProps) => {
useEffect(() => {
if (panelRef && panelRef.current) {
}
}, [panelRef]);
- const mh = panelMaximized
+ const maxH = panelMaximized
? '100%'
: maxHeight;
- return <Grid item style={{maxHeight: mh}} {...props}>
+ return <Grid item style={{maxHeight: maxH, minHeight}} {...props}>
<span ref={panelRef} /> {/* Element to scroll to when the panel is selected */}
<Paper style={{height: '100%'}} elevation={panelIlluminated ? 8 : 0}>
{ forwardProps
// SPDX-License-Identifier: AGPL-3.0
import React, {useState, useEffect} from 'react';
-import { IconButton, StyleRulesCallback, withStyles, WithStyles, FormControl, InputLabel, Input, InputAdornment, Tooltip } from '@material-ui/core';
+import {
+ IconButton,
+ FormControl,
+ InputLabel,
+ Input,
+ InputAdornment,
+ Tooltip,
+} from '@material-ui/core';
import SearchIcon from '@material-ui/icons/Search';
-type CssRules = 'container' | 'input' | 'button';
-
-const styles: StyleRulesCallback<CssRules> = theme => {
- return {
- container: {
- position: 'relative',
- width: '100%'
- },
- input: {
- border: 'none',
- borderRadius: theme.spacing.unit / 4,
- boxSizing: 'border-box',
- padding: theme.spacing.unit,
- paddingRight: theme.spacing.unit * 4,
- width: '100%',
- },
- button: {
- position: 'absolute',
- top: theme.spacing.unit / 2,
- right: theme.spacing.unit / 2,
- width: theme.spacing.unit * 3,
- height: theme.spacing.unit * 3
- }
- };
-};
-
interface SearchInputDataProps {
value: string;
label?: string;
debounce?: number;
}
-type SearchInputProps = SearchInputDataProps & SearchInputActionProps & WithStyles<CssRules>;
+type SearchInputProps = SearchInputDataProps & SearchInputActionProps;
export const DEFAULT_SEARCH_DEBOUNCE = 1000;
-const SearchInputComponent = (props: SearchInputProps) => {
+export const SearchInput = (props: SearchInputProps) => {
const [timeout, setTimeout] = useState(0);
const [value, setValue] = useState("");
const [label, setLabel] = useState("Search");
} />
</FormControl>
</form>;
-}
-
-export const SearchInput = withStyles(styles)(SearchInputComponent);
\ No newline at end of file
+};
description: string;
state: ContainerRequestState;
requestingContainerUuid: string | null;
+ cumulativeCost: number;
containerUuid: string | null;
containerCountMax: number;
mounts: {[path: string]: MountType};
environment: {};
cwd: string;
command: string[];
+ cost: number;
outputPath: string;
mounts: MountType[];
runtimeConstraints: RuntimeConstraints;
runtimeStatus: RuntimeStatus;
+ runtimeUserUuid: string;
schedulingParameters: SchedulingParameters;
output: string | null;
containerImage: string;
if (containerRequest.containerUuid) {
const container = await services.containerService.get(containerRequest.containerUuid);
dispatch<any>(updateResources([container]));
+ if (container.runtimeUserUuid) {
+ const runtimeUser = await services.userService.get(container.runtimeUserUuid);
+ dispatch<any>(updateResources([runtimeUser]));
+ }
return { containerRequest, container };
}
return { containerRequest };
"auth_uuid",
"command",
"container_image",
+ "cost",
"created_at",
"cwd",
"environment",
import React from "react";
import { Grid, StyleRulesCallback, withStyles } from "@material-ui/core";
import { Dispatch } from 'redux';
-import { formatDate } from "common/formatters";
+import { formatContainerCost, formatDate } from "common/formatters";
import { resourceLabel } from "common/labels";
import { DetailsAttribute } from "components/details-attribute/details-attribute";
import { ResourceKind } from "models/resource";
import { ArvadosTheme } from "common/custom-theme";
import { ProcessRuntimeStatus } from "views-components/process-runtime-status/process-runtime-status";
import { getPropertyChip } from "views-components/resource-properties-form/property-chip";
+import { ContainerRequestResource } from "models/container-request";
+import { filterResources } from "store/resources/resources";
type CssRules = 'link' | 'propertyTag';
});
const mapStateToProps = (state: RootState, props: { request: ProcessResource }) => {
+ const process = getProcess(props.request.uuid)(state.resources);
return {
- container: getProcess(props.request.uuid)(state.resources)?.container,
+ container: process?.container,
+ subprocesses: filterResources((resource: ContainerRequestResource) =>
+ resource.kind === ResourceKind.CONTAINER_REQUEST &&
+ resource.requestingContainerUuid === process?.containerRequest.containerUuid
+ )(state.resources),
};
};
export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
connect(mapStateToProps, mapDispatchToProps)(
- (props: { request: ProcessResource, container?: ContainerResource, twoCol?: boolean, hideProcessPanelRedundantFields?: boolean, classes: Record<CssRules, string> } & ProcessDetailsAttributesActionProps) => {
+ (props: { request: ProcessResource, container?: ContainerResource, subprocesses: ContainerRequestResource[], twoCol?: boolean, hideProcessPanelRedundantFields?: boolean, classes: Record<CssRules, string> } & ProcessDetailsAttributesActionProps) => {
const containerRequest = props.request;
const container = props.container;
+ const subprocesses = props.subprocesses;
const classes = props.classes;
const mdSize = props.twoCol ? 6 : 12;
const filteredPropertyKeys = Object.keys(containerRequest.properties)
<DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
</Grid>}
<Grid item xs={12} md={mdSize}>
- <DetailsAttribute label='Container Request UUID' linkToUuid={containerRequest.uuid} value={containerRequest.uuid} />
+ <DetailsAttribute label='Container request UUID' linkToUuid={containerRequest.uuid} value={containerRequest.uuid} />
</Grid>
<Grid item xs={12} md={mdSize}>
- <DetailsAttribute label='Docker Image locator'
+ <DetailsAttribute label='Docker image locator'
linkToUuid={containerRequest.containerImage} value={containerRequest.containerImage} />
</Grid>
<Grid item xs={12} md={mdSize}>
<ContainerRunTime uuid={containerRequest.uuid} />
</DetailsAttribute>
</Grid>
+ {(containerRequest && containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-modifiedby-user">
+ <DetailsAttribute
+ label='Submitted by' linkToUuid={containerRequest.modifiedByUserUuid}
+ uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
+ </Grid>}
+ {(container && container.runtimeUserUuid && container.runtimeUserUuid !== containerRequest.modifiedByUserUuid) && <Grid item xs={12} md={mdSize} data-cy="process-details-attributes-runtime-user">
+ <DetailsAttribute
+ label='Run as' linkToUuid={container.runtimeUserUuid}
+ uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
+ </Grid>}
<Grid item xs={12} md={mdSize}>
- <DetailsAttribute label='Requesting Container UUID' value={containerRequest.requestingContainerUuid || "(none)"} />
+ <DetailsAttribute label='Requesting container UUID' value={containerRequest.requestingContainerUuid || "(none)"} />
</Grid>
<Grid item xs={6}>
- <DetailsAttribute label='Output Collection' />
+ <DetailsAttribute label='Output collection' />
{containerRequest.outputUuid && <span onClick={() => props.navigateToOutput(containerRequest.outputUuid!)}>
<CollectionName className={classes.link} uuid={containerRequest.outputUuid} />
</span>}
</Grid>
+ {container && container.cost > 0 && <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Cost ' value={formatContainerCost(container.cost)} />
+ </Grid>}
+ {containerRequest && containerRequest.cumulativeCost > 0 && subprocesses.length > 0 && <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Container & subprocess cost' value={formatContainerCost(containerRequest.cumulativeCost)} />
+ </Grid>}
{containerRequest.properties.template_uuid &&
<Grid item xs={12} md={mdSize}>
<span onClick={() => props.openWorkflow(containerRequest.properties.template_uuid)}>
ImageOffIcon,
OutputIcon,
MaximizeIcon,
- UnMaximizeIcon
+ UnMaximizeIcon,
+ InfoIcon
} from 'components/icon/icon';
import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
import {
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: renderPrimitiveValue(boolValue, false) }] :
[{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: renderPrimitiveValue(intValue, false) }]
: [{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: renderPrimitiveValue(floatValue, false) }]:
[{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: renderPrimitiveValue(stringValue, false) }] :
[{display: <EmptyValue />}];
case isPrimitiveOfType(input, CWLType.FILE):
...secondaryFiles
];
const mainFilePdhUrl = mainFile ? getResourcePdhUrl(mainFile, pdh) : "";
-
return files.length ?
files.map((file, i) => fileToProcessIOValue(file, (i > 0), auth, pdh, (i > 0 ? mainFilePdhUrl : ""))) :
[{display: <EmptyValue />}];
case isPrimitiveOfType(input, CWLType.DIRECTORY):
const directory = (input as DirectoryCommandInputParameter).value;
-
return directory !== undefined &&
!(Array.isArray(directory) && directory.length === 0) ?
[directoryToProcessIOValue(directory, auth, pdh)] :
!(input.type instanceof Array) &&
input.type.type === 'enum':
const enumValue = (input as EnumCommandInputParameter).value;
-
- return enumValue !== undefined ?
- [{ display: <pre>{(input as EnumCommandInputParameter).value || ''}</pre> }] :
+ return enumValue !== undefined && enumValue ?
+ [{ display: <pre>{enumValue}</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: <>{strArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
[{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: <>{intArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
[{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: <>{floatArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
[{display: <EmptyValue />}];
case isArrayOfType(input, CWLType.FILE):
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 [];
+ return [{display: <UnsupportedValue />}];
+ }
+};
+
+const renderPrimitiveValue = (value: any, asChip: boolean) => {
+ const isObject = typeof value === 'object';
+ if (!isObject) {
+ return asChip ? <Chip label={String(value)} /> : <pre>{String(value)}</pre>;
+ } else {
+ return asChip ? <UnsupportedValueChip /> : <UnsupportedValue />;
}
};
};
const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: string): ProcessIOValue => {
+ if (isExternalValue(directory)) {return {display: <UnsupportedValue />}}
+
const normalizedDirectory = normalizeDirectoryLocation(directory);
return {
display: <KeepUrlPath auth={auth} res={normalizedDirectory} pdh={pdh}/>,
};
const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => {
+ if (isExternalValue(file)) {return {display: <UnsupportedValue />}}
+
const resourcePdh = getResourcePdhUrl(file, pdh);
return {
display: <KeepUrlPath auth={auth} res={file} pdh={pdh}/>,
}
};
+const isExternalValue = (val: any) =>
+ Object.keys(val).includes('$import') ||
+ Object.keys(val).includes('$include')
+
const EmptyValue = withStyles(styles)(
({classes}: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>
);
+const UnsupportedValue = withStyles(styles)(
+ ({classes}: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>
+);
+
+const UnsupportedValueChip = withStyles(styles)(
+ ({classes}: WithStyles<CssRules>) => <Chip icon={<InfoIcon />} label={"Cannot display value"} />
+);
+
const ImagePlaceholder = withStyles(styles)(
({classes}: WithStyles<CssRules>) => <span className={classes.imagePlaceholder}><ImageIcon /></span>
);
},
logViewer: {
height: '100%',
+ overflowY: 'scroll', // Required for MacOS's Safari -- See #19687
},
logViewerContainer: {
height: '100%',
onCopy={props.onCopyToClipboard}
process={process} />
</MPVPanelContent>
- <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-logs">
+ <MPVPanelContent forwardProps xs minHeight='50%' data-cy="process-logs">
<ProcessLogsCard
onCopy={props.onCopyToClipboard}
process={process}
import { getInitialProcessStatusFilters } from 'store/resource-type-filters/resource-type-filters';
import { ResourcesState } from 'store/resources/resources';
import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { StyleRulesCallback, Typography, WithStyles, withStyles } from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+
+type CssRules = 'iconHeader' | 'cardHeader';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ iconHeader: {
+ fontSize: '1.875rem',
+ color: theme.customs.colors.green700,
+ marginRight: theme.spacing.unit * 2,
+ },
+ cardHeader: {
+ display: 'flex',
+ },
+});
export enum SubprocessPanelColumnNames {
NAME = "Name",
'The current process may not have any or none matches current filtering.'
];
+type SubProcessesTitleProps = WithStyles<CssRules>;
+
+const SubProcessesTitle = withStyles(styles)(
+ ({classes}: SubProcessesTitleProps) =>
+ <div className={classes.cardHeader}>
+ <ProcessIcon className={classes.iconHeader} /><span></span>
+ <Typography noWrap variant='h6' color='inherit'>
+ Subprocesses
+ </Typography>
+ </div>
+);
+
export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps) => {
return <DataExplorer
id={SUBPROCESS_PANEL_ID}
doMaximizePanel={props.doMaximizePanel}
doUnMaximizePanel={props.doUnMaximizePanel}
panelMaximized={props.panelMaximized}
- panelName={props.panelName} />;
+ panelName={props.panelName}
+ title={<SubProcessesTitle/>} />;
};