Merge branch '19143-project-list-workflows'
authorPeter Amstutz <peter.amstutz@curii.com>
Wed, 25 May 2022 13:45:54 +0000 (09:45 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Wed, 25 May 2022 13:46:27 +0000 (09:46 -0400)
refs #19143
refs #19069

Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

31 files changed:
cypress/integration/create-workflow.spec.js
cypress/integration/favorites.spec.js
cypress/integration/side-panel.spec.js
src/common/labels.ts
src/index.tsx
src/models/details.ts
src/models/workflow.ts
src/routes/route-change-handlers.ts
src/store/context-menu/context-menu-actions.ts
src/store/navigation/navigation-action.ts
src/store/processes/processes-actions.ts
src/store/resource-type-filters/resource-type-filters.test.ts
src/store/resource-type-filters/resource-type-filters.ts
src/store/run-process-panel/run-process-panel-actions.test.ts
src/store/run-process-panel/run-process-panel-actions.ts
src/store/side-panel-tree/side-panel-tree-actions.ts
src/store/workbench/workbench-actions.ts
src/store/workflow-panel/workflow-panel-actions.test.ts
src/store/workflow-panel/workflow-panel-actions.ts
src/views-components/context-menu/action-sets/workflow-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/context-menu.tsx
src/views-components/details-panel/details-panel.tsx
src/views-components/details-panel/workflow-details.tsx [new file with mode: 0644]
src/views-components/side-panel-button/side-panel-button.tsx
src/views-components/side-panel-tree/side-panel-tree.tsx
src/views/process-panel/process-details-attributes.tsx
src/views/run-process-panel/inputs/project-input.tsx [new file with mode: 0644]
src/views/run-process-panel/run-process-basic-form.tsx
src/views/run-process-panel/run-process-panel-root.tsx
src/views/run-process-panel/run-process-second-step.tsx
src/views/workflow-panel/workflow-description-card.tsx

index b1ea5dbf7e297d373911d728379869ed92ffff57..8df8389ffd1a6fbc4683a1fc2405b1d146713ce6 100644 (file)
@@ -73,12 +73,12 @@ describe('Multi-file deletion tests', function () {
 
             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');
@@ -105,7 +105,7 @@ describe('Multi-file deletion tests', function () {
             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');
         });
     });
 
@@ -148,14 +148,14 @@ describe('Multi-file deletion tests', function () {
 
                 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]')
@@ -174,7 +174,7 @@ describe('Multi-file deletion tests', function () {
                                 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]')
@@ -193,13 +193,13 @@ describe('Multi-file deletion tests', function () {
                             });
                     });
 
-                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);
index 9bc90ebdee4c9fda24b91a2c9c15312965d3fd46..105657effa77300136db29760437731eec3ec764 100644 (file)
@@ -219,13 +219,16 @@ describe('Favorites tests', function () {
 
                 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}]`);
@@ -240,7 +243,10 @@ describe('Favorites tests', function () {
                         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}]`);
