Merge branch '19294-sharing-dialog-overflow' into main. Closes #19294
authorStephen Smith <stephen@curii.com>
Fri, 10 Feb 2023 21:02:56 +0000 (16:02 -0500)
committerStephen Smith <stephen@curii.com>
Fri, 10 Feb 2023 21:02:56 +0000 (16:02 -0500)
Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen@curii.com>

31 files changed:
cypress/integration/group-manage.spec.js
cypress/integration/process.spec.js
src/common/config.ts
src/components/icon/icon.tsx
src/index.css
src/models/container-request.ts
src/services/common-service/common-service.ts
src/store/banner/banner-action.ts [new file with mode: 0644]
src/store/banner/banner-reducer.ts [new file with mode: 0644]
src/store/processes/process-copy-actions.test.ts [new file with mode: 0644]
src/store/processes/process-copy-actions.ts
src/store/processes/process.ts
src/store/processes/processes-actions.ts
src/store/search-results-panel/search-results-middleware-service.ts
src/store/store.ts
src/store/subprocess-panel/subprocess-panel-middleware-service.ts
src/store/workbench/workbench-actions.ts
src/views-components/baner/banner.test.tsx [new file with mode: 0644]
src/views-components/baner/banner.tsx [new file with mode: 0644]
src/views-components/context-menu/action-sets/process-resource-action-set.ts
src/views-components/dialog-copy/dialog-process-rerun.tsx [new file with mode: 0644]
src/views-components/dialog-forms/copy-process-dialog.ts
src/views-components/main-app-bar/notifications-menu.tsx
src/views-components/projects-tree-picker/tree-picker-field.tsx
src/views-components/webdav-s3-dialog/webdav-s3-dialog.test.tsx
src/views-components/webdav-s3-dialog/webdav-s3-dialog.tsx
src/views/process-panel/process-details-card.tsx
src/views/process-panel/process-panel-root.tsx
src/views/process-panel/process-panel.tsx
src/views/workbench/workbench.test.tsx
src/views/workbench/workbench.tsx

index ffe2c8c4dfd27ac892007213c2209b0afff4686c..1fd9e4165f502eb51fb06b8bf94f4c0071ae2eb4 100644 (file)
@@ -70,6 +70,13 @@ describe('Group manage tests', function() {
                 cy.get('[data-cy=invite-people-field] input').type("other");
             });
         cy.get('[role=tooltip]').click();
+        // Add admin to the group
+        cy.get('.sharing-dialog')
+            .should('contain', 'Sharing settings')
+            .within(() => {
+                cy.get('[data-cy=invite-people-field] input').type("admin");
+            });
+        cy.get('[role=tooltip]').click();
         cy.get('.sharing-dialog').contains('Save').click();
         cy.get('.sharing-dialog').contains('Close').click();
 
@@ -109,6 +116,27 @@ describe('Group manage tests', function() {
             .within(() => {
                 cy.contains('Write');
             });
+
+        // Change admin to manage
+        cy.get('[data-cy=group-members-data-explorer]')
+            .contains(adminUser.user.full_name)
+            .parents('tr')
+            .within(() => {
+                cy.contains('Read')
+                    .parents('td')
+                    .within(() => {
+                        cy.get('button').click();
+                    });
+            });
+        cy.get('[data-cy=context-menu]')
+            .contains('Manage')
+            .click();
+        cy.get('[data-cy=group-members-data-explorer]')
+            .contains(adminUser.user.full_name)
+            .parents('tr')
+            .within(() => {
+                cy.contains('Manage');
+            });
     });
 
     it('can unhide and re-hide users', function() {
@@ -212,6 +240,7 @@ describe('Group manage tests', function() {
     });
 
     it('renames the group', function() {
+        cy.loginAs(adminUser);
         // Navigate to Groups
         cy.get('[data-cy=side-panel-tree]').contains('Groups').click();
 
index f4461b75d1231f6f557e6dbc6827a95ac052f201..02657b3959971583f4d5aa3ad2580903ca860fc9 100644 (file)
@@ -1144,4 +1144,33 @@ describe('Process tests', function() {
         });
     });
 
+
+    it('allows copying processes', function() {
+        const crName = 'first_container_request';
+        const copiedCrName = 'copied_container_request';
+        createContainerRequest(
+            activeUser,
+            crName,
+            'arvados/jobs',
+            ['echo', 'hello world'],
+            false, 'Committed')
+        .then(function(containerRequest) {
+            cy.loginAs(activeUser);
+            cy.goToPath(`/processes/${containerRequest.uuid}`);
+            cy.get('[data-cy=process-details]').should('contain', crName);
+
+            cy.get('[data-cy=process-details]').find('button[title="More options"]').click();
+            cy.get('ul[data-cy=context-menu]').contains("Copy and re-run process").click();
+        });
+
+        cy.get('[data-cy=form-dialog]').within(() => {
+            cy.get('input[name=name]').clear().type(copiedCrName);
+            cy.get('[data-cy=projects-tree-home-tree-picker]').click();
+            cy.get('[data-cy=form-submit-btn]').click();
+        });
+
+        cy.get('[data-cy=process-details]').should('contain', copiedCrName);
+        cy.get('[data-cy=process-details]').find('button').contains('Run Process');
+    });
+
 });
