Merge branch 'main' into 19482-wf-panel refs #19482
authorPeter Amstutz <peter.amstutz@curii.com>
Tue, 21 Mar 2023 20:26:19 +0000 (16:26 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Tue, 21 Mar 2023 20:26:19 +0000 (16:26 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

1  2 
cypress/integration/process.spec.js
src/views/process-panel/process-details-card.tsx

index f6fbffdc2e039cbe48fdfd3cd5817381522afd67,19544c9ca543bcbc257b823df48f2d2eaf97fa6f..bdb4fae61ae0f58062fab989752a348dac0adc7f
@@@ -1040,7 -1040,7 +1040,7 @@@ describe('Process tests', function() 
              cy.get('[data-cy=process-io-card] h6').contains('Outputs')
                  .parents('[data-cy=process-io-card]').within((ctx) => {
                      cy.get(ctx).scrollIntoView();
 -                    cy.get('[data-cy="io-preview-image-toggle"]').click();
 +                    cy.get('[data-cy="io-preview-image-toggle"]').click({waitForAnimations: false});
                      const outPdh = testOutputCollection.portable_data_hash;
  
                      verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
          });
  
          cy.get('[data-cy=process-details]').should('contain', copiedCrName);
-         cy.get('[data-cy=process-details]').find('button').contains('Run Process');
+         cy.get('[data-cy=process-details]').find('button').contains('Run');
+     });
+     const getFakeContainer = (fakeContainerUuid) => ({
+         href: `/containers/${fakeContainerUuid}`,
+         kind: "arvados#container",
+         etag: "ecfosljpnxfari9a8m7e4yv06",
+         uuid: fakeContainerUuid,
+         owner_uuid: "zzzzz-tpzed-000000000000000",
+         created_at: "2023-02-13T15:55:47.308915000Z",
+         modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
+         modified_by_user_uuid: "zzzzz-tpzed-000000000000000",
+         modified_at: "2023-02-15T19:12:45.987086000Z",
+         command: [
+           "arvados-cwl-runner",
+           "--api=containers",
+           "--local",
+           "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
+           "/var/lib/cwl/workflow.json#main",
+           "/var/lib/cwl/cwl.input.json",
+         ],
+         container_image: "4ad7d11381df349e464694762db14e04+303",
+         cwd: "/var/spool/cwl",
+         environment: {},
+         exit_code: null,
+         finished_at: null,
+         locked_by_uuid: null,
+         log: null,
+         output: null,
+         output_path: "/var/spool/cwl",
+         progress: null,
+         runtime_constraints: {
+           API: true,
+           cuda: {
+             device_count: 0,
+             driver_version: "",
+             hardware_capability: "",
+           },
+           keep_cache_disk: 2147483648,
+           keep_cache_ram: 0,
+           ram: 1342177280,
+           vcpus: 1,
+         },
+         runtime_status: {},
+         started_at: null,
+         auth_uuid: null,
+         scheduling_parameters: {
+           max_run_time: 0,
+           partitions: [],
+           preemptible: false,
+         },
+         runtime_user_uuid: "zzzzz-tpzed-vllbpebicy84rd5",
+         runtime_auth_scopes: ["all"],
+         lock_count: 2,
+         gateway_address: null,
+         interactive_session_started: false,
+         output_storage_classes: ["default"],
+         output_properties: {},
+         cost: 0.0,
+         subrequests_cost: 0.0,
+       });
+     it('shows cancel button when appropriate', function() {
+         // Ignore collection requests
+         cy.intercept({method: 'GET', url: `**/arvados/v1/collections/*`}, {
+             statusCode: 200,
+             body: {}
+         });
+         // Uncommitted container
+         const crUncommitted = `Test process ${Math.floor(Math.random() * 999999)}`;
+         createContainerRequest(
+             activeUser,
+             crUncommitted,
+             'arvados/jobs',
+             ['echo', 'hello world'],
+             false, 'Uncommitted')
+         .then(function(containerRequest) {
+             // Navigate to process and verify run / cancel button
+             cy.goToPath(`/processes/${containerRequest.uuid}`);
+             cy.waitForDom();
+             cy.get('[data-cy=process-details]').should('contain', crUncommitted);
+             cy.get('[data-cy=process-run-button]').should('exist');
+             cy.get('[data-cy=process-cancel-button]').should('not.exist');
+         });
+         // Queued container
+         const crQueued = `Test process ${Math.floor(Math.random() * 999999)}`;
+         const fakeCrUuid = 'zzzzz-dz642-000000000000001';
+         createContainerRequest(
+             activeUser,
+             crQueued,
+             'arvados/jobs',
+             ['echo', 'hello world'],
+             false, 'Committed')
+         .then(function(containerRequest) {
+             // Fake container uuid
+             cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
+                 req.reply((res) => {
+                     res.body.output_uuid = fakeCrUuid;
+                     res.body.priority = 500;
+                     res.body.state = "Committed";
+                 });
+             });
+             // Fake container
+             const container = getFakeContainer(fakeCrUuid);
+             cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrUuid}`}, {
+                 statusCode: 200,
+                 body: {...container, state: "Queued", priority: 500}
+             });
+             // Navigate to process and verify cancel button
+             cy.goToPath(`/processes/${containerRequest.uuid}`);
+             cy.waitForDom();
+             cy.get('[data-cy=process-details]').should('contain', crQueued);
+             cy.get('[data-cy=process-cancel-button]').contains('Cancel');
+         });
+         // Locked container
+         const crLocked = `Test process ${Math.floor(Math.random() * 999999)}`;
+         const fakeCrLockedUuid = 'zzzzz-dz642-000000000000002';
+         createContainerRequest(
+             activeUser,
+             crLocked,
+             'arvados/jobs',
+             ['echo', 'hello world'],
+             false, 'Committed')
+         .then(function(containerRequest) {
+             // Fake container uuid
+             cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
+                 req.reply((res) => {
+                     res.body.output_uuid = fakeCrLockedUuid;
+                     res.body.priority = 500;
+                     res.body.state = "Committed";
+                 });
+             });
+             // Fake container
+             const container = getFakeContainer(fakeCrLockedUuid);
+             cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrLockedUuid}`}, {
+                 statusCode: 200,
+                 body: {...container, state: "Locked", priority: 500}
+             });
+             // Navigate to process and verify cancel button
+             cy.goToPath(`/processes/${containerRequest.uuid}`);
+             cy.waitForDom();
+             cy.get('[data-cy=process-details]').should('contain', crLocked);
+             cy.get('[data-cy=process-cancel-button]').contains('Cancel');
+         });
+         // On Hold container
+         const crOnHold = `Test process ${Math.floor(Math.random() * 999999)}`;
+         const fakeCrOnHoldUuid = 'zzzzz-dz642-000000000000003';
+         createContainerRequest(
+             activeUser,
+             crOnHold,
+             'arvados/jobs',
+             ['echo', 'hello world'],
+             false, 'Committed')
+         .then(function(containerRequest) {
+             // Fake container uuid
+             cy.intercept({method: 'GET', url: `**/arvados/v1/container_requests/${containerRequest.uuid}`}, (req) => {
+                 req.reply((res) => {
+                     res.body.output_uuid = fakeCrOnHoldUuid;
+                     res.body.priority = 0;
+                     res.body.state = "Committed";
+                 });
+             });
+             // Fake container
+             const container = getFakeContainer(fakeCrOnHoldUuid);
+             cy.intercept({method: 'GET', url: `**/arvados/v1/container/${fakeCrOnHoldUuid}`}, {
+                 statusCode: 200,
+                 body: {...container, state: "Queued", priority: 0}
+             });
+             // Navigate to process and verify cancel button
+             cy.goToPath(`/processes/${containerRequest.uuid}`);
+             cy.waitForDom();
+             cy.get('[data-cy=process-details]').should('contain', crOnHold);
+             cy.get('[data-cy=process-run-button]').should('exist');
+             cy.get('[data-cy=process-cancel-button]').should('not.exist');
+         });
      });
  
  });
