Merge branch '19627-add-user' refs #19627
authorPeter Amstutz <peter.amstutz@curii.com>
Mon, 14 Nov 2022 18:38:08 +0000 (13:38 -0500)
committerPeter Amstutz <peter.amstutz@curii.com>
Mon, 14 Nov 2022 18:38:08 +0000 (13:38 -0500)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

14 files changed:
cypress/integration/process.spec.js
src/common/formatters.test.ts
src/common/formatters.ts
src/components/data-explorer/data-explorer.tsx
src/components/multi-panel-view/multi-panel-view.tsx
src/components/search-input/search-input.tsx
src/models/container-request.ts
src/models/container.ts
src/store/processes/processes-actions.ts
src/views/process-panel/process-details-attributes.tsx
src/views/process-panel/process-io-card.tsx
src/views/process-panel/process-log-card.tsx
src/views/process-panel/process-panel-root.tsx
src/views/subprocess-panel/subprocess-panel-root.tsx

index 84c786bdef42e74f7eb2f85060bf789d0bf7aa67..732310f78266afcd5e12091828b595710f5869ff 100644 (file)
@@ -106,13 +106,50 @@ describe('Process tests', function() {
                 },
                 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',
@@ -169,7 +206,7 @@ describe('Process tests', function() {
                 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)])
@@ -263,14 +300,14 @@ describe('Process tests', function() {
 
         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');
         });
     });
@@ -403,6 +440,9 @@ describe('Process tests', function() {
                                 "location": "keep:00000000000000000000000000000000+03/input3-2.txt"
                             }
                         ]
+                    },
+                    {
+                        "$import": "import_path"
                     }
                 ]
             }
@@ -426,6 +466,9 @@ describe('Process tests', function() {
                         "basename": "11111111111111111111111111111111+03",
                         "class": "Directory",
                         "location": "keep:11111111111111111111111111111111+03"
+                    },
+                    {
+                        "$import": "import_path"
                     }
                 ]
             }
@@ -442,7 +485,10 @@ describe('Process tests', function() {
                 "input_int_array": [
                     1,
                     3,
-                    5
+                    5,
+                    {
+                        "$import": "import_path"
+                    }
                 ]
             }
         },
@@ -457,7 +503,10 @@ describe('Process tests', function() {
             input: {
                 "input_long_array": [
                     10,
-                    20
+                    20,
+                    {
+                        "$import": "import_path"
+                    }
                 ]
             }
         },
@@ -473,7 +522,10 @@ describe('Process tests', function() {
                 "input_float_array": [
                     10.2,
                     10.4,
-                    10.6
+                    10.6,
+                    {
+                        "$import": "import_path"
+                    }
                 ]
             }
         },
@@ -489,7 +541,10 @@ describe('Process tests', function() {
                 "input_double_array": [
                     20.1,
                     20.2,
-                    20.3
+                    20.3,
+                    {
+                        "$import": "import_path"
+                    }
                 ]
             }
         },
@@ -505,9 +560,78 @@ describe('Process tests', function() {
                 "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"
+                }
+            }
         }
     ];
 