index 4c824d3275f5d83bf53a04da7dc1abf41312a9d1..e187d533ead19b2b43e9ed01aae24215f84c4955 100644 (file)
@@ -62,7 +62,6 @@ describe('Side panel tests', function() {
             {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) {
index 682513fb165e31105363a71decf7c2b4a74fa8fe..e784cec0f5d95fd915a58b0d2ca78163178f8bdb 100644 (file)
@@ -23,6 +23,8 @@ export const resourceLabel = (type: string, subtype = '') => {
             return "Group";
         case ResourceKind.VIRTUAL_MACHINE:
             return "Virtual Machine";
+        case ResourceKind.WORKFLOW:
+            return "Workflow";
         default:
             return "Unknown";
     }
index f928ea8ae33447c8b166aebdcbe1c8827b30fb63..03840d49ad232fe22d22a14c3762757690523894 100644 (file)
@@ -62,6 +62,7 @@ import { linkActionSet } from 'views-components/context-menu/action-sets/link-ac
 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';
@@ -102,6 +103,7 @@ addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
 addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
 addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
+addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
 
 storeRedirects();
 
index 150b694083bc9378ca9a41087afdb9e310fec2fb..b6eabd7014efade5dea3c51775f876413573d0bb 100644 (file)
@@ -7,5 +7,6 @@ import { CollectionResource } from "./collection";
 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;
index ad84bd9e891a95fcfff02a1cb001a72e226e0f45..6d21dbc766381831a1e048529913e77f51784a38 100644 (file)
@@ -139,7 +139,7 @@ export const parseWorkflowDefinition = (workflow: WorkflowResource): WorkflowRes
 
 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;
@@ -153,7 +153,7 @@ export const getWorkflowInputs = (workflowDefinition: WorkflowResourceDefinition
 };
 
 export const getInputLabel = (input: CommandInputParameter) => {
-    return `${input.label || input.id}`;
+    return `${input.label || input.id.split('/').pop()}`;
 };
 
 export const isRequiredInput = ({ type }: CommandInputParameter) => {
index 5b3ce6687ff0e4cb8b3e17a97d286898dda90946..237b6d9611bd3d5c9b3f8ff994c92a9dc075e5b5 100644 (file)
@@ -33,7 +33,6 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     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);
@@ -77,8 +76,6 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         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) {
index 1116949a6f31f769dfcf0fbb3ed56e147e7166c2..3e239feeaa8bb9cb27867db910b6222398e48495 100644 (file)
@@ -189,7 +189,7 @@ export const openProcessContextMenu = (event: React.MouseEvent<HTMLElement>, pro
                 name: res.name,
                 description: res.description,
                 outputUuid: res.outputUuid || '',
-                workflowUuid: res.properties.workflowUuid || '',
+                workflowUuid: res.properties.template_uuid || '',
                 menuKind: ContextMenuKind.PROCESS_RESOURCE
             }));
         }
@@ -261,6 +261,8 @@ export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
                 return ContextMenuKind.ROOT_PROJECT;
             case ResourceKind.LINK:
                 return ContextMenuKind.LINK;
+            case ResourceKind.WORKFLOW:
+                return ContextMenuKind.WORKFLOW;
             default:
                 return;
         }
index 1cdb6784bf42390c88ef60fa920a76c773ab71c8..c8811bf43955a92ad493ddc6e2acc28e8846b1b3 100644 (file)
@@ -8,6 +8,7 @@ import { ResourceKind, extractUuidKind } from 'models/resource';
 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';
@@ -40,6 +41,9 @@ export const navigateTo = (uuid: string) =>
             case ResourceKind.VIRTUAL_MACHINE:
                 dispatch<any>(navigateToAdminVirtualMachines);
                 return;