index 13e82397edabe792b5f9aef024aa04ec8b61d401,15728eb61f971bc48d484064f121934bec20517e..f339d1b3dfab65cc22dc091eb6db2fb68a5257e3
@@@ -16,15 -16,14 +16,14 @@@ import 
      Button,
  } from '@material-ui/core';
  import { ArvadosTheme } from 'common/custom-theme';
- import { CloseIcon, MoreOptionsIcon, ProcessIcon, StartIcon } from 'components/icon/icon';
- import { Process } from 'store/processes/process';
+ import { CloseIcon, MoreOptionsIcon, ProcessIcon, StartIcon, StopIcon } from 'components/icon/icon';
+ import { Process, isProcessRunnable, isProcessResumable, isProcessCancelable } from 'store/processes/process';
  import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
  import { ProcessDetailsAttributes } from './process-details-attributes';
  import { ProcessStatus } from 'views-components/data-explorer/renderers';
- import { ContainerState } from 'models/container';
- import { ContainerRequestState } from 'models/container-request';
+ import classNames from 'classnames';
  
- type CssRules = 'card' | 'content' | 'title' | 'header' | 'cancelButton' | 'avatar' | 'iconHeader' | 'runButton';
+ type CssRules = 'card' | 'content' | 'title' | 'header' | 'cancelButton' | 'avatar' | 'iconHeader' | 'actionButton';
  
  const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
      card: {
          paddingTop: theme.spacing.unit * 0.5,
          color: theme.customs.colors.green700,
      },
