Merge branch '19482-wf-panel' refs #19482
authorPeter Amstutz <peter.amstutz@curii.com>
Tue, 28 Mar 2023 13:47:12 +0000 (09:47 -0400)
committerPeter Amstutz <peter.amstutz@curii.com>
Tue, 28 Mar 2023 13:47:12 +0000 (09:47 -0400)
Arvados-DCO-1.1-Signed-off-by: Peter Amstutz <peter.amstutz@curii.com>

19 files changed:
cypress/integration/collection.spec.js
cypress/integration/process.spec.js
cypress/integration/project.spec.js
cypress/integration/workflow.spec.js [new file with mode: 0644]
src/routes/route-change-handlers.ts
src/routes/routes.ts
src/store/advanced-tab/advanced-tab.tsx
src/store/breadcrumbs/breadcrumbs-actions.ts
src/store/navigation/navigation-action.ts
src/store/process-panel/process-panel-actions.ts
src/store/workbench/workbench-actions.ts
src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
src/views-components/context-menu/action-sets/workflow-action-set.ts
src/views-components/details-panel/workflow-details.tsx
src/views/collection-panel/collection-panel.tsx
src/views/process-panel/process-details-card.tsx
src/views/process-panel/process-io-card.tsx
src/views/workbench/workbench.tsx
src/views/workflow-panel/registered-workflow-panel.tsx [new file with mode: 0644]

index efde53e5e87f1762695cb8bfe6a6192222a4091e..1a66600c8a8e6b995f0bb71696e9382a56e7b56f 100644 (file)
@@ -455,7 +455,10 @@ describe('Collection panel tests', function () {
                         });
                     cy.get('[data-cy=form-submit-btn]').click();
 
-                    cy.get('[data-cy=collection-files-panel]')
+                    // need to wait for dialog to dismiss
+                    cy.get('[data-cy=form-dialog]').should('not.exist');
+
+                    cy.waitForDom().get('[data-cy=collection-files-panel]')
                         .contains('Home')
                         .click();
 
@@ -470,6 +473,7 @@ describe('Collection panel tests', function () {
                         .contains('Remove')
                         .click();
                     cy.get('[data-cy=confirmation-dialog-ok-btn]').click();
+                    cy.get('[data-cy=form-dialog]').should('not.exist');
                 });
             });
     });
@@ -1059,8 +1063,11 @@ describe('Collection panel tests', function () {
                         cy.fixture('files/5mb.bin', 'base64').then(content => {
                             cy.get('[data-cy=drag-and-drop]').upload(content, '5mb_b.bin');
                             cy.get('[data-cy=form-submit-btn]').click();
-                            cy.get('[data-cy=form-submit-btn]').should('not.exist');
-                            cy.get('[data-cy=collection-files-right-panel]')
+                            cy.waitForDom().get('[data-cy=form-submit-btn]').should('not.exist');
+                            // subdir gets unselected, I think this is a bug but
+                            // for the time being let's just make sure the test works.
+                            cy.get('[data-cy=collection-files-panel]').contains('subdir').click();
+                            cy.waitForDom().get('[data-cy=collection-files-right-panel]')
                                  .contains('5mb_b.bin').should('exist');
                         });
                     });