index 9319736784b81676c6c15fe931e5136b806d985a..a6eabdadf9b8ae409f3fa234be59a9d1ce719f82 100644 (file)
@@ -41,6 +41,17 @@ export interface ClusterConfigJSON {
     Workbench2: {
       ExternalURL: string;
     };
+    Workbench: {
+        DisableSharingURLsUI: boolean;
+        ArvadosDocsite: string;
+        FileViewersConfigURL: string;
+        WelcomePageHTML: string;
+        InactivePageHTML: string;
+        SSHHelpPageHTML: string;
+        SSHHelpHostSuffix: string;
+        SiteName: string;
+        IdleTimeout: string;
+    };
     Websocket: {
       ExternalURL: string;
     };
@@ -64,6 +75,7 @@ export interface ClusterConfigJSON {
     SSHHelpHostSuffix: string;
     SiteName: string;
     IdleTimeout: string;
+    BannerUUID: string;
   };
   Login: {
     LoginCluster: string;
@@ -268,6 +280,17 @@ export const mockClusterConfigJSON = (
     WebDAV: { ExternalURL: '' },
     WebDAVDownload: { ExternalURL: '' },
     WebShell: { ExternalURL: '' },
+    Workbench: {
+        DisableSharingURLsUI: false,
+        ArvadosDocsite: "",
+        FileViewersConfigURL: "",
+        WelcomePageHTML: "",
+        InactivePageHTML: "",
+        SSHHelpPageHTML: "",
+        SSHHelpHostSuffix: "",
+        SiteName: "",
+        IdleTimeout: "0s"
+    }
   },
   Workbench: {
     DisableSharingURLsUI: false,
@@ -279,6 +302,7 @@ export const mockClusterConfigJSON = (
     SSHHelpHostSuffix: '',
     SiteName: '',
     IdleTimeout: '0s',
+    BannerUUID: ""
   },
   Login: {
     LoginCluster: '',
index db220a36e9506fcd3b576314ad811254cdbd397e..48cd8bed4f4c48ddb802e3c3b41eaab4616a51af 100644 (file)
@@ -33,7 +33,7 @@ import Help from '@material-ui/icons/Help';
 import HelpOutline from '@material-ui/icons/HelpOutline';
 import History from '@material-ui/icons/History';
 import Inbox from '@material-ui/icons/Inbox';
-import Memory from '@material-ui/icons/Memory'; 
+import Memory from '@material-ui/icons/Memory';
 import MoveToInbox from '@material-ui/icons/MoveToInbox';
 import Info from '@material-ui/icons/Info';
 import Input from '@material-ui/icons/Input';
@@ -166,7 +166,7 @@ export const KeyIcon: IconType = (props) => <VpnKey {...props} />;
 export const LogIcon: IconType = (props) => <SettingsEthernet {...props} />;
 export const MailIcon: IconType = (props) => <Mail {...props} />;
 export const MaximizeIcon: IconType = (props) => <FullscreenSharp {...props} />;
-export const MemoryIcon: IconType = (props) => <Memory {...props} />; 
+export const MemoryIcon: IconType = (props) => <Memory {...props} />;
 export const UnMaximizeIcon: IconType = (props) => <FullscreenExitSharp {...props} />;
 export const MoreOptionsIcon: IconType = (props) => <MoreVert {...props} />;
 export const MoveToIcon: IconType = (props) => <Input {...props} />;
@@ -214,3 +214,4 @@ export const ActiveIcon: IconType = (props) => <CheckCircleOutline {...props} />
 export const SetupIcon: IconType = (props) => <RemoveCircleOutline {...props} />;
 export const InactiveIcon: IconType = (props) => <NotInterested {...props} />;
 export const ImageIcon: IconType = (props) => <Image {...props} />;
+export const StartIcon: IconType = (props) => <PlayArrow {...props} />;
index 0172d68bcc5c9385ebdbf29d7a06a7022ef08c96..51f07761d0d9b4ab4e990886138eec7d98ba08ab 100644 (file)
@@ -5,3 +5,25 @@ body {
     width: 100vw;
     height: 100vh;
 }
+
+.app-banner {
+    width: calc(100% - 2rem);
+    height: 150px;
+    z-index: 11111;
+    position: fixed;
+    top: 0px;
+    background-color: #00bfa5;
+    border: 1px solid #01685a;
+    color: #ffffff;
+    margin: 1rem;
+    box-sizing: border-box;
+    cursor: pointer;
+}
+
+.app-banner span {
+    font-size: 2rem;
+    text-align: center;
+    display: block;
+    margin: auto;
+    padding: 2rem;
+}
\ No newline at end of file
index e6e12da841ce06178df785bfce0e7b5a7db029a1..aa5e0f799934a935c2266c00f9882cfa4a3b5cad 100644 (file)
@@ -33,6 +33,8 @@ export interface ContainerRequestResource
   name: string;
   outputName: string;
   outputPath: string;
+  outputProperties: any;
+  outputStorageClasses: string[];
   outputTtl: number;
   outputUuid: string | null;
   priority: number | null;
index 9a5b767306023ce21673d26427a23d721bd785b6..4b857eddbb8233239d2d4d152928f0a93c86e9e1 100644 (file)
@@ -154,6 +154,7 @@ export class CommonService<T> {
             return CommonService.defaultResponse(
                 this.serverApi.get(`/${this.resourceType}`, { params }),
                 this.actions,
+                true,
                 showErrors
             );
         } else {
@@ -170,6 +171,7 @@ export class CommonService<T> {
             return CommonService.defaultResponse(
                 this.serverApi.post(`/${this.resourceType}`, formData, {}),
                 this.actions,
+                true,
                 showErrors
             );
         }
diff --git a/src/store/banner/banner-action.ts b/src/store/banner/banner-action.ts
new file mode 100644 (file)
index 0000000..808ca82
--- /dev/null
@@ -0,0 +1,29 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from "redux";
+import { RootState } from "store/store";
+import { unionize, UnionOf } from 'common/unionize';
+
+export const bannerReducerActions = unionize({
+    OPEN_BANNER: {},
+    CLOSE_BANNER: {},
+});
+
+export type BannerAction = UnionOf<typeof bannerReducerActions>;
+
+export const openBanner = () =>
+    async (dispatch: Dispatch, getState: () => RootState) => {
+        dispatch(bannerReducerActions.OPEN_BANNER());
+    };
+
+export const closeBanner = () =>
+    async (dispatch: Dispatch<any>, getState: () => RootState) => {
+        dispatch(bannerReducerActions.CLOSE_BANNER());
+    };
+
+export default {
+    openBanner,
+    closeBanner
+};
diff --git a/src/store/banner/banner-reducer.ts b/src/store/banner/banner-reducer.ts
new file mode 100644 (file)
index 0000000..8009f4b
--- /dev/null
@@ -0,0 +1,26 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { BannerAction, bannerReducerActions } from "./banner-action";
+
+export interface BannerState {
+    isOpen: boolean;
+}
+
+const initialState = {
+    isOpen: false,
+};
+
+export const bannerReducer = (state: BannerState = initialState, action: BannerAction) =>
+    bannerReducerActions.match(action, {
+        default: () => state,
+        OPEN_BANNER: () => ({
+             ...state,
+             isOpen: true,
+        }),
+        CLOSE_BANNER: () => ({
+            ...state,
+            isOpen: false,
+       }),
+    });
diff --git a/src/store/processes/process-copy-actions.test.ts b/src/store/processes/process-copy-actions.test.ts
new file mode 100644 (file)
index 0000000..cb064ed
--- /dev/null
@@ -0,0 +1,483 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { configure } from 'enzyme';
+import Adapter from 'enzyme-adapter-react-16';
+import { copyProcess } from './process-copy-actions';
+import { CommonService } from 'services/common-service/common-service';
+import { snakeCase } from 'lodash';
+
+configure({ adapter: new Adapter() });
+
+describe('ProcessCopyAction', () => {
+    // let props;
+    let dispatch: any, getState: any, services: any;
+
+    let sampleFailedProcess = {
+        command: [
+        "arvados-cwl-runner",
+        "--api=containers",
+        "--local",
+        "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
+        "/var/lib/cwl/workflow.json#main",
+        "/var/lib/cwl/cwl.input.json",
+        ],
+        container_count: 1,
+        container_count_max: 10,
+        container_image: "arvados/jobs",
+        container_uuid: "zzzzz-dz642-b9j9dtk1yikp9h0",
+        created_at: "2023-01-23T22:50:50.788284000Z",
+        cumulative_cost: 0.00120553009559028,
+        cwd: "/var/spool/cwl",
+        description: "test decsription",
+        environment: {},
+        etag: "2es6px6q7uo0yqi2i291x8gd6",
+        expires_at: null,
+        filters: null,
+        href: "/container_requests/zzzzz-xvhdp-111111111111111",
+        kind: "arvados#containerRequest",
+        log_uuid: "zzzzz-4zz18-a1gxqy9o6zyrdy8",
+        modified_at: "2023-01-24T21:13:54.772612000Z",
+        modified_by_client_uuid: "zzzzz-ozdt8-q6dzdi1lcc03155",
+        modified_by_user_uuid: "jutro-tpzed-vllbpebicy84rd5",
+        mounts: {
+        "/var/lib/cwl/cwl.input.json": {
+            capacity: 0,
+            commit: "",
+            content: {
+            input: {
+                basename: "logo.ai.no.whitespace.png",
+                class: "File",
+                location:
+                "keep:5d3238c4db721a92c98b0305a47b0485+75/logo.ai.no.whitespace.png",
+            },
+            reverse_sort: true,
+            },
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "json",
+            path: "",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: false,
+        },
+        "/var/lib/cwl/workflow.json": {
+            capacity: 0,
+            commit: "",
+            content: {
+            $graph: [
+                {
+                class: "Workflow",
+                doc: "Reverse the lines in a document, then sort those lines.",
+                id: "#main",
+                inputs: [
+                    {
+                    default: null,
+                    doc: "The input file to be processed.",
+                    id: "#main/input",
+                    type: "File",
+                    },
+                    {
+                    default: true,
+                    doc: "If true, reverse (decending) sort",
+                    id: "#main/reverse_sort",
+                    type: "boolean",
+                    },
+                ],
+                outputs: [
+                    {
+                    doc: "The output with the lines reversed and sorted.",
+                    id: "#main/output",
+                    outputSource: "#main/sorted/output",
+                    type: "File",
+                    },
+                ],
+                steps: [
+                    {
+                    id: "#main/rev",
+                    in: [{ id: "#main/rev/input", source: "#main/input" }],
+                    out: ["#main/rev/output"],
+                    run: "#revtool.cwl",
+                    },
+                    {
+                    id: "#main/sorted",
+                    in: [
+                        { id: "#main/sorted/input", source: "#main/rev/output" },
+                        {
+                        id: "#main/sorted/reverse",
+                        source: "#main/reverse_sort",
+                        },
+                    ],
+                    out: ["#main/sorted/output"],
+                    run: "#sorttool.cwl",
+                    },
+                ],
+                },
+                {
+                baseCommand: "rev",
+                class: "CommandLineTool",
+                doc: "Reverse each line using the `rev` command",
+                hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+                id: "#revtool.cwl",
+                inputs: [
+                    { id: "#revtool.cwl/input", inputBinding: {}, type: "File" },
+                ],
+                outputs: [
+                    {
+                    id: "#revtool.cwl/output",
+                    outputBinding: { glob: "output.txt" },
+                    type: "File",
+                    },
+                ],
+                stdout: "output.txt",
+                },
+                {
+                baseCommand: "sort",
+                class: "CommandLineTool",
+                doc: "Sort lines using the `sort` command",
+                hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+                id: "#sorttool.cwl",
+                inputs: [
+                    {
+                    id: "#sorttool.cwl/reverse",
+                    inputBinding: { position: 1, prefix: "-r" },
+                    type: "boolean",
+                    },
+                    {
+                    id: "#sorttool.cwl/input",
+                    inputBinding: { position: 2 },
+                    type: "File",
+                    },
+                ],
+                outputs: [
+                    {
+                    id: "#sorttool.cwl/output",
+                    outputBinding: { glob: "output.txt" },
+                    type: "File",
+                    },
+                ],
+                stdout: "output.txt",
+                },
+            ],
+            cwlVersion: "v1.0",
+            },
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "json",
+            path: "",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: false,
+        },
+        "/var/spool/cwl": {
+            capacity: 0,
+            commit: "",
+            content: null,
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "collection",
+            path: "",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: true,
+        },
+        stdout: {
+            capacity: 0,
+            commit: "",
+            content: null,
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "file",
+            path: "/var/spool/cwl/cwl.output.json",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: false,
+        },
+        },
+        name: "Copy of: Copy of: Copy of: revsort.cwl",
+        output_name: "Output from revsort.cwl",
+        output_path: "/var/spool/cwl",
+        output_properties: { key: "val" },
+        output_storage_classes: ["default"],
+        output_ttl: 999999,
+        output_uuid: "zzzzz-4zz18-wolwlyfxmlhmgd4",
+        owner_uuid: "zzzzz-j7d0g-yr18k784zplfeza",
+        priority: 500,
+        properties: {
+        template_uuid: "zzzzz-7fd4e-7xsza0vgfe785cy",
+        workflowName: "revsort.cwl",
+        },
+        requesting_container_uuid: null,
+        runtime_constraints: {
+        API: true,
+        cuda: { device_count: 0, driver_version: "", hardware_capability: "" },
+        keep_cache_disk: 0,
+        keep_cache_ram: 0,
+        ram: 1342177280,
+        vcpus: 1,
+        },
+        runtime_token: "",
+        scheduling_parameters: {
+        max_run_time: 0,
+        partitions: [],
+        preemptible: false,
+        },
+        state: "Final",
+        use_existing: false,
+        uuid: "zzzzz-xvhdp-111111111111111",
+    };
+
+    let expectedContainerRequest = {
+        command: [
+        "arvados-cwl-runner",
+        "--api=containers",
+        "--local",
+        "--project-uuid=zzzzz-j7d0g-yr18k784zplfeza",
+        "/var/lib/cwl/workflow.json#main",
+        "/var/lib/cwl/cwl.input.json",
+        ],
+        container_count_max: 10,
+        container_image: "arvados/jobs",
+        cwd: "/var/spool/cwl",
+        description: "test decsription",
+        environment: {},
+        kind: "arvados#containerRequest",
+        mounts: {
+        "/var/lib/cwl/cwl.input.json": {
+            capacity: 0,
+            commit: "",
+            content: {
+            input: {
+                basename: "logo.ai.no.whitespace.png",
+                class: "File",
+                location:
+                "keep:5d3238c4db721a92c98b0305a47b0485+75/logo.ai.no.whitespace.png",
+            },
+            reverse_sort: true,
+            },
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "json",
+            path: "",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: false,
+        },
+        "/var/lib/cwl/workflow.json": {
+            capacity: 0,
+            commit: "",
+            content: {
+            $graph: [
+                {
+                class: "Workflow",
+                doc: "Reverse the lines in a document, then sort those lines.",
+                id: "#main",
+                inputs: [
+                    {
+                    default: null,
+                    doc: "The input file to be processed.",
+                    id: "#main/input",
+                    type: "File",
+                    },
+                    {
+                    default: true,
+                    doc: "If true, reverse (decending) sort",
+                    id: "#main/reverse_sort",
+                    type: "boolean",
+                    },
+                ],
+                outputs: [
+                    {
+                    doc: "The output with the lines reversed and sorted.",
+                    id: "#main/output",
+                    outputSource: "#main/sorted/output",
+                    type: "File",
+                    },
+                ],
+                steps: [
+                    {
+                    id: "#main/rev",
+                    in: [{ id: "#main/rev/input", source: "#main/input" }],
+                    out: ["#main/rev/output"],
+                    run: "#revtool.cwl",
+                    },
+                    {
+                    id: "#main/sorted",
+                    in: [
+                        {
+                        id: "#main/sorted/input",
+                        source: "#main/rev/output",
+                        },
+                        {
+                        id: "#main/sorted/reverse",
+                        source: "#main/reverse_sort",
+                        },
+                    ],
+                    out: ["#main/sorted/output"],
+                    run: "#sorttool.cwl",
+                    },
+                ],
+                },
+                {
+                baseCommand: "rev",
+                class: "CommandLineTool",
+                doc: "Reverse each line using the `rev` command",
+                hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+                id: "#revtool.cwl",
+                inputs: [
+                    {
+                    id: "#revtool.cwl/input",
+                    inputBinding: {},
+                    type: "File",
+                    },
+                ],
+                outputs: [
+                    {
+                    id: "#revtool.cwl/output",
+                    outputBinding: { glob: "output.txt" },
+                    type: "File",
+                    },
+                ],
+                stdout: "output.txt",
+                },
+                {
+                baseCommand: "sort",
+                class: "CommandLineTool",
+                doc: "Sort lines using the `sort` command",
+                hints: [{ class: "ResourceRequirement", ramMin: 8 }],
+                id: "#sorttool.cwl",
+                inputs: [
+                    {
+                    id: "#sorttool.cwl/reverse",
+                    inputBinding: { position: 1, prefix: "-r" },
+                    type: "boolean",
+                    },
+                    {
+                    id: "#sorttool.cwl/input",
+                    inputBinding: { position: 2 },
+                    type: "File",
+                    },
+                ],
+                outputs: [
+                    {
+                    id: "#sorttool.cwl/output",
+                    outputBinding: { glob: "output.txt" },
+                    type: "File",
+                    },
+                ],
+                stdout: "output.txt",
+                },
+            ],
+            cwlVersion: "v1.0",
+            },
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "json",
+            path: "",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: false,
+        },
+        "/var/spool/cwl": {
+            capacity: 0,
+            commit: "",
+            content: null,
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "collection",
+            path: "",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: true,
+        },
+        stdout: {
+            capacity: 0,
+            commit: "",
+            content: null,
+            device_type: "",
+            exclude_from_output: false,
+            git_url: "",
+            kind: "file",
+            path: "/var/spool/cwl/cwl.output.json",
+            portable_data_hash: "",
+            repository_name: "",
+            uuid: "",
+            writable: false,
+        },
+        },
+        name: "newname.cwl",
+        output_name: "Output from revsort.cwl",
+        output_path: "/var/spool/cwl",
+        output_properties: { key: "val" },
+        output_storage_classes: ["default"],
+        output_ttl: 999999,
+        owner_uuid: "zzzzz-j7d0g-000000000000000",
+        priority: 500,
+        properties: {
+        template_uuid: "zzzzz-7fd4e-7xsza0vgfe785cy",
+        workflowName: "revsort.cwl",
+        },
+        runtime_constraints: {
+        API: true,
+        cuda: {
+            device_count: 0,
+            driver_version: "",
+            hardware_capability: "",
+        },
+        keep_cache_disk: 0,
+        keep_cache_ram: 0,
+        ram: 1342177280,
+        vcpus: 1,
+        },
+        scheduling_parameters: {
+        max_run_time: 0,
+        partitions: [],
+        preemptible: false,
+        },
+        state: "Uncommitted",
+        use_existing: false,
+    };
+
+    beforeEach(() => {
+        dispatch = jest.fn();
+        services = {
+            containerRequestService: {
+                get: jest.fn().mockImplementation(async () => (CommonService.mapResponseKeys({data: sampleFailedProcess}))),
+                create: jest.fn().mockImplementation(async (data) => (CommonService.mapKeys(snakeCase)(data))),
+            },
+        };
+        getState = () => ({
+            auth: {},
+        });
+    });
+
+    it("should request the failed process and return a copy with the proper fields", async () => {
+        // when
+        const newprocess = await copyProcess({
+            name: "newname.cwl",
+            uuid: "zzzzz-xvhdp-111111111111111",
+            ownerUuid: "zzzzz-j7d0g-000000000000000",
+        })(dispatch, getState, services);
+
+        // then
+        expect(services.containerRequestService.get).toHaveBeenCalledWith("zzzzz-xvhdp-111111111111111");
+        expect(newprocess).toEqual(expectedContainerRequest);
+
+    });
+});
index 57e8539778bd8d6dbbbfe6651a65eb5fe6e55bb6..3c55a9adddb2946f908d8ac43e4594779956aba1 100644 (file)
@@ -12,6 +12,7 @@ import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
 import { getProcess } from 'store/processes/process';
 import {snackbarActions, SnackbarKind} from 'store/snackbar/snackbar-actions';
 import { initProjectsTreePicker } from 'store/tree-picker/tree-picker-actions';
+import { ContainerRequestState } from "models/container-request";
 
 export const PROCESS_COPY_FORM_NAME = 'processCopyFormName';
 
@@ -34,12 +35,52 @@ export const copyProcess = (resource: CopyFormDialogData) =>
         dispatch(startSubmit(PROCESS_COPY_FORM_NAME));
         try {
             const process = await services.containerRequestService.get(resource.uuid);
-            const { kind, containerImage, outputPath, outputName, containerCountMax, command, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters } = process;
-            await services.containerRequestService.create({ command, containerImage, outputPath, ownerUuid: resource.ownerUuid, name: resource.name, kind, outputName, containerCountMax, properties, requestingContainerUuid, mounts, runtimeConstraints, schedulingParameters, environment, cwd, outputTtl, priority, expiresAt, useExisting, filters });
+            const {
+                command,
+                containerCountMax,
+                containerImage,
+                cwd,
+                description,
+                environment,
+                kind,
+                mounts,
+                outputName,
+                outputPath,
+                outputProperties,
+                outputStorageClasses,
+                outputTtl,
+                properties,
+                runtimeConstraints,
+                schedulingParameters,
+                useExisting,
+            } = process;
+            const newProcess = await services.containerRequestService.create({
+                command,
+                containerCountMax,
+                containerImage,
+                cwd,
+                description,
+                environment,
+                kind,
+                mounts,
+                name: resource.name,
+                outputName,
+                outputPath,
+                outputProperties,
+                outputStorageClasses,
+                outputTtl,
+                ownerUuid: resource.ownerUuid,
+                priority: 500,
+                properties,
+                runtimeConstraints,
+                schedulingParameters,
+                state: ContainerRequestState.UNCOMMITTED,
+                useExisting,
+            });
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
-            return process;
+            return newProcess;
         } catch (e) {
             dispatch(dialogActions.CLOSE_DIALOG({ id: PROCESS_COPY_FORM_NAME }));
             throw new Error('Could not copy the process.');
         }
-    };
\ No newline at end of file
+    };
index 2a81669cc7ad55772b9695c4e20d77cfca130412..d7fd3aa2320c3d3a3367de2c1a9dacdea337af18 100644 (file)
@@ -119,6 +119,9 @@ export const getProcessStatusStyles = (status: string, theme: ArvadosTheme): Rea
 
 export const getProcessStatus = ({ containerRequest, container }: Process): ProcessStatus => {
     switch (true) {
+        case containerRequest.containerUuid && !container:
+            return ProcessStatus.UNKNOWN;
+
         case containerRequest.state === ContainerRequestState.FINAL &&
             container?.state !== ContainerState.COMPLETE:
             // Request was finalized before its container started (or the
index e4f35c1e4443741c8acf484c243a45e4867f5720..815d6aec5e37eb443f3f636a58c63d990481684d 100644 (file)
@@ -21,24 +21,40 @@ import { CommandInputParameter, getWorkflow, getWorkflowInputs, getWorkflowOutpu
 import { ProjectResource } from "models/project";
 import { UserResource } from "models/user";
 import { CommandOutputParameter } from "cwlts/mappings/v1.0/CommandOutputParameter";
+import { ContainerResource } from "models/container";
+import { ContainerRequestResource, ContainerRequestState } from "models/container-request";
 
 export const loadProcess = (containerRequestUuid: string) =>
-    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process> => {
-        const containerRequest = await services.containerRequestService.get(containerRequestUuid);
-        dispatch<any>(updateResources([containerRequest]));
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository): Promise<Process | undefined> => {
+        let containerRequest: ContainerRequestResource | undefined = undefined;
+        try {
+            containerRequest = await services.containerRequestService.get(containerRequestUuid);
+            dispatch<any>(updateResources([containerRequest]));
+        } catch {
+            return undefined;
+        }
 
         if (containerRequest.outputUuid) {
-            const collection = await services.collectionService.get(containerRequest.outputUuid);
-            dispatch<any>(updateResources([collection]));
+            try {
+                const collection = await services.collectionService.get(containerRequest.outputUuid, false);
+                dispatch<any>(updateResources([collection]));
+            } catch {}
         }
 
         if (containerRequest.containerUuid) {
-            const container = await services.containerService.get(containerRequest.containerUuid);
-            dispatch<any>(updateResources([container]));
-            if (container.runtimeUserUuid) {
-                const runtimeUser = await services.userService.get(container.runtimeUserUuid);
-                dispatch<any>(updateResources([runtimeUser]));
-            }
+            let container: ContainerResource | undefined = undefined;
+            try {
+                container = await services.containerService.get(containerRequest.containerUuid, false);
+                dispatch<any>(updateResources([container]));
+            } catch {}
+
+            try{
+                if (container && container.runtimeUserUuid) {
+                    const runtimeUser = await services.userService.get(container.runtimeUserUuid, false);
+                    dispatch<any>(updateResources([runtimeUser]));
+                }
+            } catch {}
+
             return { containerRequest, container };
         }
         return { containerRequest };
@@ -104,6 +120,21 @@ export const cancelRunningWorkflow = (uuid: string) =>
         }
     };
 
+export const startWorkflow = (uuid: string) =>
+    async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
+        try {
+            const process = await services.containerRequestService.update(uuid, { state: ContainerRequestState.COMMITTED });
+            if (process) {
+                dispatch<any>(updateResources([process]));
+                dispatch(snackbarActions.OPEN_SNACKBAR({ message: 'Process started', hideDuration: 2000, kind: SnackbarKind.SUCCESS }));
+            } else {
+                dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
+            }
+        } catch (e) {
+            dispatch<any>(snackbarActions.OPEN_SNACKBAR({ message: `Failed to start process`, kind: SnackbarKind.ERROR }));
+        }
+    };
+
 export const reRunProcess = (processUuid: string, workflowUuid: string) =>
     (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         const process = getResource<any>(processUuid)(getState().resources);
index 78ba6c38bf130b2b34ab067d80e893f6edbb4135..4035e148a403432c8b6dcb5aed526dd1fa9ecb43 100644 (file)
@@ -20,11 +20,12 @@ import {
     getAdvancedDataFromQuery
 } from 'store/search-bar/search-bar-actions';
 import { getSortColumn } from "store/data-explorer/data-explorer-reducer";
-import { joinFilters } from 'services/api/filter-builder';
+import { FilterBuilder, joinFilters } from 'services/api/filter-builder';
 import { DataColumns } from 'components/data-table/data-table';
 import { serializeResourceTypeFilters } from 'store//resource-type-filters/resource-type-filters';
 import { ProjectPanelColumnNames } from 'views/project-panel/project-panel';
-import { Resource } from 'models/resource';
+import { Resource, ResourceKind } from 'models/resource';
+import { ContainerRequestResource } from 'models/container-request';
 
 export class SearchResultsMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -60,6 +61,18 @@ export class SearchResultsMiddlewareService extends DataExplorerMiddlewareServic
                 .then((response) => {
                     api.dispatch(updateResources(response.items));
                     api.dispatch(appendItems(response));
+                    // Request all containers for process status to be available
+                    const containerRequests = response.items.filter((item) => item.kind === ResourceKind.CONTAINER_REQUEST) as ContainerRequestResource[];
+                    const containerUuids = containerRequests.map(container => container.containerUuid).filter(uuid => uuid !== null) as string[];
+                    containerUuids.length && this.services.containerService
+                        .list({
+                            filters: new FilterBuilder()
+                                .addIn('uuid', containerUuids)
+                                .getFilters()
+                        }, false)
+                        .then((containers) => {
+                            api.dispatch(updateResources(containers.items));
+                        });
                 }).catch(() => {
                     api.dispatch(couldNotFetchSearchResults(session.clusterId));
                 });
index 2057939e6812e79c1dd27b021591e645f89ff91e..4ef5e3d0e5c45d4bb29bcc8f701f701dd65d7af7 100644 (file)
@@ -75,6 +75,7 @@ import { Config } from 'common/config';
 import { pluginConfig } from 'plugins';
 import { MiddlewareListReducer } from 'common/plugintypes';
 import { sidePanelReducer } from './side-panel/side-panel-reducer'
+import { bannerReducer } from './banner/banner-reducer';
 
 declare global {
     interface Window {
@@ -190,6 +191,7 @@ export function configureStore(history: History, services: ServiceRepository, co
 
 const createRootReducer = (services: ServiceRepository) => combineReducers({
     auth: authReducer(services),
+    banner: bannerReducer,
     collectionPanel: collectionPanelReducer,
     collectionPanelFiles: collectionPanelFilesReducer,
     contextMenu: contextMenuReducer,
index 0f306ac076a475c525cec24509df5c3da507dafb..c622362788dc53943a29a9f9d5376bf63db66926 100644 (file)
@@ -39,17 +39,18 @@ export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
         try {
             api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
             const parentContainerRequest = await this.services.containerRequestService.get(parentContainerRequestUuid);
-            const containerRequests = await this.services.containerRequestService.list(
-                {
-                    ...getParams(dataExplorer, parentContainerRequest) ,
-                    select: containerRequestFieldsNoMounts
-                });
-
+            if (parentContainerRequest.containerUuid) {
+                const containerRequests = await this.services.containerRequestService.list(
+                    {
+                        ...getParams(dataExplorer, parentContainerRequest) ,
+                        select: containerRequestFieldsNoMounts
+                    });
+                api.dispatch(updateResources(containerRequests.items));
+                await api.dispatch<any>(loadMissingProcessesInformation(containerRequests.items));
+                // Populate the actual user view
+                api.dispatch(setItems(containerRequests));
+            }
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
-            api.dispatch(updateResources(containerRequests.items));
-            await api.dispatch<any>(loadMissingProcessesInformation(containerRequests.items));
-            // Populate the actual user view
-            api.dispatch(setItems(containerRequests));
         } catch {
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
             api.dispatch(couldNotFetchSubprocesses());
index b2da727b6615820d63639a67be27dcada870dc68..1cf71706420fc6c7be736b9d8e4282cdf94ace47 100644 (file)
@@ -579,11 +579,13 @@ export const loadProcess = (uuid: string) =>
     handleFirstTimeLoad(async (dispatch: Dispatch, getState: () => RootState) => {
         dispatch<any>(loadProcessPanel(uuid));
         const process = await dispatch<any>(processesActions.loadProcess(uuid));
-        await dispatch<any>(
-            activateSidePanelTreeItem(process.containerRequest.ownerUuid)
-        );
-        dispatch<any>(setProcessBreadcrumbs(uuid));
-        dispatch<any>(loadDetailsPanel(uuid));
+        if (process) {
+            await dispatch<any>(
+                activateSidePanelTreeItem(process.containerRequest.ownerUuid)
+            );
+            dispatch<any>(setProcessBreadcrumbs(uuid));
+            dispatch<any>(loadDetailsPanel(uuid));
+        }
     });
 
 export const updateProcess =
@@ -662,6 +664,7 @@ export const copyProcess =
                         kind: SnackbarKind.SUCCESS,
                     })
                 );
+                dispatch<any>(navigateTo(process.uuid));
             } catch (e) {
                 dispatch(
                     snackbarActions.OPEN_SNACKBAR({
diff --git a/src/views-components/baner/banner.test.tsx b/src/views-components/baner/banner.test.tsx
new file mode 100644 (file)
index 0000000..1e82008
--- /dev/null
@@ -0,0 +1,63 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import { configure, shallow, mount } from "enzyme";
+import { BannerComponent } from './banner';
+import { Button } from "@material-ui/core";
+import Adapter from "enzyme-adapter-react-16";
+import servicesProvider from '../../common/service-provider';
+
+configure({ adapter: new Adapter() });
+
+jest.mock('../../common/service-provider', () => ({
+    getServices: jest.fn(),
+}));
+
+describe('<BannerComponent />', () => {
+
+    let props;
+
+    beforeEach(() => {
+        props = {
+            isOpen: false,
+            bannerUUID: undefined,
+            keepWebInlineServiceUrl: '',
+            openBanner: jest.fn(),
+            closeBanner: jest.fn(),
+            classes: {} as any,
+        }
+    });
+
+    it('renders without crashing', () => {
+        // when
+        const banner = shallow(<BannerComponent {...props} />);
+        
+        // then
+        expect(banner.find(Button)).toHaveLength(1);
+    });
+
+    it('calls collectionService', () => {
+        // given
+        props.isOpen = true;
+        props.bannerUUID = '123';
+        const mocks = {
+            collectionService: {
+                files: jest.fn(() => ({ then: (callback) => callback([{ name: 'banner.html' }]) })),
+                getFileContents: jest.fn(() => ({ then: (callback) => callback('<h1>Test</h1>') }))
+            }
+        };
+        (servicesProvider.getServices as any).mockImplementation(() => mocks);
+
+        // when
+        const banner = mount(<BannerComponent {...props} />);
+
+        // then
+        expect(servicesProvider.getServices).toHaveBeenCalled();
+        expect(mocks.collectionService.files).toHaveBeenCalled();
+        expect(mocks.collectionService.getFileContents).toHaveBeenCalled();
+        expect(banner.html()).toContain('<h1>Test</h1>');
+    });
+});
+
diff --git a/src/views-components/baner/banner.tsx b/src/views-components/baner/banner.tsx
new file mode 100644 (file)
index 0000000..9fae638
--- /dev/null
@@ -0,0 +1,111 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React, { useState, useCallback, useEffect } from 'react';
+import { Dialog, DialogContent, DialogActions, Button, StyleRulesCallback, withStyles, WithStyles } from "@material-ui/core";
+import { connect } from "react-redux";
+import { RootState } from "store/store";
+import bannerActions from "store/banner/banner-action";
+import { ArvadosTheme } from 'common/custom-theme';
+import servicesProvider from 'common/service-provider';
+import { Dispatch } from 'redux';
+
+type CssRules = 'dialogContent' | 'dialogContentIframe';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    dialogContent: {
+        minWidth: '550px',
+        minHeight: '500px',
+        display: 'block'
+    },
+    dialogContentIframe: {
+        minWidth: '550px',
+        minHeight: '500px'
+    }
+});
+
+interface BannerProps {
+    isOpen: boolean;
+    bannerUUID?: string;
+    keepWebInlineServiceUrl: string;
+};
+
+type BannerComponentProps = BannerProps & WithStyles<CssRules> & {
+    openBanner: Function,
+    closeBanner: Function,
+};
+
+const mapStateToProps = (state: RootState): BannerProps => ({
+    isOpen: state.banner.isOpen,
+    bannerUUID: state.auth.config.clusterConfig.Workbench.BannerUUID,
+    keepWebInlineServiceUrl: state.auth.config.keepWebInlineServiceUrl,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    openBanner: () => dispatch<any>(bannerActions.openBanner()),
+    closeBanner: () => dispatch<any>(bannerActions.closeBanner()),
+});
+
+export const BANNER_LOCAL_STORAGE_KEY = 'bannerFileData';
+
+export const BannerComponent = (props: BannerComponentProps) => {
+    const { 
+        isOpen,
+        openBanner,
+        closeBanner,
+        bannerUUID,
+        keepWebInlineServiceUrl
+    } = props;
+    const [bannerContents, setBannerContents] = useState(`<h1>Loading ...</h1>`)
+
+    const onConfirm = useCallback(() => {
+        closeBanner();
+    }, [closeBanner])
+
+    useEffect(() => {
+        if (!!bannerUUID && bannerUUID !== "") {
+            servicesProvider.getServices().collectionService.files(bannerUUID)
+                .then(results => {
+                    const bannerFileData = results.find(({name}) => name === 'banner.html');
+                    const result = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
+
+                    if (result && result === JSON.stringify(bannerFileData) && !isOpen) {
+                        return;
+                    }
+
+                    if (bannerFileData) {
+                        servicesProvider.getServices()
+                            .collectionService.getFileContents(bannerFileData)
+                            .then(data => {
+                                setBannerContents(data);
+                                openBanner();
+                                localStorage.setItem(BANNER_LOCAL_STORAGE_KEY, JSON.stringify(bannerFileData));
+                            });
+                    }
+                });
+        }
+    }, [bannerUUID, keepWebInlineServiceUrl, openBanner, isOpen]);
+
+    return (
+        <Dialog open={isOpen}>
+            <div data-cy='confirmation-dialog'>
+                <DialogContent className={props.classes.dialogContent}>
+                    <div dangerouslySetInnerHTML={{ __html: bannerContents }}></div>
+                </DialogContent>
+                <DialogActions style={{ margin: '0px 24px 24px' }}>
+                    <Button
+                        data-cy='confirmation-dialog-ok-btn'
+                        variant='contained'
+                        color='primary'
+                        type='submit'
+                        onClick={onConfirm}>
+                        Close
+                    </Button>
+                </DialogActions>
+            </div>
+        </Dialog>
+    );
+}
+
+export const Banner = withStyles(styles)(connect(mapStateToProps, mapDispatchToProps)(BannerComponent));
index 78b2f340b8d814efbb9800ab396c3858964313a5..7d593ee4b4f72978b1f5c7aa285d435c7df0cf6e 100644 (file)
@@ -6,7 +6,7 @@ import { ContextMenuActionSet } from "../context-menu-action-set";
 import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "store/favorites/favorites-actions";
 import {
-    RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
+    RenameIcon, ShareIcon, MoveToIcon, DetailsIcon,
     RemoveIcon, ReRunProcessIcon, OutputIcon,
     AdvancedIcon,
     OpenIcon
@@ -16,9 +16,8 @@ import { openMoveProcessDialog } from 'store/processes/process-move-actions';
 import { openProcessUpdateDialog } from "store/processes/process-update-actions";
 import { openCopyProcessDialog } from 'store/processes/process-copy-actions';
 import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
-import { openRemoveProcessDialog, reRunProcess } from "store/processes/processes-actions";
+import { openRemoveProcessDialog } from "store/processes/processes-actions";
 import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
-import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 import { navigateToOutput } from "store/process-panel/process-panel-actions";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
@@ -42,22 +41,11 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
             dispatch<any>(openInNewTabAction(resource));
         }
     },
-    {
-        icon: CopyIcon,
-        name: "Copy to project",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openCopyProcessDialog(resource));
-        }
-    },
     {
         icon: ReRunProcessIcon,
-        name: "Re-run process",
+        name: "Copy and re-run process",
         execute: (dispatch, resource) => {
-            if(resource.workflowUuid) {
-                dispatch<any>(reRunProcess(resource.uuid, resource.workflowUuid));
-            } else {
-                dispatch(snackbarActions.OPEN_SNACKBAR({ message: `You can't re-run this process`, hideDuration: 2000, kind: SnackbarKind.ERROR }));
-            }
+            dispatch<any>(openCopyProcessDialog(resource));
         }
     },
     {
diff --git a/src/views-components/dialog-copy/dialog-process-rerun.tsx b/src/views-components/dialog-copy/dialog-process-rerun.tsx
new file mode 100644 (file)
index 0000000..9f97b1a
--- /dev/null
@@ -0,0 +1,39 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from "react";
+import { memoize } from 'lodash/fp';
+import { InjectedFormProps, Field } from 'redux-form';
+import { WithDialogProps } from 'store/dialog/with-dialog';
+import { FormDialog } from 'components/form-dialog/form-dialog';
+import { ProjectTreePickerField } from 'views-components/projects-tree-picker/tree-picker-field';
+import { COPY_NAME_VALIDATION, COPY_FILE_VALIDATION } from 'validators/validators';
+import { TextField } from "components/text-field/text-field";
+import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
+import { PickerIdProp } from 'store/tree-picker/picker-id';
+
+type ProcessRerunFormDialogProps = WithDialogProps<string> & InjectedFormProps<CopyFormDialogData>;
+
+export const DialogProcessRerun = (props: ProcessRerunFormDialogProps & PickerIdProp) =>
+    <FormDialog
+        dialogTitle='Choose location for re-run'
+        formFields={CopyDialogFields(props.pickerId)}
+        submitLabel='Copy'
+        {...props}
+    />;
+
+const CopyDialogFields = memoize((pickerId: string) =>
+    () =>
+        <>
+            <Field
+                name='name'
+                component={TextField as any}
+                validate={COPY_NAME_VALIDATION}
+                label="Enter a new name for the copy" />
+            <Field
+                name="ownerUuid"
+                component={ProjectTreePickerField}
+                validate={COPY_FILE_VALIDATION}
+                pickerId={pickerId}/>
+        </>);
index c8f33642abe9bdb45d8b49818afc5c0294d1b1d6..6a79b62613d34b435a8c3a04f7bd3ad78e92602d 100644 (file)
@@ -6,7 +6,7 @@ import { compose } from "redux";
 import { withDialog } from "store/dialog/with-dialog";
 import { reduxForm } from 'redux-form';
 import { PROCESS_COPY_FORM_NAME } from 'store/processes/process-copy-actions';
-import { DialogCopy } from "views-components/dialog-copy/dialog-copy";
+import { DialogProcessRerun } from "views-components/dialog-copy/dialog-process-rerun";
 import { copyProcess } from 'store/workbench/workbench-actions';
 import { CopyFormDialogData } from 'store/copy-dialog/copy-dialog';
 import { pickerId } from "store/tree-picker/picker-id";
@@ -20,4 +20,4 @@ export const CopyProcessDialog = compose(
         }
     }),
     pickerId(PROCESS_COPY_FORM_NAME),
-)(DialogCopy);
\ No newline at end of file
+)(DialogProcessRerun);
index e27bdad552f7c51c34610c3e3bba38d2ed87a279..30a5756f2a00998466a1ecb975409cd9a0850497 100644 (file)
@@ -3,21 +3,59 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from "react";
-import { Badge, MenuItem } from '@material-ui/core';
+import { Dispatch } from "redux";
+import { connect } from "react-redux";
+import { Badge, MenuItem } from "@material-ui/core";
 import { DropdownMenu } from "components/dropdown-menu/dropdown-menu";
-import { NotificationIcon } from 'components/icon/icon';
-
-export const NotificationsMenu = 
-    () =>
-        <DropdownMenu
-            icon={
-                <Badge
-                    badgeContent={0}
-                    color="primary">
-                    <NotificationIcon />
-                </Badge>}
-            id="account-menu"
-            title="Notifications">
-            <MenuItem>You are up to date</MenuItem>
-        </DropdownMenu>;
+import { NotificationIcon } from "components/icon/icon";
+import bannerActions from "store/banner/banner-action";
+import { BANNER_LOCAL_STORAGE_KEY } from "views-components/baner/banner";
+import { RootState } from "store/store";
 
+const mapStateToProps = (state: RootState): NotificationsMenuProps => ({
+    isOpen: state.banner.isOpen,
+    bannerUUID: state.auth.config.clusterConfig.Workbench.BannerUUID,
+});
+
+const mapDispatchToProps = (dispatch: Dispatch) => ({
+    openBanner: () => dispatch<any>(bannerActions.openBanner()),
+});
+
+type NotificationsMenuProps = {
+    isOpen: boolean;
+    bannerUUID?: string;
+}
+
+type NotificationsMenuComponentProps = NotificationsMenuProps & {
+    openBanner: any;
+}
+
+export const NotificationsMenuComponent = (props: NotificationsMenuComponentProps) => {
+    const { isOpen, openBanner } = props;
+    const result = localStorage.getItem(BANNER_LOCAL_STORAGE_KEY);
+    const menuItems: any[] = [];
+
+    if (!isOpen && result) {
+        menuItems.push(<MenuItem><span onClick={openBanner}>Restore Banner</span></MenuItem>);
+    }
+
+    if (menuItems.length === 0) {
+        menuItems.push(<MenuItem>You are up to date</MenuItem>);
+    }
+
+    return (<DropdownMenu
+        icon={
+            <Badge
+                badgeContent={0}
+                color="primary">
+                <NotificationIcon />
+            </Badge>}
+        id="account-menu"
+        title="Notifications">
+        {
+            menuItems.map(item => item)
+        }
+    </DropdownMenu>);
+}
+
+export const NotificationsMenu = connect(mapStateToProps, mapDispatchToProps)(NotificationsMenuComponent);
index 357058c54bf1c1d6564044b8159b2f47d2cc3282..2afa606e363cba8a4adaaf2b118c581af2981719 100644 (file)
@@ -12,7 +12,7 @@ import { PickerIdProp } from 'store/tree-picker/picker-id';
 
 export const ProjectTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
     <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
-        <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+        <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
             <ProjectsTreePicker
                 pickerId={props.pickerId}
                 toggleItemActive={handleChange(props)}
@@ -30,7 +30,7 @@ const handleChange = (props: WrappedFieldProps) =>
 
 export const CollectionTreePickerField = (props: WrappedFieldProps & PickerIdProp) =>
     <div style={{ display: 'flex', minHeight: 0, flexDirection: 'column' }}>
-        <div style={{ flexBasis: '200px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
+        <div style={{ flexBasis: '275px', flexShrink: 1, minHeight: 0, display: 'flex', flexDirection: 'column' }}>
             <ProjectsTreePicker
                 pickerId={props.pickerId}
                 toggleItemActive={handleChange(props)}
index ec0a9c8a588d9ab971bc2a3b00564c85b647f4b0..d654f6edab41f524a2020079f6e6b7d8841b98e5 100644 (file)
@@ -64,7 +64,7 @@ describe('WebDavS3InfoDialog', () => {
         );
 
         // then
-        expect(wrapper.text()).toContain("davs://bobby@download.example.com/by_id/zzzzz-4zz18-b1f8tbldjrm8885");
+        expect(wrapper.text()).toContain("davs://bobby@download.example.com/c=zzzzz-4zz18-b1f8tbldjrm8885");
     });
 
     it('render win/mac tab', () => {
@@ -79,7 +79,7 @@ describe('WebDavS3InfoDialog', () => {
         );
 
         // then
-        expect(wrapper.text()).toContain("https://download.example.com/by_id/zzzzz-4zz18-b1f8tbldjrm8885");
+        expect(wrapper.text()).toContain("https://download.example.com/c=zzzzz-4zz18-b1f8tbldjrm8885");
     });
 
     it('render s3 tab with federated token', () => {
index 8e9edac11accccfe710877b44c30ab4400c87e45..a32044a711ef36a70820596ceb509d5a976665e9 100644 (file)
@@ -79,7 +79,7 @@ const mountainduckTemplate = ({
             <key>Port</key>
             <string>${(cyberDavStr.split(':')[2] || '443').split('/')[0]}</string>
             <key>Username</key>
-            <string>${username}</string>${isValidIpAddress(collectionsUrl.replace('https://', ``).split(':')[0])?
+            <string>${username}</string>${isValidIpAddress(collectionsUrl.replace('https://', ``).split(':')[0]) ?
             `
             <key>Path</key>
             <string>/c=${uuid}</string>` : ''}
@@ -120,8 +120,8 @@ export const WebDavS3InfoDialog = compose(
         } else {
             winDav = new URL(props.data.downloadUrl);
             cyberDav = new URL(props.data.downloadUrl);
-            winDav.pathname = `/by_id/${props.data.uuid}`;
-            cyberDav.pathname = `/by_id/${props.data.uuid}`;
+            winDav.pathname = `/c=${props.data.uuid}`;
+            cyberDav.pathname = `/c=${props.data.uuid}`;
         }
 
         cyberDav.username = props.data.username;
@@ -279,8 +279,8 @@ export const WebDavS3InfoDialog = compose(
                     </DetailsAttribute>
 
                     <p>
-                      Note: This curl command downloads single files.
-                      Append the desired filename to the end of the URL.
+                        Note: This curl command downloads single files.
+                        Append the desired filename to the end of the URL.
                     </p>
 
                 </TabPanel>
@@ -292,7 +292,7 @@ export const WebDavS3InfoDialog = compose(
                     color='primary'
                     onClick={props.closeDialog}>
                     Close
-               </Button>
+                </Button>
             </DialogActions>
 
         </Dialog >;
index b09b499e1c424f3db24c9a122725b376cfb47e2f..4fa4701a48cc5672fb0a1d895c5749fe775347d4 100644 (file)
@@ -13,16 +13,18 @@ import {
     CardContent,
     Tooltip,
     Typography,
+    Button,
 } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
-import { CloseIcon, MoreOptionsIcon, ProcessIcon } from 'components/icon/icon';
+import { CloseIcon, MoreOptionsIcon, ProcessIcon, StartIcon } from 'components/icon/icon';
 import { Process } from 'store/processes/process';
 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
 import { ProcessDetailsAttributes } from './process-details-attributes';
 import { ProcessStatus } from 'views-components/data-explorer/renderers';
 import { ContainerState } from 'models/container';
+import { ContainerRequestState } from 'models/container-request';
 
-type CssRules = 'card' | 'content' | 'title' | 'header' | 'cancelButton' | 'avatar' | 'iconHeader';
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'cancelButton' | 'avatar' | 'iconHeader' | 'runButton';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     card: {
@@ -60,18 +62,27 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
             cursor: 'pointer'
         }
     },
+    runButton: {
+        backgroundColor: theme.customs.colors.green700,
+        '&:hover': {
+            backgroundColor: theme.customs.colors.green800,
+        },
+        padding: "0px 5px 0 0",
+        marginRight: "5px",
+    },
 });
 
 export interface ProcessDetailsCardDataProps {
     process: Process;
     cancelProcess: (uuid: string) => void;
+    startProcess: (uuid: string) => void;
     onContextMenu: (event: React.MouseEvent<HTMLElement>) => void;
 }
 
 type ProcessDetailsCardProps = ProcessDetailsCardDataProps & WithStyles<CssRules> & MPVPanelProps;
 
 export const ProcessDetailsCard = withStyles(styles)(
-    ({ cancelProcess, onContextMenu, classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
+    ({ cancelProcess, startProcess, onContextMenu, classes, process, doHidePanel, panelName }: ProcessDetailsCardProps) => {
         return <Card className={classes.card}>
             <CardHeader
                 className={classes.header}
@@ -95,6 +106,16 @@ export const ProcessDetailsCard = withStyles(styles)(
                     </Tooltip>}
                 action={
                     <div>
+                        {process.containerRequest.state === ContainerRequestState.UNCOMMITTED &&
+                            <Button
+                                variant="contained"
+                                size="small"
+                                color="primary"
+                                className={classes.runButton}
+                                onClick={() => startProcess(process.containerRequest.uuid)}>
+                                <StartIcon />
+                                Run Process
+                            </Button>}
                         {process.container && process.container.state === ContainerState.RUNNING &&
                             <span className={classes.cancelButton} onClick={() => cancelProcess(process.containerRequest.uuid)}>Cancel</span>}
                         <ProcessStatus uuid={process.containerRequest.uuid} />
index adb6c920746b08b73268c2a3d5b0980d9237280a..11b31ae0c97e5f4bb0d83e84718d60d32fd476f8 100644 (file)
@@ -51,6 +51,7 @@ export interface ProcessPanelRootActionProps {
     onContextMenu: (event: React.MouseEvent<HTMLElement>, process: Process) => void;
     onToggle: (status: string) => void;
     cancelProcess: (uuid: string) => void;
+    startProcess: (uuid: string) => void;
     onLogFilterChange: (filter: FilterOption) => void;
     navigateToLog: (uuid: string) => void;
     onCopyToClipboard: (uuid: string) => void;
@@ -122,6 +123,7 @@ export const ProcessPanelRoot = withStyles(styles)(
                         process={process}
                         onContextMenu={event => props.onContextMenu(event, process)}
                         cancelProcess={props.cancelProcess}
+                        startProcess={props.startProcess}
                     />
                 </MPVPanelContent>
                 <MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
index d853fd091d767b6efcc4870689073263ac9dddfa..2ad7e2a37b6cd8bb24a78c6fdd2581a105d65297 100644 (file)
@@ -25,7 +25,7 @@ import {
     updateOutputParams,
     loadNodeJson
 } from 'store/process-panel/process-panel-actions';
-import { cancelRunningWorkflow } from 'store/processes/processes-actions';
+import { cancelRunningWorkflow, startWorkflow } from 'store/processes/processes-actions';
 import { navigateToLogCollection, setProcessLogsPanelFilter } from 'store/process-logs-panel/process-logs-panel-actions';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 
@@ -62,6 +62,7 @@ const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps =>
         dispatch<any>(toggleProcessPanelFilter(status));
     },
     cancelProcess: (uuid) => dispatch<any>(cancelRunningWorkflow(uuid)),
+    startProcess: (uuid) => dispatch<any>(startWorkflow(uuid)),
     onLogFilterChange: (filter) => dispatch(setProcessLogsPanelFilter(filter.value)),
     navigateToLog: (uuid) => dispatch<any>(navigateToLogCollection(uuid)),
     loadInputs: (containerRequest) => dispatch<any>(loadInputs(containerRequest)),
index 471ecc40a2e63995e9a38ce27168ac65bab58459..fe5dff8a48f7c30ca474bb634abba980d043da98 100644 (file)
@@ -14,6 +14,8 @@ import { CustomTheme } from 'common/custom-theme';
 import { createServices } from "services/services";
 import 'jest-localstorage-mock';
 
+jest.mock('views-components/baner/banner', () => ({ Banner: () => 'Banner' }))
+
 const history = createBrowserHistory();
 
 it('renders without crashing', () => {
index 87f004b335693b7df95a32102415f2b7232b75be..7103efd132a1b8e1ac149229d1fbcda0e7620a7f 100644 (file)
@@ -100,6 +100,7 @@ import { WebDavS3InfoDialog } from 'views-components/webdav-s3-dialog/webdav-s3-
 import { pluginConfig } from 'plugins';
 import { ElementListReducer } from 'common/plugintypes';
 import { COLLAPSE_ICON_SIZE } from 'views-components/side-panel-toggle/side-panel-toggle'
+import { Banner } from 'views-components/baner/banner';
 
 type CssRules = 'root' | 'container' | 'splitter' | 'asidePanel' | 'contentWrapper' | 'content';
 
@@ -293,6 +294,7 @@ export const WorkbenchPanel =
             <VirtualMachineAttributesDialog />
             <FedLogin />
             <WebDavS3InfoDialog />
+            <Banner />
             {React.createElement(React.Fragment, null, pluginConfig.dialogs)}
         </Grid>}
     );