+     actionButton: {
+         padding: "0px 5px 0 0",
+         marginRight: "5px",
+         fontSize: '0.78rem',
+     },
      cancelButton: {
-         paddingRight: theme.spacing.unit * 2,
-         fontSize: '14px',
          color: theme.customs.colors.red900,
-         "&:hover": {
-             cursor: 'pointer'
-         }
-     },
-     runButton: {
-         backgroundColor: theme.customs.colors.green700,
+         borderColor: theme.customs.colors.red900,
          '&:hover': {
-             backgroundColor: theme.customs.colors.green800,
+             borderColor: theme.customs.colors.red900,
+         },
+         '& svg': {
+             fontSize: '22px',
          },
-         padding: "0px 5px 0 0",
-         marginRight: "5px",
      },
  });
  
@@@ -76,13 -74,21 +74,21 @@@ export interface ProcessDetailsCardData
      process: Process;
      cancelProcess: (uuid: string) => void;
      startProcess: (uuid: string) => void;
+     resumeOnHoldWorkflow: (uuid: string) => void;
      onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
  }
  
  type ProcessDetailsCardProps = ProcessDetailsCardDataProps & WithStyles<CssRules> & MPVPanelProps;
  
  export const ProcessDetailsCard = withStyles(styles)(
-     ({ cancelProcess, startProcess, onContextMenu, classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
+     ({ cancelProcess, startProcess, resumeOnHoldWorkflow, onContextMenu, classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
+         let runAction: ((uuid: string) => void) | undefined = undefined;
+         if (isProcessRunnable(process)) {
+             runAction = startProcess;
+         } else if (isProcessResumable(process)) {
+             runAction = resumeOnHoldWorkflow;
+         }
          return <Card className={classes.card}>
              <CardHeader
                  className={classes.header}
                      </Tooltip>}
                  action={
                      <div>
-                         {process.containerRequest.state === ContainerRequestState.UNCOMMITTED &&
+                         {runAction !== undefined &&
                              <Button
+                                 data-cy="process-run-button"
                                  variant="contained"
                                  size="small"
                                  color="primary"
-                                 className={classes.runButton}
-                                 onClick={() => startProcess(process.containerRequest.uuid)}>
+                                 className={classes.actionButton}
+                                 onClick={() => runAction && runAction(process.containerRequest.uuid)}>
                                  <StartIcon />
-                                 Run Process
+                                 Run
+                             </Button>}
+                         {isProcessCancelable(process) &&
+                             <Button
+                                 data-cy="process-cancel-button"
+                                 variant="outlined"
+                                 size="small"
+                                 color="primary"
+                                 className={classNames(classes.actionButton, classes.cancelButton)}
+                                 onClick={() => cancelProcess(process.containerRequest.uuid)}>
+                                 <StopIcon />
+                                 Cancel
                              </Button>}
-                         {process.container && process.container.state === ContainerState.RUNNING &&
-                             <span className={classes.cancelButton} onClick={() => cancelProcess(process.containerRequest.uuid)}>Cancel</span>}
                          <ProcessStatus uuid={process.containerRequest.uuid} />
                          <Tooltip title="More options" disableFocusListener>
                              <IconButton
                                  <MoreOptionsIcon />
                              </IconButton>
                          </Tooltip>
 -                        { doHidePanel &&
 -                        <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
 -                            <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
 -                        </Tooltip> }
 +                        {doHidePanel &&
 +                            <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
 +                                <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
 +                            </Tooltip>}
                      </div>
                  } />
              <CardContent className={classes.content}>