+            case ResourceKind.WORKFLOW:
+                dispatch<any>(openDetailsPanel(uuid));
+                return;
         }
 
         switch (uuid) {
@@ -58,9 +62,6 @@ export const navigateTo = (uuid: string) =>
             case SidePanelTreeCategory.SHARED_WITH_ME:
                 dispatch(navigateToSharedWithMe);
                 return;
-            case SidePanelTreeCategory.WORKFLOWS:
-                dispatch(navigateToWorkflows);
-                return;
             case SidePanelTreeCategory.TRASH:
                 dispatch(navigateToTrash);
                 return;
index 6bd2976c1182e508720e8947847bd7087e253736..213e292bfe6f047e306d20dda6ad477e7652b525 100644 (file)
@@ -18,6 +18,8 @@ import { RUN_PROCESS_BASIC_FORM, RunProcessBasicFormData } from "views/run-proce
 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> => {
@@ -60,7 +62,8 @@ export const reRunProcess = (processUuid: string, workflowUuid: string) =>
             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 = {
index a3684507b3bbd3a8db79dcd02fe97e7b9756bbb2..f001770e8a283e441935044b2fbcdf8171666033 100644 (file)
@@ -31,20 +31,21 @@ describe("serializeResourceTypeFilters", () => {
         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),
@@ -59,6 +60,7 @@ describe("serializeResourceTypeFilters", () => {
         const filters = pipe(
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.WORKFLOW),
             deselectNode(CollectionTypeFilter.GENERAL_COLLECTION),
             deselectNode(CollectionTypeFilter.LOG_COLLECTION),
             deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION),
@@ -74,6 +76,7 @@ describe("serializeResourceTypeFilters", () => {
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROJECT),
             deselectNode(ObjectTypeFilter.PROCESS),
+            deselectNode(ObjectTypeFilter.WORKFLOW),
             deselectNode(CollectionTypeFilter.OUTPUT_COLLECTION)
         )();
 
@@ -87,7 +90,8 @@ describe("serializeResourceTypeFilters", () => {
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROJECT),
             deselectNode(ProcessTypeFilter.CHILD_PROCESS),
-            deselectNode(ObjectTypeFilter.COLLECTION)
+            deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -100,7 +104,8 @@ describe("serializeResourceTypeFilters", () => {
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROJECT),
             deselectNode(ProcessTypeFilter.MAIN_PROCESS),
-            deselectNode(ObjectTypeFilter.COLLECTION)
+            deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -113,6 +118,7 @@ describe("serializeResourceTypeFilters", () => {
             () => getInitialResourceTypeFilters(),
             deselectNode(ObjectTypeFilter.PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -126,6 +132,7 @@ describe("serializeResourceTypeFilters", () => {
             deselectNode(GroupTypeFilter.PROJECT),
             deselectNode(ObjectTypeFilter.PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
@@ -139,6 +146,7 @@ describe("serializeResourceTypeFilters", () => {
             deselectNode(GroupTypeFilter.FILTER_GROUP),
             deselectNode(ObjectTypeFilter.PROCESS),
             deselectNode(ObjectTypeFilter.COLLECTION),
+            deselectNode(ObjectTypeFilter.WORKFLOW),
         )();
 
         const serializedFilters = serializeResourceTypeFilters(filters);
index 0539cefecc93dc74df0912b8450d81613c9e566b..64a391ca9e5419ac7c3e9c79e4e4a04606304c93 100644 (file)
@@ -27,6 +27,7 @@ export enum ObjectTypeFilter {
     PROJECT = 'Project',
     PROCESS = 'Process',
     COLLECTION = 'Data collection',
+    WORKFLOW = 'Workflow',
 }
 
 export enum GroupTypeFilter {
@@ -63,6 +64,7 @@ export const getSimpleObjectTypeFilters = pipe(
     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',
@@ -86,6 +88,8 @@ export const getInitialResourceTypeFilters = pipe(
         initFilter(CollectionTypeFilter.INTERMEDIATE_COLLECTION, ObjectTypeFilter.COLLECTION),
         initFilter(CollectionTypeFilter.LOG_COLLECTION, ObjectTypeFilter.COLLECTION),
     ),
+    initFilter(ObjectTypeFilter.WORKFLOW)
+
 );
 
 export const getInitialProcessTypeFilters = pipe(
@@ -133,6 +137,8 @@ const objectTypeToResourceKind = (type: ObjectTypeFilter) => {
             return ResourceKind.PROCESS;
         case ObjectTypeFilter.COLLECTION:
             return ResourceKind.COLLECTION;
+        case ObjectTypeFilter.WORKFLOW:
+            return ResourceKind.WORKFLOW;
     }
 };
 
index 745be88fb5f66a1c6d0c9d1a3f13f047b841b50e..c615f2162d0de02e1c35ce6b708edf116d85c1b1 100644 (file)
@@ -66,8 +66,8 @@ describe("run-process-panel-actions", () => {
                         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",
@@ -77,17 +77,17 @@ describe("run-process-panel-actions", () => {
                     },
                 },
                 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:
@@ -107,7 +107,7 @@ describe("run-process-panel-actions", () => {
                     "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",
                 ],
@@ -116,13 +116,13 @@ describe("run-process-panel-actions", () => {
                 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,
index adb5ade7c2bb87eb0dce96ae5769eacb10632af5..e0dada5c053c340148c55ff5f5a6918adf778c93 100644 (file)
@@ -21,6 +21,9 @@ import {
 } 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>(),
@@ -99,15 +102,23 @@ export const setWorkflow = (workflow: WorkflowResource, isWorkflowChanged = true
 
         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));
         }
     };
@@ -144,7 +155,7 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
     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 = {
@@ -173,9 +184,9 @@ export const runProcess = async (dispatch: Dispatch<any>, getState: () => RootSt
             ],
             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
index 895fe79c0bdc9011bcb0c7b26c42c6ac3b68ea8e..dd56b42870d2486a5d887caee3f54a3b134eb447 100644 (file)
@@ -23,7 +23,6 @@ export enum SidePanelTreeCategory {
     PROJECTS = 'Projects',
     SHARED_WITH_ME = 'Shared with me',
     PUBLIC_FAVORITES = 'Public Favorites',
-    WORKFLOWS = 'Workflows',
     FAVORITES = 'My Favorites',
     TRASH = 'Trash',
     ALL_PROCESSES = 'All Processes',
@@ -52,7 +51,6 @@ let SIDE_PANEL_CATEGORIES: string[] = [
     SidePanelTreeCategory.SHARED_WITH_ME,
     SidePanelTreeCategory.PUBLIC_FAVORITES,
     SidePanelTreeCategory.FAVORITES,
-    SidePanelTreeCategory.WORKFLOWS,
     SidePanelTreeCategory.GROUPS,
     SidePanelTreeCategory.ALL_PROCESSES,
     SidePanelTreeCategory.TRASH
index ba405cb8f2938f9ce190b32eaed81ec8276aa76a..d2ff84b3acd447f347d067e730305fd9d0910b0e 100644 (file)
@@ -58,7 +58,7 @@ import {
     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';
@@ -457,12 +457,6 @@ export const loadRunProcess = handleFirstTimeLoad(
     }
 );
 
-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) => {
index 1e2b048859c10f22122eccf7077aeb542b9d3828..1a7cad8264bd59983a2841aafeccfbc260caf894 100644 (file)
@@ -82,7 +82,6 @@ describe('workflow-panel-actions', () => {
         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" }));
     });
index b533717e559f4cf6c252a47862b4bfd307ea93b3..7c90fa6bb290fbfd6f58aa16f96b2876effc0b88 100644 (file)
@@ -15,6 +15,10 @@ import { initialize } from 'redux-form';
 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';
@@ -50,12 +54,21 @@ export const openRunProcess = (workflowUuid: string, ownerUuid?: string, name?:
             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));
             }
diff --git a/src/views-components/context-menu/action-sets/workflow-action-set.ts b/src/views-components/context-menu/action-sets/workflow-action-set.ts
new file mode 100644 (file)
index 0000000..2aa7890
--- /dev/null
@@ -0,0 +1,15 @@
+// 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));
+        }
+    },
+]];
index 6f3a4389211363e9294bbfe3c53fdf530d32195a..4766259a921dfc0728040dbe111dca3f7032a7b9 100644 (file)
@@ -70,7 +70,7 @@ export const addMenuActionSet = (name: string, itemSet: ContextMenuActionSet) =>
 
 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 {
@@ -110,4 +110,5 @@ export enum ContextMenuKind {
     GROUP_MEMBER = "GroupMember",
     PERMISSION_EDIT = "PermissionEdit",
     LINK = "Link",
+    WORKFLOW = "Workflow",
 }
index 399f4ef4ef273569dda2c30d440229b58f4c8708..adbbab79333b385eec7b028c98c75aa7c4a041cb 100644 (file)
@@ -18,6 +18,7 @@ import { ProjectDetails } from "./project-details";
 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';
@@ -71,6 +72,8 @@ const getItem = (res: DetailsResource): DetailsData => {
                 return new CollectionDetails(res);
             case ResourceKind.PROCESS:
                 return new ProcessDetails(res);
+            case ResourceKind.WORKFLOW:
+                return new WorkflowDetails(res);
             default:
                 return new EmptyDetails(res);
         }
@@ -152,9 +155,9 @@ export const DetailsPanel = withStyles(styles)(
                 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;
                 }
 
@@ -191,14 +194,14 @@ export const DetailsPanel = withStyles(styles)(
                     </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 >;
             }
diff --git a/src/views-components/details-panel/workflow-details.tsx b/src/views-components/details-panel/workflow-details.tsx
new file mode 100644 (file)
index 0000000..7076823
--- /dev/null
@@ -0,0 +1,88 @@
+// 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} />;
+    }
+}
index a219e55a26d768b47d0e8b97e28ed9abb87b7868..c813efb0a373f4080fe177b1234ce0222f5f5a1d 100644 (file)
@@ -107,7 +107,7 @@ export const SidePanelButton = withStyles(styles)(
                         <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
index e829483473636a08e79179807d131b1275181263..7f5b8d738797ed9a4247ccf523510ce0b648eab2 100644 (file)
@@ -10,7 +10,6 @@ import { TreeItem } from "components/tree/tree";
 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';
@@ -42,7 +41,7 @@ const mapDispatchToProps = (dispatch: Dispatch, props: SidePanelTreeProps): Side
 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>) => {
@@ -76,8 +75,6 @@ const getSidePanelIcon = (category: string) => {
             return ShareMeIcon;
         case SidePanelTreeCategory.TRASH:
             return TrashIcon;
-        case SidePanelTreeCategory.WORKFLOWS:
-            return WorkflowIcon;
         case SidePanelTreeCategory.PUBLIC_FAVORITES:
             return PublicFavoriteIcon;
         case SidePanelTreeCategory.ALL_PROCESSES:
index d0e593d50aec3a1639886c159f62b05ca474b4f4..99a4404c87e90fbca886d39b872415857c06c007 100644 (file)
@@ -74,7 +74,7 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                 </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
@@ -85,7 +85,7 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                     <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)} />
@@ -112,13 +112,13 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                         <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>
@@ -128,13 +128,13 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
                 */}
                 <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>;
         }
diff --git a/src/views/run-process-panel/inputs/project-input.tsx b/src/views/run-process-panel/inputs/project-input.tsx
new file mode 100644 (file)
index 0000000..7b45a6d
--- /dev/null
@@ -0,0 +1,136 @@
+// 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>;
+        }
+
+    });
index 13d882ba3ca874ea63df48735c2e6504a29f95d6..32a126a458fdcbd666969a95d09b34628889e104 100644 (file)
@@ -6,14 +6,19 @@ import React from 'react';
 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
@@ -24,7 +29,7 @@ export const RunProcessBasicForm =
                     <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>
@@ -32,7 +37,14 @@ export const RunProcessBasicForm =
                     <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>);
index 3c42437a674344e257b504caa6288b91166a0224..9dd5aa3fd0d98cd79b08a95315a9761b6c9c42e9 100644 (file)
@@ -3,7 +3,7 @@
 // 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';
 
@@ -17,25 +17,34 @@ export type RunProcessPanelRootActionProps = RunProcessFirstStepActionProps & {
 
 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>);
index 08cf4e6caf43955ddc39750aaa5c6d9bc8e8f8e4..2f41dedb8232dcae9184c1d386b78ea97fd26772 100644 (file)
@@ -13,7 +13,6 @@ import { isValid } from 'redux-form';
 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 {
@@ -58,14 +57,6 @@ export const RunProcessSecondStepForm = connect(mapStateToProps, { onPresetChang
     ({ 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 />
@@ -75,7 +66,7 @@ export const RunProcessSecondStepForm = connect(mapStateToProps, { onPresetChang
                     Back
                 </Button>
                 <Button disabled={!valid} variant="contained" color="primary" onClick={runProcess}>
-                    Run Process
+                    Run workflow
                 </Button>
             </Grid>
         </Grid>);
index 9c1d81c3ed21ecc712a154fb1abe44b779f9467f..df1b28112a067185254e5f50659d65a334ccfa8d 100644 (file)
@@ -14,13 +14,13 @@ import {
     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';
 
@@ -54,10 +54,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     },
 });
 
-interface WorkflowDetailsCardDataProps {
-    workflow?: WorkflowResource;
-}
-
 type WorkflowDetailsCardProps = WorkflowDetailsCardDataProps & WithStyles<CssRules>;
 
 export const WorkflowDetailsCard = withStyles(styles)(
@@ -77,16 +73,16 @@ 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
@@ -96,12 +92,12 @@ export const WorkflowDetailsCard = withStyles(styles)(
                             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>;