index 19544c9ca543bcbc257b823df48f2d2eaf97fa6f..bdb4fae61ae0f58062fab989752a348dac0adc7f 100644 (file)
@@ -1040,7 +1040,7 @@ describe('Process tests', function() {
             cy.get('[data-cy=process-io-card] h6').contains('Outputs')
                 .parents('[data-cy=process-io-card]').within((ctx) => {
                     cy.get(ctx).scrollIntoView();
-                    cy.get('[data-cy="io-preview-image-toggle"]').click();
+                    cy.get('[data-cy="io-preview-image-toggle"]').click({waitForAnimations: false});
                     const outPdh = testOutputCollection.portable_data_hash;
 
                     verifyIOParameter('output_file', null, "Label Description", 'cat.png', `${outPdh}`);
index cdb49c86741a90422688a8b88fc9c160e8d8fcd7..68a90133d25270f937bfdb6d4ecb299036902040 100644 (file)
@@ -544,7 +544,7 @@ describe('Project tests', function() {
                 });
                 cy.get('[data-cy=form-submit-btn]').click();
             });
-
+        cy.get('[data-cy=form-dialog]').should("not.exist");
         cy.get('[data-cy=side-panel-tree]').contains('Projects').click();
         cy.get('[data-cy=project-panel]').contains(projectName).should('be.visible').rightclick();
         cy.get('[data-cy=context-menu]').contains('Copy to clipboard').click();
diff --git a/cypress/integration/workflow.spec.js b/cypress/integration/workflow.spec.js
new file mode 100644 (file)
index 0000000..e1fa20a
--- /dev/null
@@ -0,0 +1,237 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+describe('Registered workflow panel tests', function() {
+    let activeUser;
+    let adminUser;
+
+    before(function() {
+        // Only set up common users once. These aren't set up as aliases because
+        // aliases are cleaned up after every test. Also it doesn't make sense
+        // to set the same users on beforeEach() over and over again, so we
+        // separate a little from Cypress' 'Best Practices' here.
+        cy.getUser('admin', 'Admin', 'User', true, true)
+            .as('adminUser').then(function() {
+                adminUser = this.adminUser;
+            }
+        );
+        cy.getUser('user', 'Active', 'User', false, true)
+            .as('activeUser').then(function() {
+                activeUser = this.activeUser;
+            }
+        );
+    });
+
+    it('should handle null definition', function() {
+        cy.createResource(activeUser.token, "workflows", {workflow: {name: "Test wf"}})
+            .then(function(workflowResource) {
+                cy.loginAs(activeUser);
+                cy.goToPath(`/workflows/${workflowResource.uuid}`);
+                cy.get('[data-cy=registered-workflow-info-panel]').should('contain', workflowResource.name);
+                cy.get('[data-cy=workflow-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
+            });
+    });
+
+    it('should handle malformed definition', function() {
+        cy.createResource(activeUser.token, "workflows", {workflow: {name: "Test wf", definition: "zap:"}})
+            .then(function(workflowResource) {
+                cy.loginAs(activeUser);
+                cy.goToPath(`/workflows/${workflowResource.uuid}`);
+                cy.get('[data-cy=registered-workflow-info-panel]').should('contain', workflowResource.name);
+                cy.get('[data-cy=workflow-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
+            });
+    });
+
+    it('should handle malformed run', function() {
+        cy.createResource(activeUser.token, "workflows", {workflow: {
+            name: "Test wf",
+            definition: JSON.stringify({
+                cwlVersion: "v1.2",
+                $graph: [
+                    {
+                        "class": "Workflow",
+                        "id": "#main",
+                        "inputs": [],
+                        "outputs": [],
+                        "requirements": [
+                            {
+                                "class": "SubworkflowFeatureRequirement"
+                            }
+                        ],
+                        "steps": [
+                            {
+                                "id": "#main/cat1-testcli.cwl (v1.2.0-109-g9b091ed)",
+                                "in": [],
+                                "label": "cat1-testcli.cwl (v1.2.0-109-g9b091ed)",
+                                "out": [
+                                    {
+                                        "id": "#main/step/args"
+                                    }
+                                ],
+                                "run": `keep:undefined/bar`
+                            }
+                        ]
+                    }
+                ],
+                "cwlVersion": "v1.2",
+                "http://arvados.org/cwl#gitBranch": "1.2.1_proposed",
+                "http://arvados.org/cwl#gitCommit": "9b091ed7e0bef98b3312e9478c52b89ba25792de",
+                "http://arvados.org/cwl#gitCommitter": "GitHub <noreply@github.com>",
+                "http://arvados.org/cwl#gitDate": "Sun, 11 Sep 2022 21:24:42 +0200",
+                "http://arvados.org/cwl#gitDescribe": "v1.2.0-109-g9b091ed",
+                "http://arvados.org/cwl#gitOrigin": "git@github.com:common-workflow-language/cwl-v1.2",
+                "http://arvados.org/cwl#gitPath": "tests/cat1-testcli.cwl",
+                "http://arvados.org/cwl#gitStatus": ""
+            })
+        }}).then(function(workflowResource) {
+            cy.loginAs(activeUser);
+            cy.goToPath(`/workflows/${workflowResource.uuid}`);
+            cy.get('[data-cy=registered-workflow-info-panel]').should('contain', workflowResource.name);
+            cy.get('[data-cy=workflow-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
+        });
+    });
+
+    const verifyIOParameter = (name, label, doc, val, collection) => {
+        cy.get('table tr').contains(name).parents('tr').within(($mainRow) => {
+            label && cy.contains(label);
+
+            if (val) {
+                if (Array.isArray(val)) {
+                    val.forEach(v => cy.contains(v));
+                } else {
+                    cy.contains(val);
+                }
+            }
+            if (collection) {
+                cy.contains(collection);
+            }
+        });
+    };
+
+    it('shows workflow details', function() {
+        cy.createCollection(adminUser.token, {
+            name: `Test collection ${Math.floor(Math.random() * 999999)}`,
+            owner_uuid: activeUser.user.uuid,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+        })
+            .then(function(collectionResource) {
+                cy.createResource(activeUser.token, "workflows", {workflow: {
+                    name: "Test wf",
+                    definition: JSON.stringify({
+                        cwlVersion: "v1.2",
+                        $graph: [
+                            {
+                                "class": "Workflow",
+                                "hints": [
+                                    {
+                                        "class": "DockerRequirement",
+                                        "dockerPull": "python:2-slim"
+                                    }
+                                ],
+                                "id": "#main",
+                                "inputs": [
+                                    {
+                                        "id": "#main/file1",
+                                        "type": "File"
+                                    },
+                                    {
+                                        "id": "#main/numbering",
+                                        "type": [
+                                            "null",
+                                            "boolean"
+                                        ]
+                                    },
+                                    {
+                                        "default": {
+                                            "basename": "args.py",
+                                            "class": "File",
+                                            "location": "keep:de738550734533c5027997c87dc5488e+53/args.py",
+                                            "nameext": ".py",
+                                            "nameroot": "args",
+                                            "size": 179
+                                        },
+                                        "id": "#main/args.py",
+                                        "type": "File"
+                                    }
+                                ],
+                                "outputs": [
+                                    {
+                                        "id": "#main/args",
+                                        "outputSource": "#main/step/args",
+                                        "type": {
+                                            "items": "string",
+                                            "name": "_:b0adccc1-502d-476f-8a5b-c8ef7119e2dc",
+                                            "type": "array"
+                                        }
+                                    }
+                                ],
+                                "requirements": [
+                                    {
+                                        "class": "SubworkflowFeatureRequirement"
+                                    }
+                                ],
+                                "steps": [
+                                    {
+                                        "id": "#main/cat1-testcli.cwl (v1.2.0-109-g9b091ed)",
+                                        "in": [
+                                            {
+                                                "id": "#main/step/file1",
+                                                "source": "#main/file1"
+                                            },
+                                            {
+                                                "id": "#main/step/numbering",
+                                                "source": "#main/numbering"
+                                            },
+                                            {
+                                                "id": "#main/step/args.py",
+                                                "source": "#main/args.py"
+                                            }
+                                        ],
+                                        "label": "cat1-testcli.cwl (v1.2.0-109-g9b091ed)",
+                                        "out": [
+                                            {
+                                                "id": "#main/step/args"
+                                            }
+                                        ],
+                                        "run": `keep:${collectionResource.portable_data_hash}/bar`
+                                    }
+                                ]
+                            }
+                        ],
+                        "cwlVersion": "v1.2",
+                        "http://arvados.org/cwl#gitBranch": "1.2.1_proposed",
+                        "http://arvados.org/cwl#gitCommit": "9b091ed7e0bef98b3312e9478c52b89ba25792de",
+                        "http://arvados.org/cwl#gitCommitter": "GitHub <noreply@github.com>",
+                        "http://arvados.org/cwl#gitDate": "Sun, 11 Sep 2022 21:24:42 +0200",
+                        "http://arvados.org/cwl#gitDescribe": "v1.2.0-109-g9b091ed",
+                        "http://arvados.org/cwl#gitOrigin": "git@github.com:common-workflow-language/cwl-v1.2",
+                        "http://arvados.org/cwl#gitPath": "tests/cat1-testcli.cwl",
+                        "http://arvados.org/cwl#gitStatus": ""
+                    })
+                }}).then(function(workflowResource) {
+                    cy.loginAs(activeUser);
+                    cy.goToPath(`/workflows/${workflowResource.uuid}`);
+                    cy.get('[data-cy=registered-workflow-info-panel]').should('contain', workflowResource.name);
+                    cy.get('[data-cy=workflow-details-attributes-modifiedby-user]').contains(`Active User (${activeUser.user.uuid})`);
+                    cy.get('[data-cy=registered-workflow-info-panel')
+                        .should('contain', 'gitCommit: 9b091ed7e0bef98b3312e9478c52b89ba25792de')
+
+                    cy.get('[data-cy=process-io-card] h6').contains('Inputs')
+                        .parents('[data-cy=process-io-card]').within(() => {
+                            verifyIOParameter('file1', null, '', '', '');
+                            verifyIOParameter('numbering', null, '', '', '');
+                            verifyIOParameter('args.py', null, '', 'args.py', 'de738550734533c5027997c87dc5488e+53');
+                        });
+                    cy.get('[data-cy=process-io-card] h6').contains('Outputs')
+                        .parents('[data-cy=process-io-card]').within(() => {
+                            verifyIOParameter('args', null, '', '', '');
+                        });
+                    cy.get('[data-cy=collection-files-panel]').within(() => {
+                        cy.get('[data-cy=collection-files-right-panel]', { timeout: 5000 })
+                            .should('contain', 'bar');
+                    });
+                });
+            });
+    });
+});
index 237b6d9611bd3d5c9b3f8ff994c92a9dc075e5b5..cded6d65cd38dcce7e00fac722131399c324feec 100644 (file)
@@ -47,6 +47,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
     const linksMatch = Routes.matchLinksRoute(pathname);
     const collectionsContentAddressMatch = Routes.matchCollectionsContentAddressRoute(pathname);
     const allProcessesMatch = Routes.matchAllProcessesRoute(pathname);
+    const registeredWorkflowMatch = Routes.matchRegisteredWorkflowRoute(pathname);
 
     store.dispatch(dialogActions.CLOSE_ALL_DIALOGS());
     store.dispatch(contextMenuActions.CLOSE_CONTEXT_MENU());
@@ -112,5 +113,7 @@ const handleLocationChange = (store: RootStore) => ({ pathname }: Location) => {
         store.dispatch(WorkbenchActions.loadCollectionContentAddress);
     } else if (allProcessesMatch) {
         store.dispatch(WorkbenchActions.loadAllProcesses());
+    } else if (registeredWorkflowMatch) {
+        store.dispatch(WorkbenchActions.loadRegisteredWorkflow(registeredWorkflowMatch.params.id));
     }
 };
index 22c8f4c8e52414b8d84c99cc439facc4d4f9ecdf..4dfd998e8dadcc08450fc727b4389c03d199c5c0 100644 (file)
@@ -31,6 +31,7 @@ export const Routes = {
     VIRTUAL_MACHINES_ADMIN: '/virtual-machines-admin',
     VIRTUAL_MACHINES_USER: '/virtual-machines-user',
     WORKFLOWS: '/workflows',
+    REGISTEREDWORKFLOW: `/workflows/:id(${RESOURCE_UUID_PATTERN})`,
     SEARCH_RESULTS: '/search-results',
     SSH_KEYS_ADMIN: `/ssh-keys-admin`,
     SSH_KEYS_USER: `/ssh-keys-user`,
@@ -61,6 +62,8 @@ export const getResourceUrl = (uuid: string) => {
             return getCollectionUrl(uuid);
         case ResourceKind.PROCESS:
             return getProcessUrl(uuid);
+        case ResourceKind.WORKFLOW:
+            return getWorkflowUrl(uuid);
         default:
             return undefined;
     }
@@ -77,12 +80,12 @@ export const getNavUrl = (uuid: string, config: FederationConfig, includeToken:
     } else if (config.remoteHostsConfig[cls]) {
         let u: URL;
         if (config.remoteHostsConfig[cls].workbench2Url) {
-           /* NOTE: wb2 presently doesn't support passing api_token
-              to arbitrary page to set credentials, only through
-              api-token route.  So for navigation to work, user needs
-              to already be logged in.  In the future we want to just
-              request the records and display in the current
-              workbench instance making this redirect unnecessary. */
+            /* NOTE: wb2 presently doesn't support passing api_token
+               to arbitrary page to set credentials, only through
+               api-token route.  So for navigation to work, user needs
+               to already be logged in.  In the future we want to just
+               request the records and display in the current
+               workbench instance making this redirect unnecessary. */
             u = new URL(config.remoteHostsConfig[cls].workbench2Url);
         } else {
             u = new URL(config.remoteHostsConfig[cls].workbenchUrl);
@@ -100,6 +103,8 @@ export const getNavUrl = (uuid: string, config: FederationConfig, includeToken:
 
 export const getProcessUrl = (uuid: string) => `/processes/${uuid}`;
 
+export const getWorkflowUrl = (uuid: string) => `/workflows/${uuid}`;
+
 export const getGroupUrl = (uuid: string) => `/group/${uuid}`;
 
 export const getUserProfileUrl = (uuid: string) => `/user/${uuid}`;
@@ -120,6 +125,9 @@ export const matchTrashRoute = (route: string) =>
 export const matchAllProcessesRoute = (route: string) =>
     matchPath(route, { path: Routes.ALL_PROCESSES });
 
+export const matchRegisteredWorkflowRoute = (route: string) =>
+    matchPath<ResourceRouteParams>(route, { path: Routes.REGISTEREDWORKFLOW });
+
 export const matchProjectRoute = (route: string) =>
     matchPath<ResourceRouteParams>(route, { path: Routes.PROJECTS });
 
index ac088f025b8cdd72374584748d3764bb5752df48..82b4dfb02e2a6790ecb1a54d5b92f19818be57f3 100644 (file)
@@ -20,6 +20,7 @@ import { SshKeyResource } from 'models/ssh-key';
 import { VirtualMachinesResource } from 'models/virtual-machines';
 import { UserResource } from 'models/user';
 import { LinkResource } from 'models/link';
+import { WorkflowResource } from 'models/workflow';
 import { KeepServiceResource } from 'models/keep-services';
 import { ApiClientAuthorization } from 'models/api-client-authorization';
 import React from 'react';
@@ -101,9 +102,14 @@ enum LinkData {
     PROPERTIES = 'properties'
 }
 
-type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ApiClientAuthorizationsData | UserData | LinkData;
+enum WorkflowData {
+    WORKFLOW = 'workflow',
+    CREATED_AT = 'created_at'
+}
+
+type AdvanceResourceKind = CollectionData | ProcessData | ProjectData | RepositoryData | SshKeyData | VirtualMachineData | KeepServiceData | ApiClientAuthorizationsData | UserData | LinkData | WorkflowData;
 type AdvanceResourcePrefix = GroupContentsResourcePrefix | ResourcePrefix;
-type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | ApiClientAuthorization | UserResource | LinkResource | undefined;
+type AdvanceResponseData = ContainerRequestResource | ProjectResource | CollectionResource | RepositoryResource | SshKeyResource | VirtualMachinesResource | KeepServiceResource | ApiClientAuthorization | UserResource | LinkResource | WorkflowResource | undefined;
 
 export const openAdvancedTabDialog = (uuid: string) =>
     async (dispatch: Dispatch<any>, getState: () => RootState, services: ServiceRepository) => {
@@ -267,6 +273,23 @@ export const openAdvancedTabDialog = (uuid: string) =>
                 });
                 dispatch<any>(initAdvancedTabDialog(advanceDataLink));
                 break;
+            case ResourceKind.WORKFLOW:
+                const wfResources = getState().resources;
+                const dataWf = getResource<WorkflowResource>(uuid)(wfResources);
+                const advanceDataWf = advancedTabData({
+                    uuid,
+                    metadata: '',
+                    user: '',
+                    apiResponseKind: wfApiResponse,
+                    data: dataWf,
+                    resourceKind: WorkflowData.WORKFLOW,
+                    resourcePrefix: GroupContentsResourcePrefix.WORKFLOW,
+                    resourceKindProperty: WorkflowData.CREATED_AT,
+                    property: dataWf!.createdAt
+                });
+                dispatch<any>(initAdvancedTabDialog(advanceDataWf));
+                break;
+
             default:
                 dispatch(snackbarActions.OPEN_SNACKBAR({ message: "Could not open advanced tab for this resource.", hideDuration: 2000, kind: SnackbarKind.ERROR }));
         }
@@ -600,3 +623,22 @@ const linkApiResponse = (apiResponse: LinkResource): JSX.Element => {
 
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
+
+
+const wfApiResponse = (apiResponse: WorkflowResource): JSX.Element => {
+    const {
+        uuid, name,
+        ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, description, definition
+    } = apiResponse;
+    const response = `
+"uuid": "${uuid}",
+"name": "${name}",
+"owner_uuid": "${ownerUuid}",
+"created_at": "${stringify(createdAt)}",
+"modified_at": ${stringify(modifiedAt)},
+"modified_by_client_uuid": ${stringify(modifiedByClientUuid)},
+"modified_by_user_uuid": ${stringify(modifiedByUserUuid)}
+"description": ${stringify(description)}`;
+
+    return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
+};
index 74cfde00300d545e80db987aab4e7fe0e5d7696c..a7e42510b1239ffe643c809b0b7ecf884c881299 100644 (file)
@@ -22,20 +22,21 @@ import { ProcessResource } from 'models/process';
 import { OrderBuilder } from 'services/api/order-builder';
 import { Breadcrumb } from 'components/breadcrumbs/breadcrumbs';
 import { ContainerRequestResource, containerRequestFieldsNoMounts } from 'models/container-request';
-import { CollectionIcon, IconType, ProcessIcon, ProjectIcon } from 'components/icon/icon';
+import { CollectionIcon, IconType, ProcessIcon, ProjectIcon, WorkflowIcon } from 'components/icon/icon';
 import { CollectionResource } from 'models/collection';
 import { getSidePanelIcon } from 'views-components/side-panel-tree/side-panel-tree';
+import { WorkflowResource } from 'models/workflow';
 
 export const BREADCRUMBS = 'breadcrumbs';
 
-export const setBreadcrumbs = (breadcrumbs: any, currentItem?: CollectionResource | ContainerRequestResource | GroupResource) => {
+export const setBreadcrumbs = (breadcrumbs: any, currentItem?: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource) => {
     if (currentItem) {
         breadcrumbs.push(resourceToBreadcrumb(currentItem));
     }
     return propertiesActions.SET_PROPERTY({ key: BREADCRUMBS, value: breadcrumbs });
 };
 
-const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerRequestResource | GroupResource): IconType | undefined => {
+const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource): IconType | undefined => {
     switch (resource.kind) {
         case ResourceKind.PROJECT:
             return ProjectIcon;
@@ -43,12 +44,14 @@ const resourceToBreadcrumbIcon = (resource: CollectionResource | ContainerReques
             return ProcessIcon;
         case ResourceKind.COLLECTION:
             return CollectionIcon;
+        case ResourceKind.WORKFLOW:
+            return WorkflowIcon;
         default:
             return undefined;
     }
 }
 
-const resourceToBreadcrumb = (resource: CollectionResource | ContainerRequestResource | GroupResource): Breadcrumb => ({
+const resourceToBreadcrumb = (resource: CollectionResource | ContainerRequestResource | GroupResource | WorkflowResource): Breadcrumb => ({
     label: resource.name,
     uuid: resource.uuid,
     icon: resourceToBreadcrumbIcon(resource),
@@ -90,6 +93,9 @@ export const setSidePanelBreadcrumbs = (uuid: string) =>
                 breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
             }
             dispatch(setBreadcrumbs(breadcrumbs, processItem));
+        } else if (uuidKind === ResourceKind.WORKFLOW) {
+            const workflowItem = await services.workflowService.get(currentUuid);
+            dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
         }
         dispatch(setBreadcrumbs(breadcrumbs));
     };
@@ -136,6 +142,9 @@ export const setCategoryBreadcrumbs = (uuid: string, category: string) =>
                 breadcrumbs.push(resourceToBreadcrumb(parentProcessItem));
             }
             dispatch(setBreadcrumbs(breadcrumbs, processItem));
+        } else if (uuidKind === ResourceKind.WORKFLOW) {
+            const workflowItem = await services.workflowService.get(currentUuid);
+            dispatch(setBreadcrumbs(breadcrumbs, workflowItem));
         }
         dispatch(setBreadcrumbs(breadcrumbs));
     };
@@ -172,10 +181,10 @@ const getCollectionParent = (collection: CollectionResource) =>
         });
         const [parentOutput, parentLog] = await Promise.all([parentOutputPromise, parentLogPromise]);
         return parentOutput.items.length > 0 ?
-                parentOutput.items[0] :
-                parentLog.items.length > 0 ?
-                    parentLog.items[0] :
-                    undefined;
+            parentOutput.items[0] :
+            parentLog.items.length > 0 ?
+                parentLog.items[0] :
+                undefined;
     }
 
 
@@ -234,7 +243,7 @@ export const setUserProfileBreadcrumbs = (userUuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
             const user = getResource<UserResource>(userUuid)(getState().resources)
-                        || await services.userService.get(userUuid, false);
+                || await services.userService.get(userUuid, false);
             const breadcrumbs: Breadcrumb[] = [
                 { label: USERS_PANEL_LABEL, uuid: USERS_PANEL_LABEL },
                 { label: user ? user.username : userUuid, uuid: userUuid },
index 146530cae8e3ffaf530da19ca3be07743026c78f..edda2c61ca4d57861c98b7577238019923fc8db2 100644 (file)
@@ -42,7 +42,8 @@ export const navigateTo = (uuid: string) =>
                 dispatch<any>(navigateToAdminVirtualMachines);
                 return;
             case ResourceKind.WORKFLOW:
-                dispatch<any>(openDetailsPanel(uuid));
+                dispatch<any>(pushOrGoto(getNavUrl(uuid, getState().auth)));
+                // dispatch<any>(openDetailsPanel(uuid));
                 return;
         }
 
index b361f7acae0cbd8478fc7ed4896be92467863601..9668485c2cbbd6fd399ea9776c555fbedd643fe2 100644 (file)
@@ -165,7 +165,7 @@ export const initProcessPanelFilters = processPanelActions.SET_PROCESS_PANEL_FIL
     ProcessStatus.CANCELLED
 ]);
 
-const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
+export const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): ProcessIOParameter[] => {
     return inputs.map(input => {
         return {
             id: getIOParamId(input),
@@ -175,7 +175,7 @@ const formatInputData = (inputs: CommandInputParameter[], auth: AuthState): Proc
     });
 };
 
-const formatOutputData = (definitions: CommandOutputParameter[], values: any, pdh: string | undefined, auth: AuthState): ProcessIOParameter[] => {
+export const formatOutputData = (definitions: CommandOutputParameter[], values: any, pdh: string | undefined, auth: AuthState): ProcessIOParameter[] => {
     return definitions.map(output => {
         return {
             id: getIOParamId(output),
index 1cf71706420fc6c7be736b9d8e4282cdf94ace47..524337796efe37a6cfc472c9174eb3a7bd6012bd 100644 (file)
@@ -87,6 +87,7 @@ import {
     loadCollectionPanel,
 } from 'store/collection-panel/collection-panel-action';
 import { CollectionResource } from 'models/collection';
+import { WorkflowResource } from 'models/workflow';
 import {
     loadSearchResultsPanel,
     searchResultsPanelActions,
@@ -452,41 +453,34 @@ export const loadCollection = (uuid: string) =>
                     userUuid,
                     services,
                 });
+                let collection: CollectionResource | undefined;
+                let breadcrumbfunc: ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>) | undefined;
+                let sidepanel: string | undefined;
                 match({
-                    OWNED: (collection) => {
-                        dispatch(
-                            collectionPanelActions.SET_COLLECTION(
-                                collection as CollectionResource
-                            )
-                        );
-                        dispatch(updateResources([collection]));
-                        dispatch(activateSidePanelTreeItem(collection.ownerUuid));
-                        dispatch(setSidePanelBreadcrumbs(collection.ownerUuid));
-                        dispatch(loadCollectionPanel(collection.uuid));
+                    OWNED: (thecollection) => {
+                        collection = thecollection as CollectionResource;
+                        sidepanel = collection.ownerUuid;
+                        breadcrumbfunc = setSidePanelBreadcrumbs;
                     },
-                    SHARED: (collection) => {
-                        dispatch(
-                            collectionPanelActions.SET_COLLECTION(
-                                collection as CollectionResource
-                            )
-                        );
-                        dispatch(updateResources([collection]));
-                        dispatch<any>(setSharedWithMeBreadcrumbs(collection.ownerUuid));
-                        dispatch(activateSidePanelTreeItem(collection.ownerUuid));
-                        dispatch(loadCollectionPanel(collection.uuid));
+                    SHARED: (thecollection) => {
+                        collection = thecollection as CollectionResource;
+                        sidepanel = collection.ownerUuid;
+                        breadcrumbfunc = setSharedWithMeBreadcrumbs;
                     },
-                    TRASHED: (collection) => {
-                        dispatch(
-                            collectionPanelActions.SET_COLLECTION(
-                                collection as CollectionResource
-                            )
-                        );
-                        dispatch(updateResources([collection]));
-                        dispatch(setTrashBreadcrumbs(''));
-                        dispatch(activateSidePanelTreeItem(SidePanelTreeCategory.TRASH));
-                        dispatch(loadCollectionPanel(collection.uuid));
+                    TRASHED: (thecollection) => {
+                        collection = thecollection as CollectionResource;
+                        sidepanel = SidePanelTreeCategory.TRASH;
+                        breadcrumbfunc = () => setTrashBreadcrumbs('');
                     },
                 });
+                if (collection && breadcrumbfunc && sidepanel) {
+                    dispatch(updateResources([collection]));
+                    await dispatch<any>(finishLoadingProject(collection.ownerUuid));
+                    dispatch(collectionPanelActions.SET_COLLECTION(collection));
+                    await dispatch(activateSidePanelTreeItem(sidepanel));
+                    dispatch(breadcrumbfunc(collection.ownerUuid));
+                    dispatch(loadCollectionPanel(collection.uuid));
+                }
             }
         }
     );
@@ -580,6 +574,7 @@ export const loadProcess = (uuid: string) =>
         dispatch<any>(loadProcessPanel(uuid));
         const process = await dispatch<any>(processesActions.loadProcess(uuid));
         if (process) {
+            await dispatch<any>(finishLoadingProject(process.containerRequest.ownerUuid));
             await dispatch<any>(
                 activateSidePanelTreeItem(process.containerRequest.ownerUuid)
             );
@@ -588,6 +583,40 @@ export const loadProcess = (uuid: string) =>
         }
     });
 
+export const loadRegisteredWorkflow = (uuid: string) =>
+    handleFirstTimeLoad(async (dispatch: Dispatch,
+        getState: () => RootState,
+        services: ServiceRepository) => {
+
+        const userUuid = getUserUuid(getState());
+        if (userUuid) {
+            const match = await loadGroupContentsResource({
+                uuid,
+                userUuid,
+                services,
+            });
+            let workflow: WorkflowResource | undefined;
+            let breadcrumbfunc: ((uuid: string) => (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => Promise<void>) | undefined;
+            match({
+                OWNED: async (theworkflow) => {
+                    workflow = theworkflow as WorkflowResource;
+                    breadcrumbfunc = setSidePanelBreadcrumbs;
+                },
+                SHARED: async (theworkflow) => {
+                    workflow = theworkflow as WorkflowResource;
+                    breadcrumbfunc = setSharedWithMeBreadcrumbs;
+                },
+                TRASHED: () => { }
+            });
+            if (workflow && breadcrumbfunc) {
+                dispatch(updateResources([workflow]));
+                await dispatch<any>(finishLoadingProject(workflow.ownerUuid));
+                await dispatch<any>(activateSidePanelTreeItem(workflow.ownerUuid));
+                dispatch<any>(breadcrumbfunc(workflow.ownerUuid));
+            }
+        }
+    });
+
 export const updateProcess =
     (data: processUpdateActions.ProcessUpdateFormDialogData) =>
         async (dispatch: Dispatch) => {
@@ -876,8 +905,12 @@ const loadGroupContentsResource = async (params: {
             resource = await params.services.collectionService.get(params.uuid);
         } else if (kind === ResourceKind.PROJECT) {
             resource = await params.services.projectService.get(params.uuid);
-        } else {
+        } else if (kind === ResourceKind.WORKFLOW) {
+            resource = await params.services.workflowService.get(params.uuid);
+        } else if (kind === ResourceKind.CONTAINER_REQUEST) {
             resource = await params.services.containerRequestService.get(params.uuid);
+        } else {
+            throw new Error("loadGroupContentsResource unsupported kind " + kind)
         }
         handler = groupContentsHandlers.SHARED(resource);
     }
index bc84ed2cf3aeef617ff3cafcbacab432536a027a..3505faed4366b1518d6986176bad278d7a005fef 100644 (file)
@@ -120,6 +120,6 @@ const dialogContentExample = (example: JSX.Element | string, classes: any) => {
         className={classes.codeSnippet}
         lines={stringData ? [stringData] : []}
     >
-        {example as JSX.Element || null}
+        {React.isValidElement(example) ? (example as JSX.Element) : undefined}
     </DefaultCodeSnippet>;
 }
index 2aa78904e4cfa8938aa5f8e2de3e77554bea1013..cf28bcd3ff477ff27783059989f9d3a59354824d 100644 (file)
@@ -4,10 +4,55 @@
 
 import { ContextMenuActionSet } from "views-components/context-menu/context-menu-action-set";
 import { openRunProcess } from "store/workflow-panel/workflow-panel-actions";
+import {
+    RenameIcon,
+    ShareIcon,
+    MoveToIcon,
+    CopyIcon,
+    DetailsIcon,
+    AdvancedIcon,
+    OpenIcon,
+    Link,
+    RestoreVersionIcon,
+    FolderSharedIcon,
+    StartIcon
+} from "components/icon/icon";
+import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 
 export const workflowActionSet: ContextMenuActionSet = [[
     {
-        name: "Run",
+        icon: OpenIcon,
+        name: "Open in new tab",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openInNewTabAction(resource));
+        }
+    },
+    {
+        icon: Link,
+        name: "Copy to clipboard",
+        execute: (dispatch, resource) => {
+            dispatch<any>(copyToClipboardAction(resource));
+        }
+    },
+    {
+        icon: DetailsIcon,
+        name: "View details",
+        execute: dispatch => {
+            dispatch<any>(toggleDetailsPanel());
+        }
+    },
+    {
+        icon: AdvancedIcon,
+        name: "API Details",
+        execute: (dispatch, resource) => {
+            dispatch<any>(openAdvancedTabDialog(resource.uuid));
+        }
+    },
+    {
+        icon: StartIcon,
+        name: "Run Workflow",
         execute: (dispatch, resource) => {
             dispatch<any>(openRunProcess(resource.uuid, resource.ownerUuid, resource.name));
         }
index 98978dd279671eaf23a8ca174440208f4ffa1773..ca224b1d587aec3815c87c46396a54409f09b24d 100644 (file)
@@ -3,8 +3,11 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { WorkflowIcon } from 'components/icon/icon';
-import { WorkflowResource } from 'models/workflow';
+import { WorkflowIcon, StartIcon } from 'components/icon/icon';
+import {
+    WorkflowResource, parseWorkflowDefinition, getWorkflowInputs,
+    getWorkflowOutputs, getWorkflow
+} from 'models/workflow';
 import { DetailsData } from "./details-data";
 import { DetailsAttribute } from 'components/details-attribute/details-attribute';
 import { ResourceWithName } from 'views-components/data-explorer/renderers';
@@ -15,6 +18,11 @@ import { openRunProcess } from "store/workflow-panel/workflow-panel-actions";
 import { Dispatch } from 'redux';
 import { connect } from 'react-redux';
 import { ArvadosTheme } from 'common/custom-theme';
+import { ProcessIOParameter } from 'views/process-panel/process-io-card';
+import { formatInputData, formatOutputData } from 'store/process-panel/process-panel-actions';
+import { AuthState } from 'store/auth/auth-reducer';
+import { RootState } from 'store/store';
+import { getPropertyChip } from "views-components/resource-properties-form/property-chip";
 
 export interface WorkflowDetailsCardDataProps {
     workflow?: WorkflowResource;
@@ -29,29 +37,101 @@ const mapDispatchToProps = (dispatch: Dispatch) => ({
         () => wf && dispatch<any>(openRunProcess(wf.uuid, wf.ownerUuid, wf.name)),
 });
 
-type CssRules = 'runButton';
+type CssRules = 'runButton' | 'propertyTag';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     runButton: {
+        backgroundColor: theme.customs.colors.green700,
+        '&:hover': {
+            backgroundColor: theme.customs.colors.green800,
+        },
+        marginRight: "5px",
         boxShadow: 'none',
         padding: '2px 10px 2px 5px',
-        fontSize: '0.75rem'
+        marginLeft: 'auto'
+    },
+    propertyTag: {
+        marginRight: theme.spacing.unit / 2,
+        marginBottom: theme.spacing.unit / 2
     },
 });
 
-export const WorkflowDetailsAttributes = connect(null, mapDispatchToProps)(
+interface AuthStateDataProps {
+    auth: AuthState;
+};
+
+export interface RegisteredWorkflowPanelDataProps {
+    item: WorkflowResource;
+    workflowCollection: string;
+    inputParams: ProcessIOParameter[];
+    outputParams: ProcessIOParameter[];
+    gitprops: { [key: string]: string; };
+};
+
+export const getRegisteredWorkflowPanelData = (item: WorkflowResource, auth: AuthState): RegisteredWorkflowPanelDataProps => {
+    let inputParams: ProcessIOParameter[] = [];
+    let outputParams: ProcessIOParameter[] = [];
+    let workflowCollection = "";
+    const gitprops: { [key: string]: string; } = {};
+
+    // parse definition
+    const wfdef = parseWorkflowDefinition(item);
+
+    if (wfdef) {
+        const inputs = getWorkflowInputs(wfdef);
+        if (inputs) {
+            inputs.forEach(elm => {
+                if (elm.default !== undefined && elm.default !== null) {
+                    elm.value = elm.default;
+                }
+            });
+            inputParams = formatInputData(inputs, auth);
+        }
+
+        const outputs = getWorkflowOutputs(wfdef);
+        if (outputs) {
+            outputParams = formatOutputData(outputs, {}, undefined, auth);
+        }
+
+        const wf = getWorkflow(wfdef);
+        if (wf) {
+            const REGEX = /keep:([0-9a-f]{32}\+\d+)\/.*/;
+            if (wf["steps"]) {
+                const pdh = wf["steps"][0].run.match(REGEX);
+                if (pdh) {
+                    workflowCollection = pdh[1];
+                }
+            }
+        }
+
+        for (const elm in wfdef) {
+            if (elm.startsWith("http://arvados.org/cwl#git")) {
+                gitprops[elm.substr(23)] = wfdef[elm]
+            }
+        }
+    }
+
+    return { item, workflowCollection, inputParams, outputParams, gitprops };
+};
+
+const mapStateToProps = (state: RootState): AuthStateDataProps => {
+    return { auth: state.auth };
+};
+
+export const WorkflowDetailsAttributes = connect(mapStateToProps, mapDispatchToProps)(
     withStyles(styles)(
-        ({ workflow, onClick, classes }: WorkflowDetailsCardDataProps & WorkflowDetailsCardActionProps & WithStyles<CssRules>) => {
+        ({ workflow, onClick, auth, classes }: WorkflowDetailsCardDataProps & AuthStateDataProps & WorkflowDetailsCardActionProps & WithStyles<CssRules>) => {
+            if (!workflow) {
+                return <Grid />
+            }
+
+            const data = getRegisteredWorkflowPanelData(workflow, auth);
             return <Grid container>
                 <Button onClick={workflow && onClick(workflow)} className={classes.runButton} variant='contained'
-                    data-cy='details-panel-run-btn' color='primary' size='small'>
-                    Run
+                    data-cy='workflow-details-panel-run-btn' color='primary' size='small'>
+                    <StartIcon />
+                    Run Workflow
                 </Button>
-                {workflow && workflow.description !== "" && <Grid item xs={12} >
-                    <DetailsAttribute
-                        label={"Description"}
-                        value={workflow?.description} />
-                </Grid>}
                 <Grid item xs={12} >
                     <DetailsAttribute
                         label={"Workflow UUID"}
@@ -68,11 +148,16 @@ export const WorkflowDetailsAttributes = connect(null, mapDispatchToProps)(
                 <Grid item xs={12}>
                     <DetailsAttribute label='Last modified' value={formatDate(workflow?.modifiedAt)} />
                 </Grid>
-                <Grid item xs={12} >
+                <Grid item xs={12} data-cy="workflow-details-attributes-modifiedby-user">
                     <DetailsAttribute
                         label='Last modified by user' linkToUuid={workflow?.modifiedByUserUuid}
                         uuidEnhancer={(uuid: string) => <ResourceWithName uuid={uuid} />} />
                 </Grid>
+                <Grid item xs={12} md={12}>
+                    <DetailsAttribute label='Properties' />
+                    {Object.keys(data.gitprops).map(k =>
+                        getPropertyChip(k, data.gitprops[k], undefined, classes.propertyTag))}
+                </Grid>
             </Grid >;
         }));
 
index df1b1f1dfe8d4e8626b64ae5244899d982abff1d..54b7134ee42966bd29333e12770e575201247e6b 100644 (file)
@@ -11,7 +11,7 @@ import {
     Grid,
     Tooltip,
     Typography,
-    Card
+    Card, CardHeader, CardContent
 } from '@material-ui/core';
 import { connect, DispatchProp } from "react-redux";
 import { RouteComponentProps } from 'react-router';
@@ -51,7 +51,11 @@ type CssRules = 'root'
     | 'centeredLabel'
     | 'warningLabel'
     | 'collectionName'
-    | 'readOnlyIcon';
+    | 'readOnlyIcon'
+    | 'header'
+    | 'title'
+    | 'avatar'
+    | 'content';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -61,9 +65,6 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         cursor: 'pointer'
     },
     infoCard: {
-        paddingLeft: theme.spacing.unit * 2,
-        paddingRight: theme.spacing.unit * 2,
-        paddingBottom: theme.spacing.unit * 2,
     },
     propertiesCard: {
         padding: 0,
@@ -106,6 +107,26 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     readOnlyIcon: {
         marginLeft: theme.spacing.unit,
         fontSize: 'small',
+    },
+    header: {
+        paddingTop: theme.spacing.unit,
+        paddingBottom: theme.spacing.unit,
+    },
+    title: {
+        overflow: 'hidden',
+        paddingTop: theme.spacing.unit * 0.5,
+        color: theme.customs.colors.green700,
+    },
+    avatar: {
+        alignSelf: 'flex-start',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+    content: {
+        padding: theme.spacing.unit * 1.0,
+        paddingTop: theme.spacing.unit * 0.5,
+        '&:last-child': {
+            paddingBottom: theme.spacing.unit * 1,
+        }
     }
 });
 
@@ -152,24 +173,28 @@ export const CollectionPanel = withStyles(styles)(connect(
                     ? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
                         <MPVPanelContent xs="auto" data-cy='collection-info-panel'>
                             <Card className={classes.infoCard}>
-                                <Grid container justify="space-between">
-                                    <Grid item xs={11}><span>
-                                        <IconButton onClick={this.openCollectionDetails}>
-                                            {isOldVersion
-                                                ? <CollectionOldVersionIcon className={classes.iconHeader} />
-                                                : <CollectionIcon className={classes.iconHeader} />}
-                                        </IconButton>
-                                        <IllegalNamingWarning name={item.name} />
+                                <CardHeader
+                                    className={classes.header}
+                                    classes={{
+                                        content: classes.title,
+                                        avatar: classes.avatar,
+                                    }}
+                                    avatar={<IconButton onClick={this.openCollectionDetails}>
+                                        {isOldVersion
+                                            ? <CollectionOldVersionIcon className={classes.iconHeader} />
+                                            : <CollectionIcon className={classes.iconHeader} />}
+                                    </IconButton>}
+                                    title={
                                         <span>
+                                            <IllegalNamingWarning name={item.name} />
                                             {item.name}
                                             {isWritable ||
                                                 <Tooltip title="Read-only">
                                                     <ReadOnlyIcon data-cy="read-only-icon" className={classes.readOnlyIcon} />
-                                                </Tooltip>
-                                            }
+                                                </Tooltip>}
                                         </span>
-                                    </span></Grid>
-                                    <Grid item xs={1} style={{ textAlign: "right" }}>
+                                    }
+                                    action={
                                         <Tooltip title="Actions" disableFocusListener>
                                             <IconButton
                                                 data-cy='collection-panel-options-btn'
@@ -178,26 +203,24 @@ export const CollectionPanel = withStyles(styles)(connect(
                                                 <MoreOptionsIcon />
                                             </IconButton>
                                         </Tooltip>
-                                    </Grid>
-                                </Grid>
-                                <Grid container justify="space-between">
-                                    <Grid item xs={12}>
-                                        <Typography variant="caption">
-                                            {item.description}
+                                    }
+                                />
+                                <CardContent className={classes.content}>
+                                    <Typography variant="caption">
+                                        {item.description}
+                                    </Typography>
+                                    <CollectionDetailsAttributes item={item} classes={classes} twoCol={true} showVersionBrowser={() => dispatch<any>(openDetailsPanel(item.uuid, 1))} />
+                                    {(item.properties.container_request || item.properties.containerRequest) &&
+                                        <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
+                                            <DetailsAttribute classLabel={classes.link} label='Link to process' />
+                                        </span>
+                                    }
+                                    {isOldVersion &&
+                                        <Typography className={classes.warningLabel} variant="caption">
+                                            This is an old version. Make a copy to make changes. Go to the <Link to={getCollectionUrl(item.currentVersionUuid)}>head version</Link> for sharing options.
                                         </Typography>
-                                        <CollectionDetailsAttributes item={item} classes={classes} twoCol={true} showVersionBrowser={() => dispatch<any>(openDetailsPanel(item.uuid, 1))} />
-                                        {(item.properties.container_request || item.properties.containerRequest) &&
-                                            <span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
-                                                <DetailsAttribute classLabel={classes.link} label='Link to process' />
-                                            </span>
-                                        }
-                                        {isOldVersion &&
-                                            <Typography className={classes.warningLabel} variant="caption">
-                                                This is an old version. Make a copy to make changes. Go to the <Link to={getCollectionUrl(item.currentVersionUuid)}>head version</Link> for sharing options.
-                                            </Typography>
-                                        }
-                                    </Grid>
-                                </Grid>
+                                    }
+                                </CardContent>
                             </Card>
                         </MPVPanelContent>
                         <MPVPanelContent xs>
@@ -205,7 +228,7 @@ export const CollectionPanel = withStyles(styles)(connect(
                                 <CollectionPanelFiles isWritable={isWritable} />
                             </Card>
                         </MPVPanelContent>
-                    </MPVContainer>
+                    </MPVContainer >
                     : null;
             }
 
index 15728eb61f971bc48d484064f121934bec20517e..f339d1b3dfab65cc22dc091eb6db2fb68a5257e3 100644 (file)
@@ -142,10 +142,10 @@ export const ProcessDetailsCard = withStyles(styles)(
                                 <MoreOptionsIcon />
                             </IconButton>
                         </Tooltip>
-                        { doHidePanel &&
-                        <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
-                            <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
-                        </Tooltip> }
+                        {doHidePanel &&
+                            <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+                                <IconButton onClick={doHidePanel}><CloseIcon /></IconButton>
+                            </Tooltip>}
                     </div>
                 } />
             <CardContent className={classes.content}>
index 045bfca2113cce747cfdb3b099141d70fbb29efa..43be92406726c3b509faeab7eebc0dcf91540b90 100644 (file)
@@ -39,23 +39,23 @@ import {
 } from 'components/icon/icon';
 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 import {
-  BooleanCommandInputParameter,
-  CommandInputParameter,
-  CWLType,
-  Directory,
-  DirectoryArrayCommandInputParameter,
-  DirectoryCommandInputParameter,
-  EnumCommandInputParameter,
-  FileArrayCommandInputParameter,
-  FileCommandInputParameter,
-  FloatArrayCommandInputParameter,
-  FloatCommandInputParameter,
-  IntArrayCommandInputParameter,
-  IntCommandInputParameter,
-  isArrayOfType,
-  isPrimitiveOfType,
-  StringArrayCommandInputParameter,
-  StringCommandInputParameter,
+    BooleanCommandInputParameter,
+    CommandInputParameter,
+    CWLType,
+    Directory,
+    DirectoryArrayCommandInputParameter,
+    DirectoryCommandInputParameter,
+    EnumCommandInputParameter,
+    FileArrayCommandInputParameter,
+    FileCommandInputParameter,
+    FloatArrayCommandInputParameter,
+    FloatCommandInputParameter,
+    IntArrayCommandInputParameter,
+    IntCommandInputParameter,
+    isArrayOfType,
+    isPrimitiveOfType,
+    StringArrayCommandInputParameter,
+    StringCommandInputParameter,
 } from "models/workflow";
 import { CommandOutputParameter } from 'cwlts/mappings/v1.0/CommandOutputParameter';
 import { File } from 'models/workflow';
@@ -77,27 +77,27 @@ import { DefaultCodeSnippet } from 'components/default-code-snippet/default-code
 import { KEEP_URL_REGEX } from 'models/resource';
 
 type CssRules =
-  | "card"
-  | "content"
-  | "title"
-  | "header"
-  | "avatar"
-  | "iconHeader"
-  | "tableWrapper"
-  | "tableRoot"
-  | "paramValue"
-  | "keepLink"
-  | "collectionLink"
-  | "imagePreview"
-  | "valArray"
-  | "secondaryVal"
-  | "secondaryRow"
-  | "emptyValue"
-  | "noBorderRow"
-  | "symmetricTabs"
-  | "imagePlaceholder"
-  | "rowWithPreview"
-  | "labelColumn";
+    | "card"
+    | "content"
+    | "title"
+    | "header"
+    | "avatar"
+    | "iconHeader"
+    | "tableWrapper"
+    | "tableRoot"
+    | "paramValue"
+    | "keepLink"
+    | "collectionLink"
+    | "imagePreview"
+    | "valArray"
+    | "secondaryVal"
+    | "secondaryRow"
+    | "emptyValue"
+    | "noBorderRow"
+    | "symmetricTabs"
+    | "imagePlaceholder"
+    | "rowWithPreview"
+    | "labelColumn";
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     card: {
@@ -221,12 +221,13 @@ export enum ProcessIOCardType {
     OUTPUT = 'Outputs',
 }
 export interface ProcessIOCardDataProps {
-    process: Process;
+    process?: Process;
     label: ProcessIOCardType;
     params: ProcessIOParameter[] | null;
     raw: any;
     mounts?: InputCollectionMount[];
     outputUuid?: string;
+    showParams?: boolean;
 }
 
 export interface ProcessIOCardActionProps {
@@ -240,7 +241,8 @@ const mapDispatchToProps = (dispatch: Dispatch): ProcessIOCardActionProps => ({
 type ProcessIOCardProps = ProcessIOCardDataProps & ProcessIOCardActionProps & WithStyles<CssRules> & MPVPanelProps;
 
 export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps)(
-    ({ classes, label, params, raw, mounts, outputUuid, doHidePanel, doMaximizePanel, doUnMaximizePanel, panelMaximized, panelName, process, navigateTo }: ProcessIOCardProps) => {
+    ({ classes, label, params, raw, mounts, outputUuid, doHidePanel, doMaximizePanel,
+        doUnMaximizePanel, panelMaximized, panelName, process, navigateTo, showParams }: ProcessIOCardProps) => {
         const [mainProcTabState, setMainProcTabState] = useState(0);
         const [subProcTabState, setSubProcTabState] = useState(0);
         const handleMainProcTabChange = (event: React.MouseEvent<HTMLElement>, value: number) => {
@@ -253,7 +255,7 @@ export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps
         const [showImagePreview, setShowImagePreview] = useState(false);
 
         const PanelIcon = label === ProcessIOCardType.INPUT ? InputIcon : OutputIcon;
-        const mainProcess = !process.containerRequest.requestingContainerUuid;
+        const mainProcess = !(process && process!.containerRequest.requestingContainerUuid);
 
         const loading = raw === null || raw === undefined || params === null;
         const hasRaw = !!(raw && Object.keys(raw).length > 0);
@@ -278,47 +280,47 @@ export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps
                 }
                 action={
                     <div>
-                        { mainProcess && <Tooltip title={"Toggle Image Preview"} disableFocusListener>
-                            <IconButton data-cy="io-preview-image-toggle" onClick={() =>{setShowImagePreview(!showImagePreview)}}>{showImagePreview ? <ImageIcon /> : <ImageOffIcon />}</IconButton>
-                        </Tooltip> }
-                        { doUnMaximizePanel && panelMaximized &&
-                        <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
-                            <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
-                        </Tooltip> }
-                        { doMaximizePanel && !panelMaximized &&
-                        <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
-                            <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
-                        </Tooltip> }
-                        { doHidePanel &&
-                        <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
-                            <IconButton disabled={panelMaximized} onClick={doHidePanel}><CloseIcon /></IconButton>
-                        </Tooltip> }
+                        {mainProcess && <Tooltip title={"Toggle Image Preview"} disableFocusListener>
+                            <IconButton data-cy="io-preview-image-toggle" onClick={() => { setShowImagePreview(!showImagePreview) }}>{showImagePreview ? <ImageIcon /> : <ImageOffIcon />}</IconButton>
+                        </Tooltip>}
+                        {doUnMaximizePanel && panelMaximized &&
+                            <Tooltip title={`Unmaximize ${panelName || 'panel'}`} disableFocusListener>
+                                <IconButton onClick={doUnMaximizePanel}><UnMaximizeIcon /></IconButton>
+                            </Tooltip>}
+                        {doMaximizePanel && !panelMaximized &&
+                            <Tooltip title={`Maximize ${panelName || 'panel'}`} disableFocusListener>
+                                <IconButton onClick={doMaximizePanel}><MaximizeIcon /></IconButton>
+                            </Tooltip>}
+                        {doHidePanel &&
+                            <Tooltip title={`Close ${panelName || 'panel'}`} disableFocusListener>
+                                <IconButton disabled={panelMaximized} onClick={doHidePanel}><CloseIcon /></IconButton>
+                            </Tooltip>}
                     </div>
                 } />
             <CardContent className={classes.content}>
-                {mainProcess ?
+                {(mainProcess || showParams) ?
                     (<>
                         {/* raw is undefined until params are loaded */}
                         {loading && <Grid container item alignItems='center' justify='center'>
                             <CircularProgress />
                         </Grid>}
                         {/* Once loaded, either raw or params may still be empty
-                          *   Raw when all params are empty
-                          *   Params when raw is provided by containerRequest properties but workflow mount is absent for preview
-                          */}
+                          *   Raw when all params are empty
+                          *   Params when raw is provided by containerRequest properties but workflow mount is absent for preview
+                          */}
                         {(!loading && (hasRaw || hasParams)) &&
                             <>
                                 <Tabs value={mainProcTabState} onChange={handleMainProcTabChange} variant="fullWidth" className={classes.symmetricTabs}>
                                     {/* params will be empty on processes without workflow definitions in mounts, so we only show raw */}
                                     {hasParams && <Tab label="Parameters" />}
-                                    <Tab label="JSON" />
+                                    {!showParams && <Tab label="JSON" />}
                                 </Tabs>
                                 {(mainProcTabState === 0 && params && hasParams) && <div className={classes.tableWrapper}>
-                                        <ProcessIOPreview data={params} showImagePreview={showImagePreview} />
-                                    </div>}
+                                    <ProcessIOPreview data={params} showImagePreview={showImagePreview} valueLabel={showParams ? "Default value" : "Value"} />
+                                </div>}
                                 {(mainProcTabState === 1 || !hasParams) && <div className={classes.tableWrapper}>
-                                        <ProcessIORaw data={raw} />
-                                    </div>}
+                                    <ProcessIORaw data={raw} />
+                                </div>}
                             </>}
                         {!loading && !hasRaw && !hasParams && <Grid container item alignItems='center' justify='center'>
                             <DefaultView messages={["No parameters found"]} />
@@ -340,9 +342,9 @@ export const ProcessIOCard = withStyles(styles)(connect(null, mapDispatchToProps
                                     {subProcTabState === 0 && hasInputMounts && <ProcessInputMounts mounts={mounts || []} />}
                                     {subProcTabState === 0 && hasOutputCollecton && <>
                                         {outputUuid && <Typography className={classes.collectionLink}>
-                                            Output Collection: <MuiLink className={classes.keepLink} onClick={() => {navigateTo(outputUuid || "")}}>
-                                            {outputUuid}
-                                        </MuiLink></Typography>}
+                                            Output Collection: <MuiLink className={classes.keepLink} onClick={() => { navigateTo(outputUuid || "") }}>
+                                                {outputUuid}
+                                            </MuiLink></Typography>}
                                         <ProcessOutputCollectionFiles isWritable={false} currentItemUuid={outputUuid} />
                                     </>}
                                     {(subProcTabState === 1 || (!hasInputMounts && !hasOutputCollecton)) && <div className={classes.tableWrapper}>
@@ -377,19 +379,20 @@ export type ProcessIOParameter = {
 interface ProcessIOPreviewDataProps {
     data: ProcessIOParameter[];
     showImagePreview: boolean;
+    valueLabel: string;
 }
 
 type ProcessIOPreviewProps = ProcessIOPreviewDataProps & WithStyles<CssRules>;
 
 const ProcessIOPreview = withStyles(styles)(
-    ({ classes, data, showImagePreview }: ProcessIOPreviewProps) => {
+    ({ classes, data, showImagePreview, valueLabel }: ProcessIOPreviewProps) => {
         const showLabel = data.some((param: ProcessIOParameter) => param.label);
         return <Table className={classes.tableRoot} aria-label="Process IO Preview">
             <TableHead>
                 <TableRow>
                     <TableCell>Name</TableCell>
                     {showLabel && <TableCell className={classes.labelColumn}>Label</TableCell>}
-                    <TableCell>Value</TableCell>
+                    <TableCell>{valueLabel}</TableCell>
                     <TableCell>Collection</TableCell>
                 </TableRow>
             </TableHead>
@@ -401,7 +404,7 @@ const ProcessIOPreview = withStyles(styles)(
                         [classes.noBorderRow]: (rest.length > 0),
                     };
 
-                    return <>
+                    return <React.Fragment key={param.id}>
                         <TableRow className={classNames(mainRowClasses)} data-cy="process-io-param">
                             <TableCell>
                                 {param.id}
@@ -418,10 +421,10 @@ const ProcessIOPreview = withStyles(styles)(
                         </TableRow>
                         {rest.map((val, i) => {
                             const rowClasses = {
-                                [classes.noBorderRow]: (i < rest.length-1),
+                                [classes.noBorderRow]: (i < rest.length - 1),
                                 [classes.secondaryRow]: val.secondary,
                             };
-                            return <TableRow className={classNames(rowClasses)}>
+                            return <TableRow className={classNames(rowClasses)} key={i}>
                                 <TableCell />
                                 {showLabel && <TableCell />}
                                 <TableCell>
@@ -434,11 +437,11 @@ const ProcessIOPreview = withStyles(styles)(
                                 </TableCell>
                             </TableRow>
                         })}
-                    </>;
+                    </React.Fragment>;
                 })}
             </TableBody>
-        </Table>;
-});
+        </Table >;
+    });
 
 interface ProcessValuePreviewProps {
     value: ProcessIOValue;
@@ -446,7 +449,7 @@ interface ProcessValuePreviewProps {
 }
 
 const ProcessValuePreview = withStyles(styles)(
-    ({value, showImagePreview, classes}: ProcessValuePreviewProps & WithStyles<CssRules>) =>
+    ({ value, showImagePreview, classes }: ProcessValuePreviewProps & WithStyles<CssRules>) =>
         <Typography className={classes.paramValue}>
             {value.imageUrl && showImagePreview ? <img className={classes.imagePreview} src={value.imageUrl} alt="Inline Preview" /> : ""}
             {value.imageUrl && !showImagePreview ? <ImagePlaceholder /> : ""}
@@ -505,33 +508,33 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
         case isPrimitiveOfType(input, CWLType.BOOLEAN):
             const boolValue = (input as BooleanCommandInputParameter).value;
             return boolValue !== undefined &&
-                    !(Array.isArray(boolValue) && boolValue.length === 0) ?
-                [{display: renderPrimitiveValue(boolValue, false) }] :
-                [{display: <EmptyValue />}];
+                !(Array.isArray(boolValue) && boolValue.length === 0) ?
+                [{ display: renderPrimitiveValue(boolValue, false) }] :
+                [{ display: <EmptyValue /> }];
 
         case isPrimitiveOfType(input, CWLType.INT):
         case isPrimitiveOfType(input, CWLType.LONG):
             const intValue = (input as IntCommandInputParameter).value;
             return intValue !== undefined &&
-                    // Missing values are empty array
-                    !(Array.isArray(intValue) && intValue.length === 0) ?
-                [{display: renderPrimitiveValue(intValue, false) }]
-                : [{display: <EmptyValue />}];
+                // Missing values are empty array
+                !(Array.isArray(intValue) && intValue.length === 0) ?
+                [{ display: renderPrimitiveValue(intValue, false) }]
+                : [{ display: <EmptyValue /> }];
 
         case isPrimitiveOfType(input, CWLType.FLOAT):
         case isPrimitiveOfType(input, CWLType.DOUBLE):
             const floatValue = (input as FloatCommandInputParameter).value;
             return floatValue !== undefined &&
-                    !(Array.isArray(floatValue) && floatValue.length === 0) ?
-                [{display: renderPrimitiveValue(floatValue, false) }]:
-                [{display: <EmptyValue />}];
+                !(Array.isArray(floatValue) && floatValue.length === 0) ?
+                [{ display: renderPrimitiveValue(floatValue, false) }] :
+                [{ display: <EmptyValue /> }];
 
         case isPrimitiveOfType(input, CWLType.STRING):
             const stringValue = (input as StringCommandInputParameter).value || undefined;
             return stringValue !== undefined &&
-                    !(Array.isArray(stringValue) && stringValue.length === 0) ?
-                [{display: renderPrimitiveValue(stringValue, false) }] :
-                [{display: <EmptyValue />}];
+                !(Array.isArray(stringValue) && stringValue.length === 0) ?
+                [{ display: renderPrimitiveValue(stringValue, false) }] :
+                [{ display: <EmptyValue /> }];
 
         case isPrimitiveOfType(input, CWLType.FILE):
             const mainFile = (input as FileCommandInputParameter).value;
@@ -544,14 +547,14 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
             const mainFilePdhUrl = mainFile ? getResourcePdhUrl(mainFile, pdh) : "";
             return files.length ?
                 files.map((file, i) => fileToProcessIOValue(file, (i > 0), auth, pdh, (i > 0 ? mainFilePdhUrl : ""))) :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         case isPrimitiveOfType(input, CWLType.DIRECTORY):
             const directory = (input as DirectoryCommandInputParameter).value;
             return directory !== undefined &&
-                    !(Array.isArray(directory) && directory.length === 0) ?
+                !(Array.isArray(directory) && directory.length === 0) ?
                 [directoryToProcessIOValue(directory, auth, pdh)] :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         case typeof input.type === 'object' &&
             !(input.type instanceof Array) &&
@@ -559,27 +562,27 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
             const enumValue = (input as EnumCommandInputParameter).value;
             return enumValue !== undefined && enumValue ?
                 [{ display: <pre>{enumValue}</pre> }] :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         case isArrayOfType(input, CWLType.STRING):
             const strArray = (input as StringArrayCommandInputParameter).value || [];
             return strArray.length ?
                 [{ display: <>{strArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         case isArrayOfType(input, CWLType.INT):
         case isArrayOfType(input, CWLType.LONG):
             const intArray = (input as IntArrayCommandInputParameter).value || [];
             return intArray.length ?
                 [{ display: <>{intArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         case isArrayOfType(input, CWLType.FLOAT):
         case isArrayOfType(input, CWLType.DOUBLE):
             const floatArray = (input as FloatArrayCommandInputParameter).value || [];
             return floatArray.length ?
                 [{ display: <>{floatArray.map((val) => renderPrimitiveValue(val, true))}</> }] :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         case isArrayOfType(input, CWLType.FILE):
             const fileArrayMainFiles = ((input as FileArrayCommandInputParameter).value || []);
@@ -593,21 +596,21 @@ export const getIOParamDisplayValue = (auth: AuthState, input: CommandInputParam
                     ...(mainFile ? [fileToProcessIOValue(mainFile, false, auth, pdh, i > 0 ? firstMainFilePdh : "")] : []),
                     ...(secondaryFiles.map(file => fileToProcessIOValue(file, true, auth, pdh, firstMainFilePdh)))
                 ];
-            // Reduce each mainFile/secondaryFile group into single array preserving ordering
+                // Reduce each mainFile/secondaryFile group into single array preserving ordering
             }).reduce((acc: ProcessIOValue[], mainFile: ProcessIOValue[]) => (acc.concat(mainFile)), []);
 
             return fileArrayValues.length ?
                 fileArrayValues :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         case isArrayOfType(input, CWLType.DIRECTORY):
             const directories = (input as DirectoryArrayCommandInputParameter).value || [];
             return directories.length ?
                 directories.map(directory => directoryToProcessIOValue(directory, auth, pdh)) :
-                [{display: <EmptyValue />}];
+                [{ display: <EmptyValue /> }];
 
         default:
-            return [{display: <UnsupportedValue />}];
+            return [{ display: <UnsupportedValue /> }];
     }
 };
 
@@ -626,8 +629,8 @@ const renderPrimitiveValue = (value: any, asChip: boolean) => {
 const getKeepUrl = (file: File | Directory, pdh?: string): string => {
     const isKeepUrl = file.location?.startsWith('keep:') || false;
     const keepUrl = isKeepUrl ?
-                        file.location?.replace('keep:', '') :
-                        pdh ? `${pdh}/${file.location}` : file.location;
+        file.location?.replace('keep:', '') :
+        pdh ? `${pdh}/${file.location}` : file.location;
     return keepUrl || '';
 };
 
@@ -642,7 +645,7 @@ const getResourcePdhUrl = (res: File | Directory, pdh?: string): string => {
     return keepUrl ? keepUrl.split('/').slice(0, 1)[0] : '';
 };
 
-const KeepUrlBase = withStyles(styles)(({auth, res, pdh, classes}: KeepUrlProps & WithStyles<CssRules>) => {
+const KeepUrlBase = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
     const pdhUrl = getResourcePdhUrl(res, pdh);
     // Passing a pdh always returns a relative wb2 collection url
     const pdhWbPath = getNavUrl(pdhUrl, auth);
@@ -651,7 +654,7 @@ const KeepUrlBase = withStyles(styles)(({auth, res, pdh, classes}: KeepUrlProps
         <></>;
 });
 
-const KeepUrlPath = withStyles(styles)(({auth, res, pdh, classes}: KeepUrlProps & WithStyles<CssRules>) => {
+const KeepUrlPath = withStyles(styles)(({ auth, res, pdh, classes }: KeepUrlProps & WithStyles<CssRules>) => {
     const keepUrl = getKeepUrl(res, pdh);
     const keepUrlParts = keepUrl ? keepUrl.split('/') : [];
     const keepUrlPath = keepUrlParts.length > 1 ? keepUrlParts.slice(1).join('/') : '';
@@ -692,17 +695,17 @@ const normalizeDirectoryLocation = (directory: Directory): Directory => {
 };
 
 const directoryToProcessIOValue = (directory: Directory, auth: AuthState, pdh?: string): ProcessIOValue => {
-    if (isExternalValue(directory)) {return {display: <UnsupportedValue />}}
+    if (isExternalValue(directory)) { return { display: <UnsupportedValue /> } }
 
     const normalizedDirectory = normalizeDirectoryLocation(directory);
     return {
-        display: <KeepUrlPath auth={auth} res={normalizedDirectory} pdh={pdh}/>,
-        collection: <KeepUrlBase auth={auth} res={normalizedDirectory} pdh={pdh}/>,
+        display: <KeepUrlPath auth={auth} res={normalizedDirectory} pdh={pdh} />,
+        collection: <KeepUrlBase auth={auth} res={normalizedDirectory} pdh={pdh} />,
     };
 };
 
 const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, pdh: string | undefined, mainFilePdh: string): ProcessIOValue => {
-    if (isExternalValue(file)) {return {display: <UnsupportedValue />}}
+    if (isExternalValue(file)) { return { display: <UnsupportedValue /> } }
 
     if (isFileUrl(file.location)) {
         return {
@@ -713,10 +716,10 @@ const fileToProcessIOValue = (file: File, secondary: boolean, auth: AuthState, p
 
     const resourcePdh = getResourcePdhUrl(file, pdh);
     return {
-        display: <KeepUrlPath auth={auth} res={file} pdh={pdh}/>,
+        display: <KeepUrlPath auth={auth} res={file} pdh={pdh} />,
         secondary,
         imageUrl: isFileImage(file.basename) ? getImageUrl(auth, file, pdh) : undefined,
-        collection: (resourcePdh !== mainFilePdh) ? <KeepUrlBase auth={auth} res={file} pdh={pdh}/> : <></>,
+        collection: (resourcePdh !== mainFilePdh) ? <KeepUrlBase auth={auth} res={file} pdh={pdh} /> : <></>,
     }
 };
 
@@ -724,18 +727,18 @@ const isExternalValue = (val: any) =>
     Object.keys(val).includes('$import') ||
     Object.keys(val).includes('$include')
 
-const EmptyValue = withStyles(styles)(
-    ({classes}: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>
+export const EmptyValue = withStyles(styles)(
+    ({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>No value</span>
 );
 
 const UnsupportedValue = withStyles(styles)(
-    ({classes}: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>
+    ({ classes }: WithStyles<CssRules>) => <span className={classes.emptyValue}>Cannot display value</span>
 );
 
 const UnsupportedValueChip = withStyles(styles)(
-    ({classes}: WithStyles<CssRules>) => <Chip icon={<InfoIcon />} label={"Cannot display value"} />
+    ({ classes }: WithStyles<CssRules>) => <Chip icon={<InfoIcon />} label={"Cannot display value"} />
 );
 
 const ImagePlaceholder = withStyles(styles)(
-    ({classes}: WithStyles<CssRules>) => <span className={classes.imagePlaceholder}><ImageIcon /></span>
+    ({ classes }: WithStyles<CssRules>) => <span className={classes.imagePlaceholder}><ImageIcon /></span>
 );
index 7103efd132a1b8e1ac149229d1fbcda0e7620a7f..d549c52935136d42c6fb295b71269a5750a04c4a 100644 (file)
@@ -41,6 +41,7 @@ import { SharedWithMePanel } from 'views/shared-with-me-panel/shared-with-me-pan
 import { RunProcessPanel } from 'views/run-process-panel/run-process-panel';
 import SplitterLayout from 'react-splitter-layout';
 import { WorkflowPanel } from 'views/workflow-panel/workflow-panel';
+import { RegisteredWorkflowPanel } from 'views/workflow-panel/registered-workflow-panel';
 import { SearchResultsPanel } from 'views/search-results-panel/search-results-panel';
 import { SshKeyPanel } from 'views/ssh-key-panel/ssh-key-panel';
 import { SshKeyAdminPanel } from 'views/ssh-key-panel/ssh-key-admin-panel';
@@ -166,6 +167,7 @@ let routes = <>
     <Route path={Routes.TRASH} component={TrashPanel} />
     <Route path={Routes.SHARED_WITH_ME} component={SharedWithMePanel} />
     <Route path={Routes.RUN_PROCESS} component={RunProcessPanel} />
+    <Route path={Routes.REGISTEREDWORKFLOW} component={RegisteredWorkflowPanel} />
     <Route path={Routes.WORKFLOWS} component={WorkflowPanel} />
     <Route path={Routes.SEARCH_RESULTS} component={SearchResultsPanel} />
     <Route path={Routes.VIRTUAL_MACHINES_USER} component={VirtualMachineUserPanel} />
@@ -195,22 +197,22 @@ routes = React.createElement(React.Fragment, null, pluginConfig.centerPanelList.
 const applyCollapsedState = (isCollapsed) => {
     const rightPanel: Element = document.getElementsByClassName('layout-pane')[1]
     const totalWidth: number = document.getElementsByClassName('splitter-layout')[0]?.clientWidth
-    const rightPanelExpandedWidth = ((totalWidth-COLLAPSE_ICON_SIZE)) / (totalWidth/100) 
-    if(rightPanel) {
+    const rightPanelExpandedWidth = ((totalWidth - COLLAPSE_ICON_SIZE)) / (totalWidth / 100)
+    if (rightPanel) {
         rightPanel.setAttribute('style', `width: ${isCollapsed ? rightPanelExpandedWidth : getSplitterInitialSize()}%`)
     }
     const splitter = document.getElementsByClassName('layout-splitter')[0]
     isCollapsed ? splitter?.classList.add('layout-splitter-disabled') : splitter?.classList.remove('layout-splitter-disabled')
-    
+
 }
 
 export const WorkbenchPanel =
-    withStyles(styles)((props: WorkbenchPanelProps) =>{
+    withStyles(styles)((props: WorkbenchPanelProps) => {
 
         //panel size will not scale automatically on window resize, so we do it manually
-        window.addEventListener('resize', ()=>applyCollapsedState(props.sidePanelIsCollapsed))
+        window.addEventListener('resize', () => applyCollapsedState(props.sidePanelIsCollapsed))
         applyCollapsedState(props.sidePanelIsCollapsed)
-        
+
         return <Grid container item xs className={props.classes.root}>
             {props.sessionIdleTimeout > 0 && <AutoLogout />}
             <Grid container item xs className={props.classes.container}>
@@ -296,5 +298,6 @@ export const WorkbenchPanel =
             <WebDavS3InfoDialog />
             <Banner />
             {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
-        </Grid>}
+        </Grid>
+    }
     );
diff --git a/src/views/workflow-panel/registered-workflow-panel.tsx b/src/views/workflow-panel/registered-workflow-panel.tsx
new file mode 100644 (file)
index 0000000..7b0f2a4
--- /dev/null
@@ -0,0 +1,225 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Tooltip,
+    Typography,
+    Card,
+    CardHeader,
+    CardContent,
+    IconButton,
+} from '@material-ui/core';
+import { Dispatch } from "redux";
+import { connect, DispatchProp } from "react-redux";
+import { RouteComponentProps } from 'react-router';
+import { ArvadosTheme } from 'common/custom-theme';
+import { RootState } from 'store/store';
+import { WorkflowIcon, MoreOptionsIcon } from 'components/icon/icon';
+import { WorkflowResource } from 'models/workflow';
+import { ProcessOutputCollectionFiles } from 'views/process-panel/process-output-collection-files';
+import { WorkflowDetailsAttributes, RegisteredWorkflowPanelDataProps, getRegisteredWorkflowPanelData } from 'views-components/details-panel/workflow-details';
+import { getResource } from 'store/resources/resources';
+import { openContextMenu, resourceUuidToContextMenuKind } from 'store/context-menu/context-menu-actions';
+import { MPVContainer, MPVPanelContent, MPVPanelState } from 'components/multi-panel-view/multi-panel-view';
+import { ProcessIOCard, ProcessIOCardType } from 'views/process-panel/process-io-card';
+
+type CssRules = 'root'
+    | 'button'
+    | 'infoCard'
+    | 'propertiesCard'
+    | 'filesCard'
+    | 'iconHeader'
+    | 'tag'
+    | 'label'
+    | 'value'
+    | 'link'
+    | 'centeredLabel'
+    | 'warningLabel'
+    | 'collectionName'
+    | 'readOnlyIcon'
+    | 'header'
+    | 'title'
+    | 'avatar'
+    | 'content';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    root: {
+        width: '100%',
+    },
+    button: {
+        cursor: 'pointer'
+    },
+    infoCard: {
+    },
+    propertiesCard: {
+        padding: 0,
+    },
+    filesCard: {
+        padding: 0,
+    },
+    iconHeader: {
+        fontSize: '1.875rem',
+        color: theme.customs.colors.greyL
+    },
+    tag: {
+        marginRight: theme.spacing.unit / 2,
+        marginBottom: theme.spacing.unit / 2
+    },
+    label: {
+        fontSize: '0.875rem',
+    },
+    centeredLabel: {
+        fontSize: '0.875rem',
+        textAlign: 'center'
+    },
+    warningLabel: {
+        fontStyle: 'italic'
+    },
+    collectionName: {
+        flexDirection: 'column',
+    },
+    value: {
+        textTransform: 'none',
+        fontSize: '0.875rem'
+    },
+    link: {
+        fontSize: '0.875rem',
+        color: theme.palette.primary.main,
+        '&:hover': {
+            cursor: 'pointer'
+        }
+    },
+    readOnlyIcon: {
+        marginLeft: theme.spacing.unit,
+        fontSize: 'small',
+    },
+    header: {
+        paddingTop: theme.spacing.unit,
+        paddingBottom: theme.spacing.unit,
+    },
+    title: {
+        overflow: 'hidden',
+        paddingTop: theme.spacing.unit * 0.5,
+        color: theme.customs.colors.green700,
+    },
+    avatar: {
+        alignSelf: 'flex-start',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+    content: {
+        padding: theme.spacing.unit * 1.0,
+        paddingTop: theme.spacing.unit * 0.5,
+        '&:last-child': {
+            paddingBottom: theme.spacing.unit * 1,
+        }
+    }
+});
+
+type RegisteredWorkflowPanelProps = RegisteredWorkflowPanelDataProps & DispatchProp & WithStyles<CssRules>
+
+export const RegisteredWorkflowPanel = withStyles(styles)(connect(
+    (state: RootState, props: RouteComponentProps<{ id: string }>) => {
+        const item = getResource<WorkflowResource>(props.match.params.id)(state.resources);
+        if (item) {
+            return getRegisteredWorkflowPanelData(item, state.auth);
+        }
+        return { item, inputParams: [], outputParams: [], workflowCollection: "", gitprops: {} };
+    })(
+        class extends React.Component<RegisteredWorkflowPanelProps> {
+            render() {
+                const { classes, item, inputParams, outputParams, workflowCollection } = this.props;
+                const panelsData: MPVPanelState[] = [
+                    { name: "Details" },
+                    { name: "Inputs" },
+                    { name: "Outputs" },
+                    { name: "Files" },
+                ];
+                return item
+                    ? <MPVContainer className={classes.root} spacing={8} direction="column" justify-content="flex-start" wrap="nowrap" panelStates={panelsData}>
+                        <MPVPanelContent xs="auto" data-cy='registered-workflow-info-panel'>
+                            <Card className={classes.infoCard}>
+                                <CardHeader
+                                    className={classes.header}
+                                    classes={{
+                                        content: classes.title,
+                                        avatar: classes.avatar,
+                                    }}
+                                    avatar={<WorkflowIcon className={classes.iconHeader} />}
+                                    title={
+                                        <Tooltip title={item.name} placement="bottom-start">
+                                            <Typography noWrap variant='h6'>
+                                                {item.name}
+                                            </Typography>
+                                        </Tooltip>
+                                    }
+                                    subheader={
+                                        <Tooltip title={item.description || '(no-description)'} placement="bottom-start">
+                                            <Typography noWrap variant='body1' color='inherit'>
+                                                {item.description || '(no-description)'}
+                                            </Typography>
+                                        </Tooltip>}
+                                    action={
+                                        <Tooltip title="More options" disableFocusListener>
+                                            <IconButton
+                                                aria-label="More options"
+                                                onClick={event => this.handleContextMenu(event)}>
+                                                <MoreOptionsIcon />
+                                            </IconButton>
+                                        </Tooltip>}
+
+                                />
+
+                                <CardContent className={classes.content}>
+                                    <WorkflowDetailsAttributes workflow={item} />
+                                </CardContent>
+                            </Card>
+                        </MPVPanelContent>
+                        <MPVPanelContent forwardProps xs data-cy="process-inputs">
+                            <ProcessIOCard
+                                label={ProcessIOCardType.INPUT}
+                                params={inputParams}
+                                raw={{}}
+                                showParams={true}
+                            />
+                        </MPVPanelContent>
+                        <MPVPanelContent forwardProps xs data-cy="process-outputs">
+                            <ProcessIOCard
+                                label={ProcessIOCardType.OUTPUT}
+                                params={outputParams}
+                                raw={{}}
+                                showParams={true}
+                            />
+                        </MPVPanelContent>
+                        <MPVPanelContent xs>
+                            <Card className={classes.filesCard}>
+                                <ProcessOutputCollectionFiles isWritable={false} currentItemUuid={workflowCollection} />
+                            </Card>
+                        </MPVPanelContent>
+                    </MPVContainer>
+                    : null;
+            }
+
+            handleContextMenu = (event: React.MouseEvent<any>) => {
+                const { uuid, ownerUuid, name, description,
+                    kind } = this.props.item;
+                const menuKind = this.props.dispatch<any>(resourceUuidToContextMenuKind(uuid));
+                const resource = {
+                    uuid,
+                    ownerUuid,
+                    name,
+                    description,
+                    kind,
+                    menuKind,
+                };
+                // Avoid expanding/collapsing the panel
+                event.stopPropagation();
+                this.props.dispatch<any>(openContextMenu(event, resource));
+            }
+        }
+    )
+);