});
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');
+ });
});
});
import RemoveCircleOutline from '@material-ui/icons/RemoveCircleOutline';
import NotInterested from '@material-ui/icons/NotInterested';
import Image from '@material-ui/icons/Image';
+import Stop from '@material-ui/icons/Stop';
// Import FontAwesome icons
import { library } from '@fortawesome/fontawesome-svg-core';
export const InactiveIcon: IconType = (props) => <NotInterested {...props} />;
export const ImageIcon: IconType = (props) => <Image {...props} />;
export const StartIcon: IconType = (props) => <PlayArrow {...props} />;
+export const StopIcon: IconType = (props) => <Stop {...props} />;
}
};
+export const isProcessRunnable = ({ containerRequest }: Process): boolean => (
+ containerRequest.state === ContainerRequestState.UNCOMMITTED
+);
+
+export const isProcessResumable = ({ containerRequest, container }: Process): boolean => (
+ containerRequest.state === ContainerRequestState.COMMITTED &&
+ containerRequest.priority === 0 &&
+ // Don't show run button when container is present & running or cancelled
+ !(container && (container.state === ContainerState.RUNNING ||
+ container.state === ContainerState.CANCELLED ||
+ container.state === ContainerState.COMPLETE))
+);
+
+export const isProcessCancelable = ({ containerRequest, container }: Process): boolean => (
+ containerRequest.priority !== null &&
+ containerRequest.priority > 0 &&
+ container !== undefined &&
+ (container.state === ContainerState.QUEUED ||
+ container.state === ContainerState.LOCKED ||
+ container.state === ContainerState.RUNNING)
+);
+
const isSubprocess = (containerUuid: string) => (resource: Resource) =>
resource.kind === ResourceKind.CONTAINER_REQUEST
&& (resource as ContainerRequestResource).requestingContainerUuid === containerUuid;
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
try {
const process = await services.containerRequestService.update(uuid, { priority: 0 });
+ dispatch<any>(updateResources([process]));
+ if (process.containerUuid) {
+ const container = await services.containerService.get(process.containerUuid, false);
+ dispatch<any>(updateResources([container]));
+ }
return process;
} catch (e) {
throw new Error('Could not cancel the process.');
}
};
+export const resumeOnHoldWorkflow = (uuid: string) =>
+ async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+ try {
+ const process = await services.containerRequestService.update(uuid, { priority: 500 });
+ dispatch<any>(updateResources([process]));
+ if (process.containerUuid) {
+ const container = await services.containerService.get(process.containerUuid, false);
+ dispatch<any>(updateResources([container]));
+ }
+ return process;
+ } catch (e) {
+ throw new Error('Could not resume the process.');
+ }
+ };
+
export const startWorkflow = (uuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
try {
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",
},
});
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
onToggle: (status: string) => void;
cancelProcess: (uuid: string) => void;
startProcess: (uuid: string) => void;
+ resumeOnHoldWorkflow: (uuid: string) => void;
onLogFilterChange: (filter: FilterOption) => void;
navigateToLog: (uuid: string) => void;
onCopyToClipboard: (uuid: string) => void;
onContextMenu={event => props.onContextMenu(event, process)}
cancelProcess={props.cancelProcess}
startProcess={props.startProcess}
+ resumeOnHoldWorkflow={props.resumeOnHoldWorkflow}
/>
</MPVPanelContent>
<MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
updateOutputParams,
loadNodeJson
} from 'store/process-panel/process-panel-actions';
-import { cancelRunningWorkflow, startWorkflow } from 'store/processes/processes-actions';
+import { cancelRunningWorkflow, resumeOnHoldWorkflow, startWorkflow } 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';
},
cancelProcess: (uuid) => dispatch<any>(cancelRunningWorkflow(uuid)),
startProcess: (uuid) => dispatch<any>(startWorkflow(uuid)),
+ resumeOnHoldWorkflow: (uuid) => dispatch<any>(resumeOnHoldWorkflow(uuid)),
onLogFilterChange: (filter) => dispatch(setProcessLogsPanelFilter(filter.value)),
navigateToLog: (uuid) => dispatch<any>(navigateToLogCollection(uuid)),
loadInputs: (containerRequest) => dispatch<any>(loadInputs(containerRequest)),