cy.get('[data-cy=run-process-next-button]').click();
- cy.get('[data-cy=new-process-panel]').contains('Run Process').should('be.disabled');
+ cy.get('[data-cy=new-process-panel]').contains('Run workflow').should('be.disabled');
cy.get('[data-cy=new-process-panel]')
.within(() => {
cy.get('[name=name]').type(`Workflow name (${Math.floor(Math.random() * 999999)})`);
- cy.get('[readonly]').click();
+ cy.contains('input').next().click();
});
cy.get('[data-cy=choose-a-file-dialog]').as('chooseFileDialog');
cy.get('@chooseFileDialog').find('button').contains('Ok').click();
cy.get('[data-cy=new-process-panel]')
- .find('button').contains('Run Process').should('not.be.disabled');
+ .find('button').contains('Run workflow').should('not.be.disabled');
});
});
cy.get('[data-cy=side-panel-button]').click();
- cy.get('#aside-menu-list').contains('Run a process').click();
+ cy.get('#aside-menu-list').contains('Run a workflow').click();
cy.get('@testWorkflow')
.then((testWorkflow) => {
cy.get('main').contains(testWorkflow.name).click();
cy.get('[data-cy=run-process-next-button]').click();
- cy.get('label').contains('#main/foo').parent('div').find('input').click();
+ cy.get('label').contains('foo').parent('div').find('input').click();
cy.get('div[role=dialog]')
.within(() => {
cy.get('p').contains('Projects').closest('div[role=button]')
cy.get('[data-cy=ok-button]').click();
});
- cy.get('label').contains('#main/bar').parent('div').find('input').click();
+ cy.get('label').contains('bar').parent('div').find('input').click();
cy.get('div[role=dialog]')
.within(() => {
cy.get('p').contains('Projects').closest('div[role=button]')
});
});
- cy.get('label').contains('#main/foo').parent('div')
+ cy.get('label').contains('foo').parent('div')
.within(() => {
cy.contains('baz');
cy.contains('bar');
});
- cy.get('label').contains('#main/bar').parent('div')
+ cy.get('label').contains('bar').parent('div')
.within(() => {
cy.contains(testCollection.name);
cy.contains(testCollection2.name);
cy.get('[data-cy=side-panel-button]').click();
- cy.get('#aside-menu-list').contains('Run a process').click();
+ cy.get('#aside-menu-list').contains('Run a workflow').click();
cy.get('@testWorkflow')
.then((testWorkflow) => {
cy.get('main').contains(testWorkflow.name).click();
cy.get('[data-cy=run-process-next-button]').click();
- cy.get('[readonly]').click();
+ cy.get('[data-cy=new-process-panel]')
+ .within(() => {
+ cy.contains('input').next().click();
+ });
cy.get('[data-cy=choose-a-file-dialog]').as('chooseFileDialog');
cy.get('[data-cy=projects-tree-favourites-tree-picker]').contains('Favorites').closest('ul').find('i').click();
cy.get('@chooseFileDialog').find(`[data-id=${mySharedWritableProject.uuid}]`);
cy.get('main').contains(testWorkflow2.name).click();
cy.get('button').contains('Change Workflow').click();
cy.get('[data-cy=run-process-next-button]').click();
- cy.get('[readonly]').click();
+ cy.get('[data-cy=new-process-panel]')
+ .within(() => {
+ cy.contains('image_collection').next().click();
+ });
cy.get('[data-cy=choose-a-directory-dialog]').as('chooseDirectoryDialog');
cy.get('[data-cy=projects-tree-favourites-tree-picker]').contains('Favorites').closest('ul').find('i').click();
cy.get('@chooseDirectoryDialog').find(`[data-id=${mySharedWritableProject.uuid}]`);
{url: '/shared-with-me', label: 'Shared with me'},
{url: '/public-favorites', label: 'Public Favorites'},
{url: '/favorites', label: 'My Favorites'},
- {url: '/workflows', label: 'Workflows'},
{url: '/all_processes', label: 'All Processes'},
{url: '/trash', label: 'Trash'},
].map(function(section) {
return "Group";
case ResourceKind.VIRTUAL_MACHINE:
return "Virtual Machine";
+ case ResourceKind.WORKFLOW:
+ return "Workflow";
default:
return "Unknown";
}
import { loadFileViewersConfig } from 'store/file-viewers/file-viewers-actions';
import { filterGroupAdminActionSet, projectAdminActionSet } from 'views-components/context-menu/action-sets/project-admin-action-set';
import { permissionEditActionSet } from 'views-components/context-menu/action-sets/permission-edit-action-set';
+import { workflowActionSet } from 'views-components/context-menu/action-sets/workflow-action-set';
import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
import { storeRedirects } from './common/redirect-to';
addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
+addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
storeRedirects();
import { ProcessResource } from "./process";
import { EmptyResource } from "./empty";
import { CollectionFile, CollectionDirectory } from 'models/collection-file';
+import { WorkflowResource } from 'models/workflow';
-export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory;
+export type DetailsResource = ProjectResource | CollectionResource | ProcessResource | EmptyResource | CollectionFile | CollectionDirectory | WorkflowResource;
export const getWorkflow = (workflowDefinition: WorkflowResourceDefinition) => {
if (!workflowDefinition.$graph) { return undefined; }
- const mainWorkflow = workflowDefinition.$graph.find(item => item.class === 'Workflow' && item.id === '#main');
+ const mainWorkflow = workflowDefinition.$graph.find(item => item.id === '#main');
return mainWorkflow
? mainWorkflow
: undefined;
};
export const getInputLabel = (input: CommandInputParameter) => {
- return `${input.label || input.id}`;
+ return `${input.label || input.id.split('/').pop()}`;
};
export const isRequiredInput = ({ type }: CommandInputParameter) => {
const runProcessMatch = Routes.matchRunProcessRoute(pathname);
const virtualMachineUserMatch = Routes.matchUserVirtualMachineRoute(pathname);
const virtualMachineAdminMatch = Routes.matchAdminVirtualMachineRoute(pathname);
- const workflowMatch = Routes.matchWorkflowRoute(pathname);
const sshKeysUserMatch = Routes.matchSshKeysUserRoute(pathname);
const sshKeysAdminMatch = Routes.matchSshKeysAdminRoute(pathname);
const siteManagerMatch = Routes.matchSiteManagerRoute(pathname);
store.dispatch(WorkbenchActions.loadSharedWithMe);
} else if (runProcessMatch) {
store.dispatch(WorkbenchActions.loadRunProcess);
- } else if (workflowMatch) {
- store.dispatch(WorkbenchActions.loadWorkflow);
} else if (searchResultsMatch) {
store.dispatch(WorkbenchActions.loadSearchResults);
} else if (virtualMachineUserMatch) {
name: res.name,
description: res.description,
outputUuid: res.outputUuid || '',
- workflowUuid: res.properties.workflowUuid || '',
+ workflowUuid: res.properties.template_uuid || '',
menuKind: ContextMenuKind.PROCESS_RESOURCE
}));
}
return ContextMenuKind.ROOT_PROJECT;
case ResourceKind.LINK:
return ContextMenuKind.LINK;
+ case ResourceKind.WORKFLOW:
+ return ContextMenuKind.WORKFLOW;
default:
return;
}
import { SidePanelTreeCategory } from '../side-panel-tree/side-panel-tree-actions';
import { Routes, getGroupUrl, getNavUrl, getUserProfileUrl } from 'routes/routes';
import { RootState } from 'store/store';
+import { openDetailsPanel } from 'store/details-panel/details-panel-action';
import { ServiceRepository } from 'services/services';
import { pluginConfig } from 'plugins';
import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
case ResourceKind.VIRTUAL_MACHINE:
dispatch<any>(navigateToAdminVirtualMachines);
return;
+ case ResourceKind.WORKFLOW:
+ dispatch<any>(openDetailsPanel(uuid));
+ return;
}
switch (uuid) {
case SidePanelTreeCategory.SHARED_WITH_ME:
dispatch(navigateToSharedWithMe);
return;
- case SidePanelTreeCategory.WORKFLOWS:
- dispatch(navigateToWorkflows);
- return;
case SidePanelTreeCategory.TRASH:
dispatch(navigateToTrash);
return;
import { RunProcessAdvancedFormData, RUN_PROCESS_ADVANCED_FORM } from "views/run-process-panel/run-process-advanced-form";
import { MOUNT_PATH_CWL_WORKFLOW, MOUNT_PATH_CWL_INPUT } from 'models/process';
import { getWorkflow, getWorkflowInputs } from "models/workflow";
+import { ProjectResource } from "models/project";
+import { UserResource } from "models/user";
export const loadProcess = (containerRequestUuid: string) =>
async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
const stringifiedDefinition = JSON.stringify(process.mounts[MOUNT_PATH_CWL_WORKFLOW].content);
const newWorkflow = { ...workflow, definition: stringifiedDefinition };
- const basicInitialData: RunProcessBasicFormData = { name: `Copy of: ${process.name}`, description: process.description };
+ const owner = getResource<ProjectResource | UserResource>(workflow.ownerUuid)(getState().resources);
+ const basicInitialData: RunProcessBasicFormData = { name: `Copy of: ${process.name}`, description: process.description, owner };
dispatch<any>(initialize(RUN_PROCESS_BASIC_FORM, basicInitialData));
const advancedInitialData: RunProcessAdvancedFormData = {
const filters = getInitialResourceTypeFilters();
const serializedFilters = serializeResourceTypeFilters(filters);
expect(serializedFilters)
- .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.COLLECTION}"]]`);
+ .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.COLLECTION}","${ResourceKind.WORKFLOW}"]]`);
});
it("should serialize all but collection filters", () => {
const filters = deselectNode(ObjectTypeFilter.COLLECTION)(getInitialResourceTypeFilters());
const serializedFilters = serializeResourceTypeFilters(filters);
expect(serializedFilters)
- .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}"]]`);
+ .toEqual(`["uuid","is_a",["${ResourceKind.PROJECT}","${ResourceKind.PROCESS}","${ResourceKind.WORKFLOW}"]]`);
});
it("should serialize output collections and projects", () => {
const filters = pipe(
() => getInitialResourceTypeFilters(),
deselectNode(ObjectTypeFilter.PROCESS),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
deselectNode(CollectionTypeFilter.LOG_COLLECTION),
deselectNode(CollectionTypeFilter.INTERMEDIATE_COLLECTION),
const filters = pipe(
() => getInitialResourceTypeFilters(),
deselectNode(ObjectTypeFilter.PROCESS),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
deselectNode(CollectionTypeFilter.LOG_COLLECTION),
deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION),
() => getInitialResourceTypeFilters(),
deselectNode(ObjectTypeFilter.PROJECT),
deselectNode(ObjectTypeFilter.PROCESS),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
)();
() => getInitialResourceTypeFilters(),
deselectNode(ObjectTypeFilter.PROJECT),
deselectNode(ProcessTypeFilter.CHILD_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION)
+ deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
() => getInitialResourceTypeFilters(),
deselectNode(ObjectTypeFilter.PROJECT),
deselectNode(ProcessTypeFilter.MAIN_PROCESS),
- deselectNode(ObjectTypeFilter.COLLECTION)
+ deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
() => getInitialResourceTypeFilters(),
deselectNode(ObjectTypeFilter.PROCESS),
deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
deselectNode(GroupTypeFilter.PROJECT),
deselectNode(ObjectTypeFilter.PROCESS),
deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
deselectNode(GroupTypeFilter.FILTER_GROUP),
deselectNode(ObjectTypeFilter.PROCESS),
deselectNode(ObjectTypeFilter.COLLECTION),
+ deselectNode(ObjectTypeFilter.WORKFLOW),
)();
const serializedFilters = serializeResourceTypeFilters(filters);
PROJECT = 'Project',
PROCESS = 'Process',
COLLECTION = 'Data collection',
+ WORKFLOW = 'Workflow',
}
export enum GroupTypeFilter {
initFilter(ObjectTypeFilter.PROJECT),
initFilter(ObjectTypeFilter.PROCESS),
initFilter(ObjectTypeFilter.COLLECTION),
+ initFilter(ObjectTypeFilter.WORKFLOW),
);
// Using pipe() with more than 7 arguments makes the return type be 'any',
initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION),
initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
),
+ initFilter(ObjectTypeFilter.WORKFLOW)
+
);
export const getInitialProcessTypeFilters = pipe(
return ResourceKind.PROCESS;
case ObjectTypeFilter.COLLECTION:
return ResourceKind.COLLECTION;
+ case ObjectTypeFilter.WORKFLOW:
+ return ResourceKind.WORKFLOW;
}
};
email: "test@gmail.com",
firstName: "TestFirstName",
lastName: "TestLastName",
- uuid: "ce8i5-tpzed-yid70bw31f51234",
- ownerUuid: "ce8i5-tpzed-000000000000000",
+ uuid: "zzzzz-tpzed-yid70bw31f51234",
+ ownerUuid: "zzzzz-tpzed-000000000000000",
isAdmin: false,
isActive: true,
username: "testfirstname",
},
},
runProcessPanel: {
- processPathname: "/projects/ce8i5-tpzed-yid70bw31f51234",
- processOwnerUuid: "ce8i5-tpzed-yid70bw31f51234",
+ processPathname: "/projects/zzzzz-tpzed-yid70bw31f51234",
+ processOwnerUuid: "zzzzz-tpzed-yid70bw31f51234",
selectedWorkflow: {
- href: "/workflows/ce8i5-7fd4e-2tlnerdkxnl4fjt",
+ href: "/workflows/zzzzz-7fd4e-2tlnerdkxnl4fjt",
kind: "arvados#workflow",
etag: "8gh5xlhlgo61yqscyl1spw8tc",
- uuid: "ce8i5-7fd4e-2tlnerdkxnl4fjt",
- ownerUuid: "ce8i5-tpzed-o4njwilpp4ov321",
+ uuid: "zzzzz-7fd4e-2tlnerdkxnl4fjt",
+ ownerUuid: "zzzzz-tpzed-o4njwilpp4ov321",
createdAt: "2020-07-15T19:40:50.296041000Z",
- modifiedByClientUuid: "ce8i5-ozdt8-libnr89sc5nq111",
- modifiedByUserUuid: "ce8i5-tpzed-o4njwilpp4ov321",
+ modifiedByClientUuid: "zzzzz-ozdt8-libnr89sc5nq111",
+ modifiedByUserUuid: "zzzzz-tpzed-o4njwilpp4ov321",
modifiedAt: "2020-07-15T19:40:50.296376000Z",
name: "revsort.cwl",
description:
"arvados-cwl-runner",
"--api=containers",
"--local",
- "--project-uuid=ce8i5-tpzed-yid70bw31f51234",
+ "--project-uuid=zzzzz-tpzed-yid70bw31f51234",
"/var/lib/cwl/workflow.json#main",
"/var/lib/cwl/cwl.input.json",
],
description: "basicFormTestDescription",
mounts: undefined,
name: "basicFormTestName",
- outputName: undefined,
+ outputName: "Output from basicFormTestName",
outputPath: "/var/spool/cwl",
- ownerUuid: "ce8i5-tpzed-yid70bw31f51234",
+ ownerUuid: "zzzzz-tpzed-yid70bw31f51234",
priority: 1,
properties: {
workflowName: "revsort.cwl",
- workflowUuid: "ce8i5-7fd4e-2tlnerdkxnl4fjt",
+ template_uuid: "zzzzz-7fd4e-2tlnerdkxnl4fjt",
},
runtimeConstraints: {
API: true,
} from 'views/run-process-panel/run-process-advanced-form';
import { dialogActions } from 'store/dialog/dialog-actions';
import { setBreadcrumbs } from 'store/breadcrumbs/breadcrumbs-actions';
+import { getResource } from 'store/resources/resources';
+import { ProjectResource } from "models/project";
+import { UserResource } from "models/user";
export const runProcessPanelActions = unionize({
SET_PROCESS_PATHNAME: ofType<string>(),
const advancedFormValues = getWorkflowRunnerSettings(workflow);
+ let owner = getResource<ProjectResource | UserResource>(getState().runProcessPanel.processOwnerUuid)(getState().resources);
+ const userUuid = getUserUuid(getState());
+ if (!owner || !userUuid || owner.writableBy.indexOf(userUuid) === -1) {
+ owner = undefined;
+ }
+
if (isStepChanged && isWorkflowChanged) {
dispatch(runProcessPanelActions.SET_STEP_CHANGED(false));
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
+ dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name: workflow.name, owner }));
dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, advancedFormValues));
}
if (!isWorkflowChanged) {
dispatch(runProcessPanelActions.SET_SELECTED_WORKFLOW(workflow));
dispatch<any>(loadPresets(workflow.uuid));
+ dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name: workflow.name, owner }));
dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, advancedFormValues));
}
};
const userUuid = getUserUuid(getState());
if (!userUuid) { return; }
const { processOwnerUuid, selectedWorkflow } = state.runProcessPanel;
- const ownerUUid = processOwnerUuid ? processOwnerUuid : userUuid;
+ const ownerUUid = basicForm.owner ? basicForm.owner.uuid : (processOwnerUuid ? processOwnerUuid : userUuid);
if (selectedWorkflow) {
const advancedForm = getFormValues(RUN_PROCESS_ADVANCED_FORM)(state) as RunProcessAdvancedFormData || getWorkflowRunnerSettings(selectedWorkflow);
const newProcessData = {
],
outputPath: '/var/spool/cwl',
priority: 1,
- outputName: advancedForm[OUTPUT_FIELD] ? advancedForm[OUTPUT_FIELD] : undefined,
+ outputName: advancedForm[OUTPUT_FIELD] ? advancedForm[OUTPUT_FIELD] : `Output from ${basicForm.name}`,
properties: {
- workflowUuid: selectedWorkflow.uuid,
+ template_uuid: selectedWorkflow.uuid,
workflowName: selectedWorkflow.name
},
useExisting: false
PROJECTS = 'Projects',
SHARED_WITH_ME = 'Shared with me',
PUBLIC_FAVORITES = 'Public Favorites',
- WORKFLOWS = 'Workflows',
FAVORITES = 'My Favorites',
TRASH = 'Trash',
ALL_PROCESSES = 'All Processes',
SidePanelTreeCategory.SHARED_WITH_ME,
SidePanelTreeCategory.PUBLIC_FAVORITES,
SidePanelTreeCategory.FAVORITES,
- SidePanelTreeCategory.WORKFLOWS,
SidePanelTreeCategory.GROUPS,
SidePanelTreeCategory.ALL_PROCESSES,
SidePanelTreeCategory.TRASH
sharedWithMePanelActions
} from 'store/shared-with-me-panel/shared-with-me-panel-actions';
import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
-import { loadWorkflowPanel, workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
+import { workflowPanelActions } from 'store/workflow-panel/workflow-panel-actions';
import { loadSshKeysPanel } from 'store/auth/auth-action-ssh';
import { loadLinkAccountPanel, linkAccountPanelActions } from 'store/link-account-panel/link-account-panel-actions';
import { loadSiteManagerPanel } from 'store/auth/auth-action-session';
}
);
-export const loadWorkflow = handleFirstTimeLoad(async (dispatch: Dispatch<any>) => {
- dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.WORKFLOWS));
- await dispatch(loadWorkflowPanel());
- dispatch(setSidePanelBreadcrumbs(SidePanelTreeCategory.WORKFLOWS));
-});
-
export const loadPublicFavorites = () =>
handleFirstTimeLoad(
(dispatch: Dispatch) => {
await openRunProcess("zzzzz-7fd4e-0123456789abcde", "zzzzz-tpzed-0123456789abcde", "testing", { inputparm: "value" })(dispatchWrapper, store.getState, services);
expect(dispatchMock).toHaveBeenCalledWith(runProcessPanelActions.SET_WORKFLOWS(wflist));
expect(dispatchMock).toHaveBeenCalledWith(runProcessPanelActions.SET_SELECTED_WORKFLOW(wflist[0]));
- expect(dispatchMock).toHaveBeenCalledWith(runProcessPanelActions.SET_PROCESS_OWNER_UUID("zzzzz-tpzed-0123456789abcde"));
expect(dispatchMock).toHaveBeenCalledWith(initialize(RUN_PROCESS_BASIC_FORM, { name: "testing" }));
expect(dispatchMock).toHaveBeenCalledWith(initialize(RUN_PROCESS_INPUTS_FORM, { inputparm: "value" }));
});
import { RUN_PROCESS_BASIC_FORM } from 'views/run-process-panel/run-process-basic-form';
import { RUN_PROCESS_INPUTS_FORM } from 'views/run-process-panel/run-process-inputs-form';
import { RUN_PROCESS_ADVANCED_FORM } from 'views/run-process-panel/run-process-advanced-form';
+import { getResource, ResourcesState } from 'store/resources/resources';
+import { ProjectResource } from 'models/project';
+import { UserResource } from 'models/user';
+import { getUserUuid } from "common/getuser";
export const WORKFLOW_PANEL_ID = "workflowPanel";
const UUID_PREFIX_PROPERTY_NAME = 'uuidPrefix';
dispatch<any>(loadPresets(workflow.uuid));
dispatch(initialize(RUN_PROCESS_ADVANCED_FORM, getWorkflowRunnerSettings(workflow)));
+ let owner;
if (ownerUuid) {
- dispatch(runProcessPanelActions.SET_PROCESS_OWNER_UUID(ownerUuid));
+ // Must be writable.
+ const userUuid = getUserUuid(getState());
+ owner = getResource<ProjectResource | UserResource>(ownerUuid)(getState().resources);
+ if (!owner || !userUuid || owner.writableBy.indexOf(userUuid) === -1) {
+ owner = undefined;
+ }
}
- if (name) {
- dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name }));
+ if (owner) {
+ dispatch(runProcessPanelActions.SET_PROCESS_OWNER_UUID(owner.uuid));
}
+
+ dispatch(initialize(RUN_PROCESS_BASIC_FORM, { name, owner }));
+
if (inputObj) {
dispatch(initialize(RUN_PROCESS_INPUTS_FORM, inputObj));
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
+import { openRunProcess } from "store/workflow-panel/workflow-panel-actions";
+
+export const workflowActionSet: ContextMenuActionSet = [[
+ {
+ name: "Run",
+ execute: (dispatch, resource) => {
+ dispatch<any>(openRunProcess(resource.uuid, resource.ownerUuid, resource.name));
+ }
+ },
+]];
const emptyActionSet: ContextMenuActionSet = [];
const getMenuActionSet = (resource?: ContextMenuResource): ContextMenuActionSet => (
- resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
+ resource ? menuActionSets.get(resource.menuKind) || emptyActionSet : emptyActionSet
);
export enum ContextMenuKind {
GROUP_MEMBER = "GroupMember",
PERMISSION_EDIT = "PermissionEdit",
LINK = "Link",
+ WORKFLOW = "Workflow",
}
import { CollectionDetails } from "./collection-details";
import { ProcessDetails } from "./process-details";
import { EmptyDetails } from "./empty-details";
+import { WorkflowDetails } from "./workflow-details";
import { DetailsData } from "./details-data";
import { DetailsResource } from "models/details";
import { Config } from 'common/config';
return new CollectionDetails(res);
case ResourceKind.PROCESS:
return new ProcessDetails(res);
+ case ResourceKind.WORKFLOW:
+ return new WorkflowDetails(res);
default:
return new EmptyDetails(res);
}
let shouldShowInlinePreview = false;
if (!('kind' in res)) {
shouldShowInlinePreview = isInlineFileUrlSafe(
- res ? res.url : "",
- authConfig.keepWebServiceUrl,
- authConfig.keepWebInlineServiceUrl
+ res ? res.url : "",
+ authConfig.keepWebServiceUrl,
+ authConfig.keepWebInlineServiceUrl
) || authConfig.clusterConfig.Collections.TrustAllContent;
}
</Grid>
<Grid item>
<Tabs onChange={this.handleChange}
- value={(item.getTabLabels().length >= tabNr+1) ? tabNr : 0}>
- { item.getTabLabels().map((tabLabel, idx) =>
+ value={(item.getTabLabels().length >= tabNr + 1) ? tabNr : 0}>
+ {item.getTabLabels().map((tabLabel, idx) =>
<Tab key={`tab-label-${idx}`} disableRipple label={tabLabel} />)
}
</Tabs>
</Grid>
<Grid item xs className={this.props.classes.tabContainer} >
- {item.getDetails({tabNr, showPreview: shouldShowInlinePreview})}
+ {item.getDetails({ tabNr, showPreview: shouldShowInlinePreview })}
</Grid>
</Grid >;
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { DefaultIcon, WorkflowIcon } from 'components/icon/icon';
+import { WorkflowResource } from 'models/workflow';
+import { DetailsData } from "./details-data";
+import { DefaultView } from 'components/default-view/default-view';
+import { DetailsAttribute } from 'components/details-attribute/details-attribute';
+import { ResourceOwnerWithName } from 'views-components/data-explorer/renderers';
+import { formatDate } from "common/formatters";
+import { Grid } from '@material-ui/core';
+import { withStyles, StyleRulesCallback, WithStyles, Button } from '@material-ui/core';
+import { openRunProcess } from "store/workflow-panel/workflow-panel-actions";
+import { Dispatch } from 'redux';
+import { connect } from 'react-redux';
+import { ArvadosTheme } from 'common/custom-theme';
+
+export interface WorkflowDetailsCardDataProps {
+ workflow?: WorkflowResource;
+}
+
+export interface WorkflowDetailsCardActionProps {
+ onClick: (wf: WorkflowResource) => () => void;
+}
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+ onClick: (wf: WorkflowResource) =>
+ () => wf && dispatch<any>(openRunProcess(wf.uuid, wf.ownerUuid, wf.name)),
+});
+
+type CssRules = 'runButton';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+ runButton: {
+ boxShadow: 'none',
+ padding: '2px 10px 2px 5px',
+ fontSize: '0.75rem'
+ },
+});
+
+export const WorkflowDetailsAttributes = connect(null, mapDispatchToProps)(
+ withStyles(styles)(
+ ({ workflow, onClick, classes }: WorkflowDetailsCardDataProps & WorkflowDetailsCardActionProps & WithStyles<CssRules>) => {
+ return <Grid container>
+ <Button onClick={workflow && onClick(workflow)} className={classes.runButton} variant='contained'
+ data-cy='details-panel-run-btn' color='primary' size='small'>
+ Run
+ </Button>
+ {workflow && workflow.description !== "" && <Grid item xs={12} >
+ <DetailsAttribute
+ label={"Description"}
+ value={workflow?.description} />
+ </Grid>}
+ <Grid item xs={12} >
+ <DetailsAttribute
+ label={"Workflow UUID"}
+ linkToUuid={workflow?.uuid} />
+ </Grid>
+ <Grid item xs={12} >
+ <DetailsAttribute
+ label='Owner' linkToUuid={workflow?.ownerUuid}
+ uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+ </Grid>
+ <Grid item xs={12}>
+ <DetailsAttribute label='Created at' value={formatDate(workflow?.createdAt)} />
+ </Grid>
+ <Grid item xs={12}>
+ <DetailsAttribute label='Last modified' value={formatDate(workflow?.modifiedAt)} />
+ </Grid>
+ <Grid item xs={12} >
+ <DetailsAttribute
+ label='Last modified by user' linkToUuid={workflow?.modifiedByUserUuid}
+ uuidEnhancer={(uuid: string) => <ResourceOwnerWithName uuid={uuid} />} />
+ </Grid>
+ </Grid >;
+ }));
+
+export class WorkflowDetails extends DetailsData<WorkflowResource> {
+ getIcon(className?: string) {
+ return <WorkflowIcon className={className} />;
+ }
+
+ getDetails() {
+ return <WorkflowDetailsAttributes workflow={this.item} />;
+ }
+}
<CollectionIcon className={classes.icon} /> New collection
</MenuItem>
<MenuItem data-cy='side-panel-run-process' className={classes.menuItem} onClick={this.handleRunProcessClick}>
- <ProcessIcon className={classes.icon} /> Run a process
+ <ProcessIcon className={classes.icon} /> Run a workflow
</MenuItem>
<MenuItem data-cy='side-panel-new-project' className={classes.menuItem} onClick={this.handleNewProjectClick}>
<ProjectIcon className={classes.icon} /> New project
import { ProjectResource } from "models/project";
import { ListItemTextIcon } from "components/list-item-text-icon/list-item-text-icon";
import { ProcessIcon, ProjectIcon, FilterGroupIcon, FavoriteIcon, ProjectsIcon, ShareMeIcon, TrashIcon, PublicFavoriteIcon, GroupsIcon } from 'components/icon/icon';
-import { WorkflowIcon } from 'components/icon/icon';
import { activateSidePanelTreeItem, toggleSidePanelTreeItemCollapse, SIDE_PANEL_TREE, SidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
import { openSidePanelContextMenu } from 'store/context-menu/context-menu-actions';
import { noop } from 'lodash';
export const SidePanelTree = connect(undefined, mapDispatchToProps)(
(props: SidePanelTreeActionProps) =>
<span data-cy="side-panel-tree">
- <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />
+ <TreePicker {...props} render={renderSidePanelItem} pickerId={SIDE_PANEL_TREE} />
</span>);
const renderSidePanelItem = (item: TreeItem<ProjectResource>) => {
return ShareMeIcon;
case SidePanelTreeCategory.TRASH:
return TrashIcon;
- case SidePanelTreeCategory.WORKFLOWS:
- return WorkflowIcon;
case SidePanelTreeCategory.PUBLIC_FAVORITES:
return PublicFavoriteIcon;
case SidePanelTreeCategory.ALL_PROCESSES:
</Grid>
<Grid item xs={12} md={mdSize}>
<DetailsAttribute label='Docker Image locator'
- linkToUuid={containerRequest.containerImage} value={containerRequest.containerImage} />
+ linkToUuid={containerRequest.containerImage} value={containerRequest.containerImage} />
</Grid>
<Grid item xs={12} md={mdSize}>
<DetailsAttribute
<DetailsAttribute label='Container UUID' value={containerRequest.containerUuid} />
</Grid>
{!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
- <DetailsAttribute label='Status' value={getProcessStatus({containerRequest, container})} />
+ <DetailsAttribute label='Status' value={getProcessStatus({ containerRequest, container })} />
</Grid>}
<Grid item xs={12} md={mdSize}>
<DetailsAttribute label='Created at' value={formatDate(containerRequest.createdAt)} />
<DetailsAttribute classLabel={classes.link} label='Inputs' />
</span>
</Grid>
- {containerRequest.properties.workflowUuid &&
- <Grid item xs={12} md={mdSize}>
- <span onClick={() => props.openWorkflow(containerRequest.properties.workflowUuid)}>
- <DetailsAttribute classValue={classes.link}
- label='Workflow' value={containerRequest.properties.workflowName} />
- </span>
- </Grid>}
+ {containerRequest.properties.template_uuid &&
+ <Grid item xs={12} md={mdSize}>
+ <span onClick={() => props.openWorkflow(containerRequest.properties.template_uuid)}>
+ <DetailsAttribute classValue={classes.link}
+ label='Workflow' value={containerRequest.properties.workflowName} />
+ </span>
+ </Grid>}
<Grid item xs={12} md={mdSize}>
<DetailsAttribute label='Priority' value={containerRequest.priority} />
</Grid>
*/}
<Grid item xs={12} md={12}>
<DetailsAttribute label='Properties' />
- { Object.keys(containerRequest.properties).length > 0
+ {Object.keys(containerRequest.properties).length > 0
? Object.keys(containerRequest.properties).map(k =>
- Array.isArray(containerRequest.properties[k])
+ Array.isArray(containerRequest.properties[k])
? containerRequest.properties[k].map((v: string) =>
getPropertyChip(k, v, undefined, classes.propertyTag))
: getPropertyChip(k, containerRequest.properties[k], undefined, classes.propertyTag))
- : <div>No properties</div> }
+ : <div>No properties</div>}
</Grid>
</Grid>;
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { connect, DispatchProp } from 'react-redux';
+import { Field } from 'redux-form';
+import { Input, Dialog, DialogTitle, DialogContent, DialogActions, Button } from '@material-ui/core';
+import {
+ GenericCommandInputParameter
+} from 'models/workflow';
+import { GenericInput, GenericInputProps } from './generic-input';
+import { ProjectsTreePicker } from 'views-components/projects-tree-picker/projects-tree-picker';
+import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { TreeItem } from 'components/tree/tree';
+import { ProjectsTreePickerItem } from 'views-components/projects-tree-picker/generic-projects-tree-picker';
+import { ProjectResource } from 'models/project';
+import { ResourceKind } from 'models/resource';
+import { RootState } from 'store/store';
+import { getUserUuid } from 'common/getuser';
+
+export type ProjectCommandInputParameter = GenericCommandInputParameter<ProjectResource, ProjectResource>;
+
+const require: any = (value?: ProjectResource) => (value === undefined);
+
+export interface ProjectInputProps {
+ input: ProjectCommandInputParameter;
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+}
+export const ProjectInput = ({ input, options }: ProjectInputProps) =>
+ <Field
+ name={input.id}
+ commandInput={input}
+ component={ProjectInputComponent as any}
+ format={format}
+ validate={require}
+ {...{
+ options
+ }} />;
+
+const format = (value?: ProjectResource) => value ? value.name : '';
+
+interface ProjectInputComponentState {
+ open: boolean;
+ project?: ProjectResource;
+}
+
+interface HasUserUuid {
+ userUuid: string;
+};
+
+const mapStateToProps = (state: RootState) => ({ userUuid: getUserUuid(state) });
+
+export const ProjectInputComponent = connect(mapStateToProps)(
+ class ProjectInputComponent extends React.Component<GenericInputProps & DispatchProp & HasUserUuid & {
+ options?: { showOnlyOwned: boolean, showOnlyWritable: boolean };
+ }, ProjectInputComponentState> {
+ state: ProjectInputComponentState = {
+ open: false,
+ };
+
+ componentDidMount() {
+ this.props.dispatch<any>(
+ initProjectsTreePicker(this.props.commandInput.id));
+ }
+
+ render() {
+ return <>
+ {this.renderInput()}
+ {this.renderDialog()}
+ </>;
+ }
+
+ openDialog = () => {
+ this.setState({ open: true });
+ }
+
+ closeDialog = () => {
+ this.setState({ open: false });
+ }
+
+ submit = () => {
+ this.closeDialog();
+ this.props.input.onChange(this.state.project);
+ }
+
+ setProject = (_: {}, { data }: TreeItem<ProjectsTreePickerItem>) => {
+ if ('kind' in data && data.kind === ResourceKind.PROJECT) {
+ this.setState({ project: data });
+ } else {
+ this.setState({ project: undefined });
+ }
+ }
+
+ invalid = () => (!this.state.project || this.state.project.writableBy.indexOf(this.props.userUuid) === -1);
+
+ renderInput() {
+ return <GenericInput
+ component={props =>
+ <Input
+ readOnly
+ fullWidth
+ value={props.input.value}
+ error={props.meta.touched && !!props.meta.error}
+ disabled={props.commandInput.disabled}
+ onClick={!this.props.commandInput.disabled ? this.openDialog : undefined}
+ onKeyPress={!this.props.commandInput.disabled ? this.openDialog : undefined} />}
+ {...this.props} />;
+ }
+
+ renderDialog() {
+ return <Dialog
+ open={this.state.open}
+ onClose={this.closeDialog}
+ fullWidth
+ data-cy="choose-a-project-dialog"
+ maxWidth='md'>
+ <DialogTitle>Choose a project</DialogTitle>
+ <DialogContent>
+ <ProjectsTreePicker
+ pickerId={this.props.commandInput.id}
+ options={this.props.options}
+ toggleItemActive={this.setProject} />
+ </DialogContent>
+ <DialogActions>
+ <Button onClick={this.closeDialog}>Cancel</Button>
+ <Button
+ disabled={this.invalid()}
+ variant='contained'
+ color='primary'
+ onClick={this.submit}>Ok</Button>
+ </DialogActions>
+ </Dialog>;
+ }
+
+ });
import { reduxForm, Field } from 'redux-form';
import { Grid } from '@material-ui/core';
import { TextField } from 'components/text-field/text-field';
+import { ProjectInput, ProjectCommandInputParameter } from 'views/run-process-panel/inputs/project-input';
import { PROCESS_NAME_VALIDATION } from 'validators/validators';
+import { ProjectResource } from 'models/project';
+import { UserResource } from 'models/user';
export const RUN_PROCESS_BASIC_FORM = 'runProcessBasicForm';
export interface RunProcessBasicFormData {
name: string;
description: string;
+ owner?: ProjectResource | UserResource;
}
+
export const RunProcessBasicForm =
reduxForm<RunProcessBasicFormData>({
form: RUN_PROCESS_BASIC_FORM
<Field
name='name'
component={TextField as any}
- label="Enter a new name for run process"
+ label="Name for this workflow run"
required
validate={PROCESS_NAME_VALIDATION} />
</Grid>
<Field
name='description'
component={TextField as any}
- label="Enter a description for run process" />
+ label="Optional description of this workflow run" />
+ </Grid>
+ <Grid item xs={12} md={6}>
+ <ProjectInput input={{
+ id: "owner",
+ label: "Project where the workflow will run"
+ } as ProjectCommandInputParameter}
+ options={{ showOnlyOwned: false, showOnlyWritable: true }} />
</Grid>
</Grid>
</form>);
// SPDX-License-Identifier: AGPL-3.0
import React from 'react';
-import { Stepper, Step, StepLabel, StepContent } from '@material-ui/core';
+import { Stepper, Step, StepLabel, StepContent, StyleRulesCallback, withStyles, WithStyles } from '@material-ui/core';
import { RunProcessFirstStepDataProps, RunProcessFirstStepActionProps, RunProcessFirstStep } from 'views/run-process-panel/run-process-first-step';
import { RunProcessSecondStepForm } from './run-process-second-step';
type RunProcessPanelRootProps = RunProcessPanelRootDataProps & RunProcessPanelRootActionProps;
-export const RunProcessPanelRoot = ({ runProcess, currentStep, onSearch, onSetStep, onSetWorkflow, workflows, selectedWorkflow }: RunProcessPanelRootProps) =>
- <Stepper activeStep={currentStep} orientation="vertical" elevation={2}>
- <Step>
- <StepLabel>Choose a workflow</StepLabel>
- <StepContent>
- <RunProcessFirstStep
- workflows={workflows}
- selectedWorkflow={selectedWorkflow}
- onSearch={onSearch}
- onSetStep={onSetStep}
- onSetWorkflow={onSetWorkflow} />
- </StepContent>
- </Step>
- <Step>
- <StepLabel>Select inputs</StepLabel>
- <StepContent>
- <RunProcessSecondStepForm
- goBack={() => onSetStep(0)}
- runProcess={runProcess} />
- </StepContent>
- </Step>
- </Stepper>;
\ No newline at end of file
+type CssRules = 'stepper';
+
+const styles: StyleRulesCallback<CssRules> = theme => ({
+ stepper: {
+ overflow: "scroll",
+ }
+});
+
+export const RunProcessPanelRoot = withStyles(styles)(
+ ({ runProcess, currentStep, onSearch, onSetStep, onSetWorkflow, workflows, selectedWorkflow, classes }: WithStyles<CssRules> & RunProcessPanelRootProps) =>
+ <Stepper activeStep={currentStep} orientation="vertical" elevation={2} className={classes.stepper}>
+ <Step>
+ <StepLabel>Choose a workflow</StepLabel>
+ <StepContent>
+ <RunProcessFirstStep
+ workflows={workflows}
+ selectedWorkflow={selectedWorkflow}
+ onSearch={onSearch}
+ onSetStep={onSetStep}
+ onSetWorkflow={onSetWorkflow} />
+ </StepContent>
+ </Step>
+ <Step>
+ <StepLabel>Select inputs</StepLabel>
+ <StepContent>
+ <RunProcessSecondStepForm
+ goBack={() => onSetStep(0)}
+ runProcess={runProcess} />
+ </StepContent>
+ </Step>
+ </Stepper>);
import { RUN_PROCESS_INPUTS_FORM } from './run-process-inputs-form';
import { RunProcessAdvancedForm, RUN_PROCESS_ADVANCED_FORM } from './run-process-advanced-form';
import { createStructuredSelector } from 'reselect';
-import { WorkflowPresetSelect } from 'views/run-process-panel/workflow-preset-select';
import { selectPreset } from 'store/run-process-panel/run-process-panel-actions';
export interface RunProcessSecondStepFormDataProps {
({ inputs, workflow, selectedPreset, presets, onPresetChange, valid, goBack, runProcess }: RunProcessSecondStepFormProps) =>
<Grid container spacing={16} data-cy="new-process-panel">
<Grid item xs={12}>
- <Grid container spacing={32}>
- <Grid item xs={12} md={6}>
- {workflow && selectedPreset && presets &&
- < WorkflowPresetSelect
- {...{ workflow, selectedPreset, presets, onChange: onPresetChange }} />
- }
- </Grid>
- </Grid>
<RunProcessBasicForm />
<RunProcessInputsForm inputs={inputs} />
<RunProcessAdvancedForm />
Back
</Button>
<Button disabled={!valid} variant="contained" color="primary" onClick={runProcess}>
- Run Process
+ Run workflow
</Button>
</Grid>
</Grid>);
TableHead,
TableCell,
TableBody,
- TableRow
+ TableRow,
} from '@material-ui/core';
import { ArvadosTheme } from 'common/custom-theme';
import { WorkflowIcon } from 'components/icon/icon';
import { DataTableDefaultView } from 'components/data-table-default-view/data-table-default-view';
-import { WorkflowResource, parseWorkflowDefinition, getWorkflowInputs, getInputLabel, stringifyInputType } from 'models/workflow';
-import { WorkflowGraph } from "views/workflow-panel/workflow-graph";
+import { parseWorkflowDefinition, getWorkflowInputs, getInputLabel, stringifyInputType } from 'models/workflow';
+import { WorkflowDetailsCardDataProps, WorkflowDetailsAttributes } from 'views-components/details-panel/workflow-details';
export type CssRules = 'root' | 'tab' | 'inputTab' | 'graphTab' | 'graphTabWithChosenWorkflow' | 'descriptionTab' | 'inputsTable';
},
});
-interface WorkflowDetailsCardDataProps {
- workflow?: WorkflowResource;
-}
-
type WorkflowDetailsCardProps = WorkflowDetailsCardDataProps & WithStyles<CssRules>;
export const WorkflowDetailsCard = withStyles(styles)(
<Tabs value={value} onChange={this.handleChange} centered={true}>
<Tab className={classes.tab} label="Description" />
<Tab className={classes.tab} label="Inputs" />
- <Tab className={classes.tab} label="Graph" />
+ <Tab className={classes.tab} label="Details" />
</Tabs>
{value === 0 && <CardContent className={classes.descriptionTab}>
{workflow ? <div>
{workflow.description}
</div> : (
- <DataTableDefaultView
- icon={WorkflowIcon}
- messages={['Please select a workflow to see its description.']} />
- )}
+ <DataTableDefaultView
+ icon={WorkflowIcon}
+ messages={['Please select a workflow to see its description.']} />
+ )}
</CardContent>}
{value === 1 && <CardContent className={classes.inputTab}>
{workflow
messages={['Please select a workflow to see its inputs.']} />
}
</CardContent>}
- {value === 2 && <CardContent className={workflow ? classes.graphTabWithChosenWorkflow : classes.graphTab}>
+ {value === 2 && <CardContent className={classes.descriptionTab}>
{workflow
- ? <WorkflowGraph workflow={workflow} />
+ ? <WorkflowDetailsAttributes workflow={workflow} />
: <DataTableDefaultView
icon={WorkflowIcon}
- messages={['Please select a workflow to see its visualisation.']} />
+ messages={['Please select a workflow to see its details.']} />
}
</CardContent>}
</div>;