@@ -883,13 +1007,21 @@ describe('Process tests', function() {
                     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) => {
index 83177e2207da3bde728dba28b1c6f48700ff20e5..048779727e4865724e1bdcd67e862d3012e6a361 100644 (file)
@@ -2,7 +2,7 @@
 //
 // 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', () => {
@@ -25,5 +25,21 @@ describe('formatUploadSpeed', () => {
 
         // 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');
+    });
+});
index 6d0a7e491e4e508384ccbad42f66cc2eb3f8c195..1fbf17103941f2d7bf819a5a66940a51f832c515 100644 (file)
@@ -99,3 +99,17 @@ export const formatPropertyValue = (pv: PropertyValue, vocabulary?: Vocabulary)
     }
     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}`;
+    }
+};
index 0253201120e24db61f00c6840ba86f1ee5bf0f77..c7a296a60f4aebe5d6ad946265484be9ae148620 100644 (file)
@@ -25,11 +25,11 @@ type CssRules = 'searchBox' | 'headerMenu' | "toolbar" | "footer" | "root" | 'mo
 
 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'
@@ -42,8 +42,8 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     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: {
@@ -55,7 +55,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     headerMenu: {
         float: 'right',
-        display: 'inline-block'
+        display: 'inline-block',
     }
 });
 
@@ -169,19 +169,17 @@ export const DataExplorer = withStyles(styles)(
                             (!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>
index f0cbcf56be67b3e826150db35f9fccd8dc6c3187..877061de37f567bc6a3824451cb2f6951d8a8e99 100644 (file)
@@ -67,6 +67,7 @@ interface MPVPanelDataProps {
     panelRef?: MutableRefObject<any>;
     forwardProps?: boolean;
     maxHeight?: string;
+    minHeight?: string;
 }
 
 interface MPVPanelActionProps {
@@ -82,7 +83,7 @@ type MPVPanelContentProps = {children: ReactElement} & MPVPanelProps & GridProps
 
 // 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) {
@@ -90,11 +91,11 @@ export const MPVPanelContent = ({doHidePanel, doMaximizePanel, doUnMaximizePanel
         }
     }, [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
index 8d86307ab71ac072ea676173b6098bcf5252c0d3..6d85ed22ec7cb97eaf477e57a1d7942ed7a56fff 100644 (file)
@@ -3,35 +3,16 @@
 // 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;
@@ -43,11 +24,11 @@ interface SearchInputActionProps {
     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");
@@ -114,6 +95,4 @@ const SearchInputComponent = (props: SearchInputProps) => {
                 } />
         </FormControl>
     </form>;
-}
-
-export const SearchInput = withStyles(styles)(SearchInputComponent);
\ No newline at end of file
+};
index 99ec4cf086ad6acfd3102f5e3dfd77b6c7f14867..dc6bd84fb01690e857e740f3e0775c02fc0ed6b9 100644 (file)
@@ -19,6 +19,7 @@ export interface ContainerRequestResource extends Resource, ResourceWithProperti
     description: string;
     state: ContainerRequestState;
     requestingContainerUuid: string | null;
+    cumulativeCost: number;
     containerUuid: string | null;
     containerCountMax: number;
     mounts: {[path: string]: MountType};
index 127c250886f1b1c5086080bb006ece4f0a7e7308..c86f11cee1b4ebeadd33f2dbd01ee042ff2bb693 100644 (file)
@@ -25,10 +25,12 @@ export interface ContainerResource extends Resource {
     environment: {};
     cwd: string;
     command: string[];
+    cost: number;
     outputPath: string;
     mounts: MountType[];
     runtimeConstraints: RuntimeConstraints;
     runtimeStatus: RuntimeStatus;
+    runtimeUserUuid: string;
     schedulingParameters: SchedulingParameters;
     output: string | null;
     containerImage: string;
index 458efa205f44104d59b9a20862215fa7ff131b06..f7822e06ee064929cba111529311d5e5c987f051 100644 (file)
@@ -35,6 +35,10 @@ export const loadProcess = (containerRequestUuid: 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 };
@@ -56,6 +60,7 @@ const containerFieldsNoMounts = [
     "auth_uuid",
     "command",
     "container_image",
+    "cost",
     "created_at",
     "cwd",
     "environment",
index 4892eb33025bd7f841a1c6645fa3470af5300489..7bdf889797babf88d816a17935d3848388ddb2d5 100644 (file)
@@ -5,7 +5,7 @@
 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";
@@ -19,6 +19,8 @@ import { navigateToOutput, openWorkflow } from "store/process-panel/process-pane
 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';
 
@@ -37,8 +39,13 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
 });
 
 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),
     };
 };
 
@@ -54,9 +61,10 @@ const mapDispatchToProps = (dispatch: Dispatch): ProcessDetailsAttributesActionP
 
 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)
@@ -69,10 +77,10 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                     <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}>
@@ -100,15 +108,31 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                         <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 &amp; 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)}>
index 904276595361360f0069cbd7f26e40690f6002ea..4f93f48c2e75552134514a1165eb38f11c8aad6b 100644 (file)
@@ -34,7 +34,8 @@ import {
     ImageOffIcon,
     OutputIcon,
     MaximizeIcon,
-    UnMaximizeIcon
+    UnMaximizeIcon,
+    InfoIcon
 } from 'components/icon/icon';
 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 import {
@@ -484,37 +485,33 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
     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):
@@ -526,14 +523,12 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
                 ...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)] :
@@ -543,31 +538,28 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
             !(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):
@@ -591,13 +583,21 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
 
         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 />;
     }
 };
 
@@ -668,6 +668,8 @@ const normalizeDirectoryLocation = (directory: Directory): Directory => {
 };
 
 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}/>,
@@ -676,6 +678,8 @@ const directoryToProcessIOValue = (directory: Directory, auth: AuthState, 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}/>,
@@ -685,10 +689,22 @@ const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, p
     }
 };
 
+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>
 );
index 03739699b09c8f15d2b58cf3499ce78d6972f2d6..4890c726f4c73c601f20a526400abfe6f765bd01 100644 (file)
@@ -55,6 +55,7 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
     logViewer: {
         height: '100%',
+        overflowY: 'scroll', // Required for MacOS's Safari -- See #19687
     },
     logViewerContainer: {
         height: '100%',
index bc485d9f9dac5c454a3f82f51ebc6d9d03c8ab73..ed808b361a14c68b55121da7c1d34c0cade1daee 100644 (file)
@@ -121,7 +121,7 @@ export const ProcessPanelRoot = withStyles(styles)(
                     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}
index 52fbc51ffbee2c7ef68f7f9f487ddf971e12a583..c27673d268123e6ac7e4df421d5bfe217a57f887 100644 (file)
@@ -17,6 +17,21 @@ import { createTree } from 'models/tree';
 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",
@@ -80,6 +95,18 @@ const DEFAULT_VIEW_MESSAGES = [
     '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}
@@ -93,5 +120,6 @@ export const SubprocessPanelRoot = (props: SubprocessPanelProps & MPVPanelProps)
         doMaximizePanel={props.doMaximizePanel}
         doUnMaximizePanel={props.doUnMaximizePanel}
         panelMaximized={props.panelMaximized}
-        panelName={props.panelName} />;
+        panelName={props.panelName}
+        title={<SubProcessesTitle/>} />;
 };