Merge remote-tracking branch 'origin/main' into 18692-frozen-projects-workbench-support
authorDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Mon, 8 Aug 2022 10:55:15 +0000 (12:55 +0200)
committerDaniel Kutyła <daniel.kutyla@contractors.roche.com>
Mon, 8 Aug 2022 10:55:28 +0000 (12:55 +0200)
Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>

45 files changed:
cypress/integration/collection.spec.js
cypress/integration/process.spec.js
cypress/integration/project.spec.js
cypress/integration/search.spec.js
package.json
src/components/code-snippet/code-snippet.tsx
src/components/default-code-snippet/default-code-snippet.tsx
src/components/icon/icon.tsx
src/index.tsx
src/models/log.ts
src/routes/routes.ts
src/services/auth-service/auth-service.ts
src/store/all-processes-panel/all-processes-panel-middleware-service.ts
src/store/context-menu/context-menu-actions.ts
src/store/data-explorer/data-explorer-reducer.ts
src/store/navigation/navigation-action.ts
src/store/open-in-new-tab/open-in-new-tab.actions.ts
src/store/process-logs-panel/process-logs-panel-actions.ts
src/store/process-logs-panel/process-logs-panel-reducer.ts
src/store/processes/process-command-actions.ts [deleted file]
src/store/processes/processes-actions.ts
src/store/project-panel/project-panel-middleware-service.ts
src/store/projects/project-update-actions.ts
src/store/subprocess-panel/subprocess-panel-middleware-service.ts
src/views-components/context-menu/action-sets/process-resource-action-set.ts
src/views-components/context-menu/action-sets/search-results-action-set.ts [new file with mode: 0644]
src/views-components/context-menu/actions/copy-to-clipboard-action.tsx
src/views-components/context-menu/actions/helpers.test.ts
src/views-components/context-menu/actions/helpers.ts
src/views-components/context-menu/context-menu.tsx
src/views-components/login-form/login-form.tsx
src/views-components/process-command-dialog/process-command-dialog.tsx [deleted file]
src/views-components/process-runtime-status/process-runtime-status.tsx
src/views-components/resource-properties/resource-properties-list.tsx
src/views-components/token-dialog/token-dialog.test.tsx
src/views/process-panel/process-cmd-card.tsx [new file with mode: 0644]
src/views/process-panel/process-details-attributes.tsx
src/views/process-panel/process-log-card.tsx
src/views/process-panel/process-log-code-snippet.tsx
src/views/process-panel/process-panel-root.tsx
src/views/process-panel/process-panel.tsx
src/views/search-results-panel/search-results-panel.tsx
src/views/workbench/workbench.tsx
tools/run-integration-tests.sh
yarn.lock

index fd8e65b26f5f9c60cf54edc2b06ba5553f39731f..28454a9093b3e87499b8daf652ecafb01df4fdaa 100644 (file)
@@ -374,7 +374,7 @@ describe('Collection panel tests', function () {
                 cy.goToPath(`/collections/${this.testCollection.uuid}`);
 
                 ['subdir', 'G%C3%BCnter\'s%20file', 'table%&?*2'].forEach((subdir) => {
-                    cy.get('[data-cy=collection-files-panel]')
+                    cy.waitForDom().get('[data-cy=collection-files-panel]')
                         .contains('bar').rightclick();
                     cy.get('[data-cy=context-menu]')
                         .contains('Rename')
@@ -642,7 +642,7 @@ describe('Collection panel tests', function () {
                     .contains(projName).and('contain', testProject.uuid);
                 // Double check that the collection is in the project
                 cy.goToPath(`/projects/${testProject.uuid}`);
-                cy.get('[data-cy=project-panel]').should('contain', collName);
+                cy.waitForDom().get('[data-cy=project-panel]').should('contain', collName);
             });
     });
 
index 48b936cf69c8e50e225b32e8d316826cec67c141..55290fa36bb4d8a97ecd47b3056f185a4ed83396 100644 (file)
@@ -168,8 +168,15 @@ describe('Process tests', function() {
             cy.getAll('@stdoutLogs', '@nodeInfoLogs', '@crunchRunLogs').then(function() {
                 cy.loginAs(activeUser);
                 cy.goToPath(`/processes/${containerRequest.uuid}`);
-                // Should should all logs
-                cy.get('[data-cy=process-logs-filter]').should('contain', 'All logs');
+                // Should show main logs by default
+                cy.get('[data-cy=process-logs-filter]').should('contain', 'Main logs');
+                cy.get('[data-cy=process-logs]')
+                    .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
+                    .and('not.contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
+                    .and('contain', crunchRunLogs[Math.floor(Math.random() * crunchRunLogs.length)]);
+                // Select 'All logs'
+                cy.get('[data-cy=process-logs-filter]').click();
+                cy.get('body').contains('li', 'All logs').click();
                 cy.get('[data-cy=process-logs]')
                     .should('contain', stdoutLogs[Math.floor(Math.random() * stdoutLogs.length)])
                     .and('contain', nodeInfoLogs[Math.floor(Math.random() * nodeInfoLogs.length)])
@@ -244,5 +251,27 @@ describe('Process tests', function() {
                 .should('contain', 'Free disk space is low')
                 .and('contain', 'No additional warning details available');
         });
+
+
+        // Force container_count for testing
+        let containerCount = 2;
+        cy.intercept({method: 'GET', url: '**/arvados/v1/container_requests/*'}, (req) => {
+            req.reply((res) => {
+                res.body.container_count = containerCount;
+            });
+        });
+
+        cy.getAll('@containerRequest').then(function([containerRequest]) {
+            cy.goToPath(`/processes/${containerRequest.uuid}`);
+            cy.get('[data-cy=process-runtime-status-retry-warning]')
+                .should('contain', 'Process retried 1 time');
+        });
+
+        cy.getAll('@containerRequest').then(function([containerRequest]) {
+            containerCount = 3;
+            cy.goToPath(`/processes/${containerRequest.uuid}`);
+            cy.get('[data-cy=process-runtime-status-retry-warning]')
+                .should('contain', 'Process retried 2 times');
+        });
     });
 });
index 26b4a150879301cdd7da560b572c2a6f45a03c01..d4d32117c966d230c07526f42fc309ee5dd6a29b 100644 (file)
@@ -85,6 +85,39 @@ describe('Project tests', function() {
                 // Pink is not in the test vocab
                 {IDTAGCOLORS: ['IDVALCOLORS3', 'Pink', 'IDVALCOLORS1']});
         });
+
+        // Open project edit via breadcrumbs
+        cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
+        cy.get('[data-cy=context-menu]').contains('Edit').click();
+        cy.get('[data-cy=form-dialog]').within(() => {
+            cy.get('[data-cy=resource-properties-list]').within(() => {
+                cy.get('div[role=button]').contains('Color: Magenta');
+                cy.get('div[role=button]').contains('Color: Pink');
+                cy.get('div[role=button]').contains('Color: Yellow');
+            });
+        });
+        // Add another property
+        cy.get('[data-cy=resource-properties-form]').within(() => {
+            cy.get('[data-cy=property-field-key]').within(() => {
+                cy.get('input').type('Animal');
+            });
+            cy.get('[data-cy=property-field-value]').within(() => {
+                cy.get('input').type('Dog');
+            });
+            cy.root().submit();
+        });
+        cy.get('[data-cy=form-submit-btn]').click();
+        // Reopen edit via breadcrumbs and verify properties
+        cy.get('[data-cy=breadcrumbs]').contains(projName).rightclick();
+        cy.get('[data-cy=context-menu]').contains('Edit').click();
+        cy.get('[data-cy=form-dialog]').within(() => {
+            cy.get('[data-cy=resource-properties-list]').within(() => {
+                cy.get('div[role=button]').contains('Color: Magenta');
+                cy.get('div[role=button]').contains('Color: Pink');
+                cy.get('div[role=button]').contains('Color: Yellow');
+                cy.get('div[role=button]').contains('Animal: Dog');
+            });
+        });
     });
 
     it('creates new project on home project and then a subproject inside it', function() {
index 5434ca248a5167e5461014491d77f422980ae693..2216c067f8423f7c36fdfd4c7b10ca23402d707c 100644 (file)
@@ -126,4 +126,158 @@ describe('Search tests', function() {
                 });
         });
     });
-});
\ No newline at end of file
+
+    it('shows search context menu', function() {
+        const colName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+        const federatedColName = `Collection ${Math.floor(Math.random() * Math.floor(999999))}`;
+        const federatedColUuid = "xxxxx-4zz18-000000000000000";
+
+        // Intercept config to insert remote cluster
+        cy.intercept({method: 'GET', hostname: 'localhost', url: '**/arvados/v1/config?nocache=*'}, (req) => {
+            req.reply((res) => {
+                res.body.RemoteClusters = {
+                    "*": res.body.RemoteClusters["*"],
+                    "xxxxx": {
+                        "ActivateUsers": true,
+                        "Host": "xxxxx.fakecluster.tld",
+                        "Insecure": false,
+                        "Proxy": true,
+                        "Scheme": ""
+                    }
+                };
+            });
+        });
+
+        // Fake remote cluster config
+        cy.intercept(
+          {
+            method: "GET",
+            hostname: "xxxxx.fakecluster.tld",
+            url: "**/arvados/v1/config",
+          },
+          {
+            statusCode: 200,
+            body: {
+              API: {},
+              ClusterID: "xxxxx",
+              Collections: {},
+              Containers: {},
+              InstanceTypes: {},
+              Login: {},
+              Mail: { SupportEmailAddress: "arvados@example.com" },
+              RemoteClusters: {
+                "*": {
+                  ActivateUsers: false,
+                  Host: "",
+                  Insecure: false,
+                  Proxy: false,
+                  Scheme: "https",
+                },
+              },
+              Services: {
+                Composer: { ExternalURL: "" },
+                Controller: { ExternalURL: "https://xxxxx.fakecluster.tld:34763/" },
+                DispatchCloud: { ExternalURL: "" },
+                DispatchLSF: { ExternalURL: "" },
+                DispatchSLURM: { ExternalURL: "" },
+                GitHTTP: { ExternalURL: "https://xxxxx.fakecluster.tld:39105/" },
+                GitSSH: { ExternalURL: "" },
+                Health: { ExternalURL: "https://xxxxx.fakecluster.tld:42915/" },
+                Keepbalance: { ExternalURL: "" },
+                Keepproxy: { ExternalURL: "https://xxxxx.fakecluster.tld:46773/" },
+                Keepstore: { ExternalURL: "" },
+                RailsAPI: { ExternalURL: "" },
+                WebDAV: { ExternalURL: "https://xxxxx.fakecluster.tld:36041/" },
+                WebDAVDownload: { ExternalURL: "https://xxxxx.fakecluster.tld:42957/" },
+                WebShell: { ExternalURL: "" },
+                Websocket: { ExternalURL: "wss://xxxxx.fakecluster.tld:37121/websocket" },
+                Workbench1: { ExternalURL: "https://wb1.xxxxx.fakecluster.tld/" },
+                Workbench2: { ExternalURL: "https://wb2.xxxxx.fakecluster.tld/" },
+              },
+              StorageClasses: {
+                default: { Default: true, Priority: 0 },
+              },
+              Users: {},
+              Volumes: {},
+              Workbench: {},
+            },
+          }
+        );
+
+        cy.createCollection(adminUser.token, {
+            name: colName,
+            owner_uuid: activeUser.user.uuid,
+            preserve_version: true,
+            manifest_text: ". 37b51d194a7513e45b56f6524f2d51f2+3 0:3:bar\n"
+        }).then(function(testCollection) {
+            cy.loginAs(activeUser);
+
+            // Intercept search results to add federated result
+            cy.intercept({method: 'GET', url: '**/arvados/v1/groups/contents?*'}, (req) => {
+                req.reply((res) => {
+                    res.body.items = [
+                        res.body.items[0],
+                        {
+                            ...res.body.items[0],
+                            uuid: federatedColUuid,
+                            portable_data_hash: "00000000000000000000000000000000+0",
+                            name: federatedColName,
+                            href: res.body.items[0].href.replace(testCollection.uuid, federatedColUuid),
+                        }
+                    ];
+                    res.body.items_available += 1;
+                });
+            });
+
+            cy.doSearch(colName);
+
+            // Stub new window
+            cy.window().then(win => {
+                cy.stub(win, 'open').as('Open')
+            });
+
+            // Check copy to clipboard
+            cy.get('[data-cy=search-results]').contains(colName).rightclick();
+            cy.get('[data-cy=context-menu]').within((ctx) => {
+                // Check that there are 4 items in the menu
+                cy.get(ctx).children().should('have.length', 4);
+                cy.contains('Advanced');
+                cy.contains('Copy to clipboard');
+                cy.contains('Open in new tab');
+                cy.contains('View details');
+
+                cy.contains('Copy to clipboard').click();
+                cy.window().then((win) => (
+                    win.navigator.clipboard.readText().then((text) => {
+                        expect(text).to.match(new RegExp(`/collections/${testCollection.uuid}$`));
+                    })
+                ));
+            });
+
+            // Check open in new tab
+            cy.get('[data-cy=search-results]').contains(colName).rightclick();
+            cy.get('[data-cy=context-menu]').within(() => {
+                cy.contains('Open in new tab').click();
+                cy.get('@Open').should('have.been.calledOnceWith', `${window.location.origin}/collections/${testCollection.uuid}`)
+            });
+
+            // Check federated result copy to clipboard
+            cy.get('[data-cy=search-results]').contains(federatedColName).rightclick();
+            cy.get('[data-cy=context-menu]').within(() => {
+                cy.contains('Copy to clipboard').click();
+                cy.window().then((win) => (
+                    win.navigator.clipboard.readText().then((text) => {
+                        expect(text).to.equal(`https://wb2.xxxxx.fakecluster.tld/collections/${federatedColUuid}`);
+                    })
+                ));
+            });
+            // Check open in new tab
+            cy.get('[data-cy=search-results]').contains(federatedColName).rightclick();
+            cy.get('[data-cy=context-menu]').within(() => {
+                cy.contains('Open in new tab').click();
+                cy.get('@Open').should('have.been.calledWith', `https://wb2.xxxxx.fakecluster.tld/collections/${federatedColUuid}`)
+            });
+
+        });
+    });
+});
index a8b3ee819a860fbb06aff7effef0f6f9198a89af..9e663ca6ac5a440946e91f1f3a50ce030db355e0 100644 (file)
@@ -22,7 +22,7 @@
     "@types/react-virtualized-auto-sizer": "1.0.0",
     "@types/react-window": "1.8.2",
     "@types/redux-form": "7.4.12",
-    "@types/shell-quote": "1.6.0",
+    "@types/shell-escape": "^0.2.0",
     "axios": "^0.21.1",
     "babel-core": "6.26.3",
     "babel-runtime": "6.26.0",
@@ -71,7 +71,7 @@
     "redux-thunk": "2.3.0",
     "reselect": "4.0.0",
     "set-value": "2.0.1",
-    "shell-quote": "1.6.1",
+    "shell-escape": "^0.2.0",
     "sinon": "7.3",
     "tslint": "5.20.0",
     "tslint-etc": "1.6.0",
index f0a2b2131fcfbd4bfd0592c24fbebd68b1d44f0b..83c378b899ec11a884cb6d213b932423a14ba0aa 100644 (file)
@@ -3,9 +3,14 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import React from 'react';
-import { StyleRulesCallback, WithStyles, Typography, withStyles } from '@material-ui/core';
+import { StyleRulesCallback, WithStyles, Typography, withStyles, Link } from '@material-ui/core';
 import { ArvadosTheme } from 'common/custom-theme';
 import classNames from 'classnames';
+import { connect, DispatchProp } from 'react-redux';
+import { RootState } from 'store/store';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { Dispatch } from 'redux';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
 
 type CssRules = 'root' | 'space';
 
@@ -24,19 +29,57 @@ export interface CodeSnippetDataProps {
     lines: string[];
     className?: string;
     apiResponse?: boolean;
+    linked?: boolean;
+}
+
+interface CodeSnippetAuthProps {
+    auth: FederationConfig;
 }
 
 type CodeSnippetProps = CodeSnippetDataProps & WithStyles<CssRules>;
 
-export const CodeSnippet = withStyles(styles)(
-    ({ classes, lines, className, apiResponse }: CodeSnippetProps) =>
+const mapStateToProps = (state: RootState): CodeSnippetAuthProps => ({
+    auth: state.auth,
+});
+
+export const CodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+    ({ classes, lines, linked, className, apiResponse, dispatch, auth }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
         <Typography
         component="div"
         className={classNames(classes.root, className)}>
-            {
-                lines.map((line: string, index: number) => {
-                    return <Typography key={index} className={apiResponse ? classes.space : className} component="pre">{line}</Typography>;
-                })
-            }
+            <Typography className={apiResponse ? classes.space : className} component="pre">
+                {linked ?
+                    lines.map((line, index) => <React.Fragment key={index}>{renderLinks(auth, dispatch)(line)}{`\n`}</React.Fragment>) :
+                    lines.join('\n')
+                }
+            </Typography>
         </Typography>
-    );
\ No newline at end of file
+    ));
+
+const renderLinks = (auth: FederationConfig, dispatch: Dispatch) => (text: string): JSX.Element => {
+    // Matches UUIDs & PDHs
+    const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
+    const links = text.match(REGEX);
+    if (!links) {
+        return <>{text}</>;
+    }
+    return <>
+        {text.split(REGEX).map((part, index) =>
+            <React.Fragment key={index}>
+                {part}
+                {links[index] &&
+                <Link onClick={() => {
+                    const url = getNavUrl(links[index], auth)
+                    if (url) {
+                        window.open(`${window.location.origin}${url}`, '_blank');
+                    } else {
+                        dispatch(navigationNotAvailable(links[index]));
+                    }
+                }}
+                    style={ {cursor: 'pointer'} }>
+                    {links[index]}
+                </Link>}
+            </React.Fragment>
+        )}
+    </>;
+  };
index 7ba97db49fbef65b723c51327ac47e94fcf6e7fe..bdcfc10f644b8c4f7ff38bed436bc521333d7109 100644 (file)
@@ -6,8 +6,9 @@ import React from 'react';
 import { MuiThemeProvider, createMuiTheme } from '@material-ui/core/styles';
 import { CodeSnippet, CodeSnippetDataProps } from 'components/code-snippet/code-snippet';
 import grey from '@material-ui/core/colors/grey';
+import { themeOptions } from 'common/custom-theme';
 
-const theme = createMuiTheme({
+const theme = createMuiTheme(Object.assign({}, themeOptions, {
     overrides: {
         MuiTypography: {
             body1: {
@@ -22,9 +23,9 @@ const theme = createMuiTheme({
         fontFamily: 'monospace',
         useNextVariants: true,
     }
-});
+}));
 
-export const DefaultCodeSnippet = (props: CodeSnippetDataProps) => 
+export const DefaultCodeSnippet = (props: CodeSnippetDataProps) =>
     <MuiThemeProvider theme={theme}>
         <CodeSnippet {...props} />
-    </MuiThemeProvider>;
\ No newline at end of file
+    </MuiThemeProvider>;
index 74c2f54ccab6e3096a6852d7fe81105e6e63ce81..960cc38b1c63f8bb0c1e02ca35a8754a4e42dd5b 100644 (file)
@@ -77,6 +77,7 @@ import NotInterested from '@material-ui/icons/NotInterested';
 // Import FontAwesome icons
 import { library } from '@fortawesome/fontawesome-svg-core';
 import { faPencilAlt, faSlash, faUsers, faEllipsisH } from '@fortawesome/free-solid-svg-icons';
+import { FormatAlignLeft } from '@material-ui/icons';
 library.add(
     faPencilAlt,
     faSlash,
@@ -194,7 +195,8 @@ export const CanReadIcon: IconType = (props) => <RemoveRedEye {...props} />;
 export const CanWriteIcon: IconType = (props) => <Edit {...props} />;
 export const CanManageIcon: IconType = (props) => <Computer {...props} />;
 export const AddUserIcon: IconType = (props) => <PersonAdd {...props} />;
-export const WordWrapIcon: IconType = (props) => <WrapText {...props} />;
+export const WordWrapOnIcon: IconType = (props) => <WrapText {...props} />;
+export const WordWrapOffIcon: IconType = (props) => <FormatAlignLeft {...props} />;
 export const TextIncreaseIcon: IconType = (props) => <TextIncrease {...props} />;
 export const TextDecreaseIcon: IconType = (props) => <TextDecrease {...props} />;
 export const DeactivateUserIcon: IconType = (props) => <NotInterested {...props} />;
index d00095411f9db927b5617e7d788031226b75fe51..c214228258fd7945a44438346718f4937180f334 100644 (file)
@@ -66,6 +66,7 @@ import { workflowActionSet } from 'views-components/context-menu/action-sets/wor
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
 import { openNotFoundDialog } from './store/not-found-panel/not-found-panel-action';
 import { storeRedirects } from './common/redirect-to';
+import { searchResultsActionSet } from 'views-components/context-menu/action-sets/search-results-action-set';
 
 console.log(`Starting arvados [${getBuildInfo()}]`);
 
@@ -106,6 +107,7 @@ addMenuActionSet(ContextMenuKind.FROZEN_PROJECT_ADMIN, frozenAdminActionSet);
 addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
 addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
 addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
+addMenuActionSet(ContextMenuKind.SEARCH_RESULTS, searchResultsActionSet);
 
 storeRedirects();
 
index 20060f88b640f4b2f1d720b0babcee290e65a5af..f5d351acb1176578a3f60a6115a9cb3aeafe66d9 100644 (file)
@@ -17,12 +17,13 @@ export enum LogEventType {
     STDOUT = 'stdout',
     STDERR = 'stderr',
     CONTAINER = 'container',
+    KEEPSTORE = 'keepstore',
 }
 
 export interface LogResource extends Resource, ResourceWithProperties {
     kind: ResourceKind.LOG;
     objectUuid: string;
     eventAt: string;
-    eventType: string;
+    eventType: LogEventType;
     summary: string;
 }
index 50689ec37c46ac1fba6d5ff985badeb233944031..22c8f4c8e52414b8d84c99cc439facc4d4f9ecdf 100644 (file)
@@ -66,7 +66,10 @@ export const getResourceUrl = (uuid: string) => {
     }
 };
 
-export const getNavUrl = (uuid: string, config: FederationConfig) => {
+/**
+ * @returns A relative or federated url for the given uuid, with a token for federated WB1 urls
+ */
+export const getNavUrl = (uuid: string, config: FederationConfig, includeToken: boolean = true): string => {
     const path = getResourceUrl(uuid) || "";
     const cls = uuid.substring(0, 5);
     if (cls === config.localCluster || extractUuidKind(uuid) === ResourceKind.USER || COLLECTION_PDH_REGEX.exec(uuid)) {
@@ -83,7 +86,9 @@ export const getNavUrl = (uuid: string, config: FederationConfig) => {
             u = new URL(config.remoteHostsConfig[cls].workbench2Url);
         } else {
             u = new URL(config.remoteHostsConfig[cls].workbenchUrl);
-            u.search = "api_token=" + config.sessions.filter((s) => s.clusterId === cls)[0].token;
+            if (includeToken) {
+                u.search = "api_token=" + config.sessions.filter((s) => s.clusterId === cls)[0].token;
+            }
         }
         u.pathname = path;
         return u.toString();
index 548dbcaa13d6238d343560dc459ba5cb35e1ba7b..52bfa29ecae21bb2a86967d0dbd63cd9b072f9fd 100644 (file)
@@ -68,13 +68,16 @@ export class AuthService {
         }
     }
 
+    public setTargetUrl(url: string) {
+        localStorage.setItem(TARGET_URL, url);
+    }
+
     public removeTargetURL() {
         localStorage.removeItem(TARGET_URL);
-        sessionStorage.removeItem(TARGET_URL);
     }
 
     public getTargetURL() {
-        return this.getStorage().getItem(TARGET_URL);
+        return localStorage.getItem(TARGET_URL);
     }
 
     public removeApiToken() {
@@ -113,7 +116,7 @@ export class AuthService {
         const currentUrl = `${window.location.protocol}//${window.location.host}/token`;
         const homeClusterHost = remoteHosts[homeCluster];
         const rd = new URL(window.location.href);
-        this.getStorage().setItem(TARGET_URL, rd.pathname + rd.search);
+        this.setTargetUrl(rd.pathname + rd.search);
         window.location.assign(`https://${homeClusterHost}/login?${(uuidPrefix !== homeCluster && homeCluster !== loginCluster) ? "remote=" + uuidPrefix + "&" : ""}return_to=${currentUrl}`);
     }
 
index 88b64e6243955f7a9e15d90e51176efc29932bad..5d5e77d6c64396af8628fb9d1ccb9e3aef3cea83 100644 (file)
@@ -37,7 +37,11 @@ export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareSe
             try {
                 api.dispatch(progressIndicatorActions.START_WORKING(this.getId()));
                 const processItems = await this.services.containerRequestService.list(
-                    { ...getParams(dataExplorer) });
+                    {
+                        ...getParams(dataExplorer),
+                        // Omit mounts when viewing all process panel
+                        select: containerRequestFieldsNoMounts,
+                    });
 
                 api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
                 api.dispatch(resourcesActions.SET_RESOURCES(processItems.items));
@@ -62,6 +66,44 @@ export class AllProcessesPanelMiddlewareService extends DataExplorerMiddlewareSe
     }
 }
 
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+export const containerRequestFieldsNoMounts = [
+    "command",
+    "container_count_max",
+    "container_count",
+    "container_image",
+    "container_uuid",
+    "created_at",
+    "cwd",
+    "description",
+    "environment",
+    "etag",
+    "expires_at",
+    "filters",
+    "href",
+    "kind",
+    "log_uuid",
+    "modified_at",
+    "modified_by_client_uuid",
+    "modified_by_user_uuid",
+    "name",
+    "output_name",
+    "output_path",
+    "output_properties",
+    "output_storage_classes",
+    "output_ttl",
+    "output_uuid",
+    "owner_uuid",
+    "priority",
+    "properties",
+    "requesting_container_uuid",
+    "runtime_constraints",
+    "scheduling_parameters",
+    "state",
+    "use_existing",
+    "uuid",
+];
+
 const getParams = ( dataExplorer: DataExplorer ) => ({
     ...dataExplorerToListParams(dataExplorer),
     order: getOrder(dataExplorer),
index f01c1866581e8bab57bcffd44f2924274b6cc070..3bc91ae0c74c1464aecb5e5c7d0ba7b3b56a0353 100644 (file)
@@ -10,7 +10,7 @@ import { RootState } from 'store/store';
 import { getResource, getResourceWithEditableStatus } from '../resources/resources';
 import { UserResource } from 'models/user';
 import { isSidePanelTreeCategory } from 'store/side-panel-tree/side-panel-tree-actions';
-import { extractUuidKind, ResourceKind, EditableResource } from 'models/resource';
+import { extractUuidKind, ResourceKind, EditableResource, Resource } from 'models/resource';
 import { Process } from 'store/processes/process';
 import { RepositoryResource } from 'models/repositories';
 import { SshKeyResource } from 'models/ssh-key';
@@ -277,3 +277,17 @@ export const resourceUuidToContextMenuKind = (uuid: string, readonly = false) =>
                 return;
         }
     };
+
+export const openSearchResultsContextMenu = (event: React.MouseEvent<HTMLElement>, uuid: string) =>
+    (dispatch: Dispatch, getState: () => RootState) => {
+        const res = getResource<Resource>(uuid)(getState().resources);
+        if (res) {
+            dispatch<any>(openContextMenu(event, {
+                name: '',
+                uuid: res.uuid,
+                ownerUuid: '',
+                kind: res.kind,
+                menuKind: ContextMenuKind.SEARCH_RESULTS,
+            }));
+        }
+    };
index f67cccdcd48dc9eaea220c835149549d6dc871f6..1e5cd88fa1299c2eea4f42fed52ad46a4c8445e3 100644 (file)
@@ -32,7 +32,7 @@ export const initialDataExplorer: DataExplorer = {
     itemsAvailable: 0,
     page: 0,
     rowsPerPage: 50,
-    rowsPerPageOptions: [50, 100, 200, 500],
+    rowsPerPageOptions: [10, 20, 50, 100, 200, 500],
     searchValue: "",
     requestState: DataTableRequestState.IDLE
 };
index c8811bf43955a92ad493ddc6e2acc28e8846b1b3..146530cae8e3ffaf530da19ca3be07743026c78f 100644 (file)
@@ -14,7 +14,7 @@ import { pluginConfig } from 'plugins';
 import { snackbarActions, SnackbarKind } from 'store/snackbar/snackbar-actions';
 import { USERS_PANEL_LABEL, MY_ACCOUNT_PANEL_LABEL } from 'store/breadcrumbs/breadcrumbs-actions';
 
-const navigationNotAvailable = (id: string) =>
+export const navigationNotAvailable = (id: string) =>
     snackbarActions.OPEN_SNACKBAR({
         message: `${id} not available`,
         hideDuration: 3000,
index 94aec140d64f2bc0b7a906465244ea29f3c3e6b6..c465aae8695a51291918aa0fd630e57fe8b327c6 100644 (file)
@@ -3,35 +3,25 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import copy from 'copy-to-clipboard';
-import { ResourceKind } from 'models/resource';
-import { getClipboardUrl } from 'views-components/context-menu/actions/helpers';
+import { Dispatch } from 'redux';
+import { getNavUrl } from 'routes/routes';
+import { RootState } from 'store/store';
 
-const getUrl = (resource: any) => {
-    let url: string | null = null;
-    const { uuid, kind } = resource;
+export const openInNewTabAction = (resource: any) => (dispatch: Dispatch, getState: () => RootState) => {
+    const url = getNavUrl(resource.uuid, getState().auth);
 
-    if (kind === ResourceKind.COLLECTION) {
-        url = `/collections/${uuid}`;
-    }
-    if (kind === ResourceKind.PROJECT) {
-        url = `/projects/${uuid}`;
-    }
-
-    return url;
-};
-
-export const openInNewTabAction = (resource: any) => () => {
-    const url = getUrl(resource);
-
-    if (url) {
+    if (url[0] === '/') {
         window.open(`${window.location.origin}${url}`, '_blank');
+    } else if (url.length) {
+        window.open(url, '_blank');
     }
 };
 
-export const copyToClipboardAction = (resource: any) => () => {
-    const url = getUrl(resource);
+export const copyToClipboardAction = (resource: any) => (dispatch: Dispatch, getState: () => RootState) => {
+    // Copy to clipboard omits token to avoid accidental sharing
+    const url = getNavUrl(resource.uuid, getState().auth, false);
 
     if (url) {
-        copy(getClipboardUrl(url, false));
+        copy(url);
     }
-};
\ No newline at end of file
+};
index b0ddd7ee11f70dea0c122bb5afb2b4681976ee28..d4f5ab59244cabccea950b3bbcaf119feb63e83b 100644 (file)
@@ -45,21 +45,25 @@ export const addProcessLogsPanelItem = (message: ResourceEventMessage<{ text: st
     async (dispatch: Dispatch, getState: () => RootState, { logService }: ServiceRepository) => {
         if (PROCESS_PANEL_LOG_EVENT_TYPES.indexOf(message.eventType) > -1) {
             const uuid = getProcessLogsPanelCurrentUuid(getState().router);
-            if (uuid) {
-                const process = getProcess(uuid)(getState().resources);
-                if (process) {
-                    const { containerRequest, container } = process;
-                    if (message.objectUuid === containerRequest.uuid
-                        || (container && message.objectUuid === container.uuid)) {
-                        dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
-                            logType: COMBINED_FILTER_TYPE,
-                            log: message.properties.text
-                        }));
-                        dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
-                            logType: message.eventType,
-                            log: message.properties.text
-                        }));
-                    }
+            if (!uuid) { return }
+            const process = getProcess(uuid)(getState().resources);
+            if (!process) { return }
+            const { containerRequest, container } = process;
+            if (message.objectUuid === containerRequest.uuid
+                || (container && message.objectUuid === container.uuid)) {
+                dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+                    logType: ALL_FILTER_TYPE,
+                    log: message.properties.text
+                }));
+                dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+                    logType: message.eventType,
+                    log: message.properties.text
+                }));
+                if (MAIN_EVENT_TYPES.indexOf(message.eventType) > -1) {
+                    dispatch(processLogsPanelActions.ADD_PROCESS_LOGS_PANEL_ITEM({
+                        logType: MAIN_FILTER_TYPE,
+                        log: message.properties.text
+                    }));
                 }
             }
         }
@@ -84,6 +88,9 @@ const loadContainerLogs = async (containerUuid: string, logService: LogService)
 
 const createInitialLogPanelState = (logResources: LogResource[]) => {
     const allLogs = logsToLines(logResources);
+    const mainLogs = logsToLines(logResources.filter(
+        e => MAIN_EVENT_TYPES.indexOf(e.eventType) > -1
+    ));
     const groupedLogResources = groupBy(logResources, log => log.eventType);
     const groupedLogs = Object
         .keys(groupedLogResources)
@@ -91,8 +98,12 @@ const createInitialLogPanelState = (logResources: LogResource[]) => {
             ...grouped,
             [key]: logsToLines(groupedLogResources[key])
         }), {});
-    const filters = [COMBINED_FILTER_TYPE, ...Object.keys(groupedLogs)];
-    const logs = { [COMBINED_FILTER_TYPE]: allLogs, ...groupedLogs };
+    const filters = [MAIN_FILTER_TYPE, ALL_FILTER_TYPE, ...Object.keys(groupedLogs)];
+    const logs = {
+        [MAIN_FILTER_TYPE]: mainLogs,
+        [ALL_FILTER_TYPE]: allLogs,
+        ...groupedLogs
+    };
     return { filters, logs };
 };
 
@@ -111,7 +122,14 @@ export const navigateToLogCollection = (uuid: string) =>
 
 const MAX_AMOUNT_OF_LOGS = 10000;
 
-const COMBINED_FILTER_TYPE = 'All logs';
+const ALL_FILTER_TYPE = 'All logs';
+
+const MAIN_FILTER_TYPE = 'Main logs';
+const MAIN_EVENT_TYPES = [
+    LogEventType.CRUNCH_RUN,
+    LogEventType.STDERR,
+    LogEventType.STDOUT,
+];
 
 const PROCESS_PANEL_LOG_EVENT_TYPES = [
     LogEventType.ARV_MOUNT,
@@ -123,4 +141,5 @@ const PROCESS_PANEL_LOG_EVENT_TYPES = [
     LogEventType.STDERR,
     LogEventType.STDOUT,
     LogEventType.CONTAINER,
+    LogEventType.KEEPSTORE,
 ];
index c7d694c0eeb5f9f080937ce3ec9f6ee73893432a..c502f1b1ff47f18ee97873917e513f3c3daabc6e 100644 (file)
@@ -24,10 +24,13 @@ export const processLogsPanelReducer = (state = initialState, action: ProcessLog
             selectedFilter
         }),
         ADD_PROCESS_LOGS_PANEL_ITEM: ({ logType, log }) => {
+            const filters = state.filters.indexOf(logType) > -1
+                ? state.filters
+                : [...state.filters, logType];
             const currentLogs = state.logs[logType] || [];
             const logsOfType = [...currentLogs, log];
             const logs = { ...state.logs, [logType]: logsOfType };
-            return { ...state, logs };
+            return { ...state, logs, filters };
         },
         default: () => state,
     });
diff --git a/src/store/processes/process-command-actions.ts b/src/store/processes/process-command-actions.ts
deleted file mode 100644 (file)
index 9dec9b3..0000000
+++ /dev/null
@@ -1,28 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import { dialogActions } from 'store/dialog/dialog-actions';
-import { RootState } from 'store/store';
-import { Dispatch } from 'redux';
-import { getProcess } from 'store/processes/process';
-import { quote } from 'shell-quote';
-
-export const PROCESS_COMMAND_DIALOG_NAME = 'processCommandDialog';
-
-export interface ProcessCommandDialogData {
-    command: string;
-    processName: string;
-}
-
-export const openProcessCommandDialog = (processUuid: string) =>
-    (dispatch: Dispatch<any>, getState: () => RootState) => {
-        const process = getProcess(processUuid)(getState().resources);
-        if (process) {
-            const data: ProcessCommandDialogData = {
-                command: quote(process.containerRequest.command),
-                processName: process.containerRequest.name,
-            };
-            dispatch(dialogActions.OPEN_DIALOG({ id: PROCESS_COMMAND_DIALOG_NAME, data }));
-        }
-    };
index 213e292bfe6f047e306d20dda6ad477e7652b525..c4d421ac09d9b5719a9f8d1b8f9a00833b7cf662 100644 (file)
@@ -34,13 +34,55 @@ export const loadProcess = (containerRequestUuid: string) =>
         return { containerRequest };
     };
 
-export const loadContainers = (filters: string) =>
+export const loadContainers = (filters: string, loadMounts: boolean = true) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
-        const { items } = await services.containerService.list({ filters });
+        let args: any = { filters };
+        if (!loadMounts) {
+            args.select = containerFieldsNoMounts;
+        }
+        const { items } = await services.containerService.list(args);
         dispatch<any>(updateResources(items));
         return items;
     };
 
+// Until the api supports unselecting fields, we need a list of all other fields to omit mounts
+const containerFieldsNoMounts = [
+    "auth_uuid",
+    "command",
+    "container_image",
+    "created_at",
+    "cwd",
+    "environment",
+    "etag",
+    "exit_code",
+    "finished_at",
+    "gateway_address",
+    "href",
+    "interactive_session_started",
+    "kind",
+    "lock_count",
+    "locked_by_uuid",
+    "log",
+    "modified_at",
+    "modified_by_client_uuid",
+    "modified_by_user_uuid",
+    "output_path",
+    "output_properties",
+    "output_storage_classes",
+    "output",
+    "owner_uuid",
+    "priority",
+    "progress",
+    "runtime_auth_scopes",
+    "runtime_constraints",
+    "runtime_status",
+    "runtime_user_uuid",
+    "scheduling_parameters",
+    "started_at",
+    "state",
+    "uuid",
+]
+
 export const cancelRunningWorkflow = (uuid: string) =>
     async (dispatch: Dispatch, getState: () => RootState, services: ServiceRepository) => {
         try {
index ccfa4fff9fcc4d0a34cef6a2af20f7388344a425..d8a5d82dc22b5444cbd4196abb18e8402b2b929c 100644 (file)
@@ -89,7 +89,8 @@ export const loadMissingProcessesInformation = (resources: GroupContentsResource
         }, []);
         if (containerUuids.length > 0) {
             await dispatch<any>(loadContainers(
-                new FilterBuilder().addIn('uuid', containerUuids).getFilters()
+                new FilterBuilder().addIn('uuid', containerUuids).getFilters(),
+                false
             ));
         }
     };
index 52abfd3fd2085ffb97c3361564ee73d1954421a2..a6e6748535596e26d09c9b3f948ebf1cabe186dd 100644 (file)
@@ -22,6 +22,8 @@ import { projectPanelActions } from 'store/project-panel/project-panel-action';
 import { GroupClass } from "models/group";
 import { Participant } from "views-components/sharing-dialog/participant-select";
 import { ProjectProperties } from "./project-create-actions";
+import { getResource } from "store/resources/resources";
+import { ProjectResource } from "models/project";
 
 export interface ProjectUpdateFormDialogData {
     uuid: string;
@@ -37,7 +39,9 @@ export const PROJECT_UPDATE_FORM_SELECTOR = formValueSelector(PROJECT_UPDATE_FOR
 
 export const openProjectUpdateDialog = (resource: ProjectUpdateFormDialogData) =>
     (dispatch: Dispatch, getState: () => RootState) => {
-        dispatch(initialize(PROJECT_UPDATE_FORM_NAME, resource));
+        // Get complete project resource from store to handle consumers passing in partial resources
+        const project = getResource<ProjectResource>(resource.uuid)(getState().resources);
+        dispatch(initialize(PROJECT_UPDATE_FORM_NAME, project));
         dispatch(dialogActions.OPEN_DIALOG({
             id: PROJECT_UPDATE_FORM_NAME,
             data: {
index fb9b0e8b01b4fcd50b9222ed3d46b23b3a12121e..dd303233117af7a74e77f8bfa34b381ac183ddfa 100644 (file)
@@ -24,6 +24,7 @@ import { ProcessStatusFilter, buildProcessStatusFilters } from '../resource-type
 import { ContainerRequestResource } from 'models/container-request';
 import { progressIndicatorActions } from '../progress-indicator/progress-indicator-actions';
 import { loadMissingProcessesInformation } from '../project-panel/project-panel-middleware-service';
+import { containerRequestFieldsNoMounts } from 'store/all-processes-panel/all-processes-panel-middleware-service';
 
 export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
     constructor(private services: ServiceRepository, id: string) {
@@ -40,7 +41,10 @@ export class SubprocessMiddlewareService extends DataExplorerMiddlewareService {
             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) });
+                {
+                    ...getParams(dataExplorer, parentContainerRequest) ,
+                    select: containerRequestFieldsNoMounts
+                });
 
             api.dispatch(progressIndicatorActions.PERSIST_STOP_WORKING(this.getId()));
             api.dispatch(updateResources(containerRequests.items));
index 55b2d31fab2202ded4367af0dd61bc80182cc351..f17d74c92ba79dd1fd6ad68dcecf69f8ab38c2c7 100644 (file)
@@ -7,7 +7,7 @@ import { ToggleFavoriteAction } from "../actions/favorite-action";
 import { toggleFavorite } from "store/favorites/favorites-actions";
 import {
     RenameIcon, ShareIcon, MoveToIcon, CopyIcon, DetailsIcon,
-    RemoveIcon, ReRunProcessIcon, InputIcon, OutputIcon, CommandIcon,
+    RemoveIcon, ReRunProcessIcon, OutputIcon,
     AdvancedIcon
 } from "components/icon/icon";
 import { favoritePanelActions } from "store/favorite-panel/favorite-panel-action";
@@ -18,9 +18,7 @@ import { openSharingDialog } from "store/sharing-dialog/sharing-dialog-actions";
 import { openRemoveProcessDialog, reRunProcess } from "store/processes/processes-actions";
 import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
 import { snackbarActions, SnackbarKind } from "store/snackbar/snackbar-actions";
-import { openProcessInputDialog } from "store/processes/process-input-actions";
 import { navigateToOutput } from "store/process-panel/process-panel-actions";
-import { openProcessCommandDialog } from "store/processes/process-command-actions";
 import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
 import { TogglePublicFavoriteAction } from "../actions/public-favorite-action";
 import { togglePublicFavorite } from "store/public-favorites/public-favorites-actions";
@@ -53,13 +51,6 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
             }
         }
     },
-    {
-        icon: InputIcon,
-        name: "Inputs",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProcessInputDialog(resource.uuid));
-        }
-    },
     {
         icon: OutputIcon,
         name: "Outputs",
@@ -69,13 +60,6 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
             }
         }
     },
-    {
-        icon: CommandIcon,
-        name: "Command",
-        execute: (dispatch, resource) => {
-            dispatch<any>(openProcessCommandDialog(resource.uuid));
-        }
-    },
     {
         icon: DetailsIcon,
         name: "View details",
@@ -135,4 +119,4 @@ export const processResourceAdminActionSet: ContextMenuActionSet = [[
             });
         }
     },
-]];
\ No newline at end of file
+]];
diff --git a/src/views-components/context-menu/action-sets/search-results-action-set.ts b/src/views-components/context-menu/action-sets/search-results-action-set.ts
new file mode 100644 (file)
index 0000000..e916a10
--- /dev/null
@@ -0,0 +1,42 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { ContextMenuActionSet } from "../context-menu-action-set";
+import { DetailsIcon, AdvancedIcon, OpenIcon, Link } from 'components/icon/icon';
+import { openAdvancedTabDialog } from "store/advanced-tab/advanced-tab";
+import { toggleDetailsPanel } from 'store/details-panel/details-panel-action';
+import { copyToClipboardAction, openInNewTabAction } from "store/open-in-new-tab/open-in-new-tab.actions";
+
+export const searchResultsActionSet: ContextMenuActionSet = [
+    [
+        {
+            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: "Advanced",
+            execute: (dispatch, resource) => {
+                dispatch<any>(openAdvancedTabDialog(resource.uuid));
+            }
+        },
+    ]
+];
index c34087400cf5223930b806b358163e470e60741a..50ed20fd7fb01d2dc0f0cacb8850bbc5bfcec71d 100644 (file)
@@ -6,12 +6,12 @@ import React from "react";
 import copy from 'copy-to-clipboard';
 import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
 import { Link } from "components/icon/icon";
-import { getClipboardUrl } from "./helpers";
+import { getCollectionItemClipboardUrl } from "./helpers";
 
 export const CopyToClipboardAction = (props: { href?: any, download?: any, onClick?: () => void, kind?: string, currentCollectionUuid?: string; }) => {
     const copyToClipboard = () => {
         if (props.href) {
-            const clipboardUrl = getClipboardUrl(props.href, true, true);
+            const clipboardUrl = getCollectionItemClipboardUrl(props.href, true, true);
             copy(clipboardUrl);
         }
 
index b3b7f7f8dab5208cf05047d3322061e4a44d2fa6..7776d0e5aacdf976fe081ef9cbb964dd44e04b31 100644 (file)
@@ -2,7 +2,7 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
-import { sanitizeToken, getClipboardUrl, getInlineFileUrl } from "./helpers";
+import { sanitizeToken, getCollectionItemClipboardUrl, getInlineFileUrl } from "./helpers";
 
 describe('helpers', () => {
     // given
@@ -22,7 +22,7 @@ describe('helpers', () => {
     describe('getClipboardUrl', () => {
         it('should add redirectTo query param', () => {
             // when
-            const result = getClipboardUrl(url);
+            const result = getCollectionItemClipboardUrl(url);
 
             // then
             expect(result).toBe('http://localhost?redirectToDownload=https://example.com/c=zzzzz-4zz18-0123456789abcde/LIMS/1.html');
index f196074d7e8770dde905e653be6e356c305ceaa5..9140e457afef89affcb573f0c84303640d4014e0 100644 (file)
@@ -14,7 +14,10 @@ export const sanitizeToken = (href: string, tokenAsQueryParam = true): string =>
     return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `${sep}api_token=${token}` : ''}`;
 };
 
-export const getClipboardUrl = (href: string, shouldSanitizeToken = true, inline = false): string => {
+/**
+ * @returns A shareable token-free WB2 url that redirects to keep-web after login
+ */
+export const getCollectionItemClipboardUrl = (href: string, shouldSanitizeToken = true, inline = false): string => {
     const { origin } = window.location;
     const url = shouldSanitizeToken ? sanitizeToken(href, false) : href;
     const redirectKey = inline ? REDIRECT_TO_PREVIEW_KEY : REDIRECT_TO_DOWNLOAD_KEY;
index f89a17809ad668314f2d075c10a203ecf11ded2b..c659b7c508a7fd7af4cc2887742aabd4a773edc5 100644 (file)
@@ -113,4 +113,5 @@ export enum ContextMenuKind {
     PERMISSION_EDIT = "PermissionEdit",
     LINK = "Link",
     WORKFLOW = "Workflow",
+    SEARCH_RESULTS = "SearchResults"
 }
index aac1364207420247b0e620b3ee3b2679cda9d08e..3aa9e3f25aeb8ed1c6646765051596cb16ff00dd 100644 (file)
@@ -12,6 +12,7 @@ import { AxiosPromise } from 'axios';
 import { DispatchProp } from 'react-redux';
 import { saveApiToken } from 'store/auth/auth-action';
 import { navigateToRootProject } from 'store/navigation/navigation-action';
+import { replace } from 'react-router-redux';
 
 type CssRules = 'root' | 'loginBtn' | 'card' | 'wrapper' | 'progress';
 
@@ -87,8 +88,11 @@ export const LoginForm = withStyles(styles)(
                 setSubmitting(false);
                 if (response.data.uuid && response.data.api_token) {
                     const apiToken = `v2/${response.data.uuid}/${response.data.api_token}`;
+                    const rd = new URL(window.location.href);
+                    const rdUrl = rd.pathname + rd.search;
                     dispatch<any>(saveApiToken(apiToken)).finally(
-                        () => dispatch(navigateToRootProject));
+                        () => rdUrl === '/' ? dispatch(navigateToRootProject) : dispatch(replace(rdUrl))
+                    );
                 } else {
                     setError(true);
                     setHelperText(response.data.message || 'Please try again');
diff --git a/src/views-components/process-command-dialog/process-command-dialog.tsx b/src/views-components/process-command-dialog/process-command-dialog.tsx
deleted file mode 100644 (file)
index 7695837..0000000
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-import React from "react";
-import { Dialog, DialogActions, Button, StyleRulesCallback, WithStyles, withStyles, Tooltip, IconButton, CardHeader } from '@material-ui/core';
-import { withDialog } from "store/dialog/with-dialog";
-import { PROCESS_COMMAND_DIALOG_NAME } from 'store/processes/process-command-actions';
-import { WithDialogProps } from 'store/dialog/with-dialog';
-import { ProcessCommandDialogData } from 'store/processes/process-command-actions';
-import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
-import { compose } from 'redux';
-import CopyToClipboard from "react-copy-to-clipboard";
-import { CopyIcon } from 'components/icon/icon';
-
-type CssRules = 'codeSnippet' | 'copyToClipboard';
-
-const styles: StyleRulesCallback<CssRules> = theme => ({
-    codeSnippet: {
-        marginLeft: theme.spacing.unit * 3,
-        marginRight: theme.spacing.unit * 3,
-    },
-    copyToClipboard: {
-        marginRight: theme.spacing.unit,
-    }
-});
-
-export const ProcessCommandDialog = compose(
-    withDialog(PROCESS_COMMAND_DIALOG_NAME),
-    withStyles(styles),
-)(
-    (props: WithDialogProps<ProcessCommandDialogData> & WithStyles<CssRules>) =>
-        <Dialog
-            open={props.open}
-            maxWidth="md"
-            onClose={props.closeDialog}
-            style={{ alignSelf: 'stretch' }}>
-            <CardHeader
-                title={`Command - ${props.data.processName}`}
-                action={
-                    <Tooltip title="Copy to clipboard">
-                        <CopyToClipboard text={props.data.command}>
-                            <IconButton className={props.classes.copyToClipboard}>
-                                <CopyIcon />
-                            </IconButton>
-                        </CopyToClipboard>
-                    </Tooltip>
-                } />
-            <DefaultCodeSnippet
-                className={props.classes.codeSnippet}
-                lines={[props.data.command]} />
-            <DialogActions>
-                <Button
-                    variant='text'
-                    color='primary'
-                    onClick={props.closeDialog}>
-                    Close
-                </Button>
-            </DialogActions>
-        </Dialog>
-);
\ No newline at end of file
index 3858e49e2372be08549f81816327138ffb09c046..3b5fae3639591a224a6860e24ab57921afa3fbf0 100644 (file)
@@ -7,6 +7,7 @@ import {
     ExpansionPanel,
     ExpansionPanelDetails,
     ExpansionPanelSummary,
+    Paper,
     StyleRulesCallback,
     Typography,
     withStyles,
@@ -26,7 +27,8 @@ type CssRules = 'root'
     | 'error'
     | 'errorColor'
     | 'warning'
-    | 'warningColor';
+    | 'warningColor'
+    | 'paperRoot';
 
 const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     root: {
@@ -65,15 +67,21 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
     warningColor: {
         color: theme.customs.colors.yellow900,
     },
+    paperRoot: {
+        minHeight: theme.spacing.unit * 6,
+        display: 'flex',
+        alignItems: 'center',
+    },
 });
 export interface ProcessRuntimeStatusDataProps {
     runtimeStatus: RuntimeStatus | undefined;
+    containerCount: number;
 }
 
 type ProcessRuntimeStatusProps = ProcessRuntimeStatusDataProps & WithStyles<CssRules>;
 
 export const ProcessRuntimeStatus = withStyles(styles)(
-    ({ runtimeStatus, classes }: ProcessRuntimeStatusProps) => {
+    ({ runtimeStatus, containerCount, classes }: ProcessRuntimeStatusProps) => {
     return <div className={classes.root}>
         { runtimeStatus?.error &&
         <div data-cy='process-runtime-status-error'><ExpansionPanel className={classes.error} elevation={0}>
@@ -103,5 +111,14 @@ export const ProcessRuntimeStatus = withStyles(styles)(
             </ExpansionPanelDetails>
         </ExpansionPanel></div>
         }
+        { containerCount > 1 &&
+        <div data-cy='process-runtime-status-retry-warning' >
+            <Paper className={classNames(classes.warning, classes.paperRoot)} elevation={0}>
+                <Typography className={classNames(classes.heading, classes.summary, classes.warningColor)}>
+                    {`Warning: Process retried ${containerCount - 1} time${containerCount > 2 ? 's' : ''} due to failure.`}
+                </Typography>
+            </Paper>
+        </div>
+        }
     </div>
-});
\ No newline at end of file
+});
index a7b5825244abc7c2e859e676db49a32321fa2b67..47d7729b4dc1a0d16e935968f47aa22685d0fdc4 100644 (file)
@@ -38,7 +38,7 @@ ResourcePropertiesListActionProps & WithStyles<CssRules>;
 
 const List = withStyles(styles)(
     ({ classes, handleDelete, properties }: ResourcePropertiesListProps) =>
-        <div>
+        <div data-cy="resource-properties-list">
             {properties &&
                 Object.keys(properties).map(k =>
                     Array.isArray(properties[k])
@@ -63,4 +63,4 @@ export const resourcePropertiesList = (formName: string) =>
         (dispatch: Dispatch): ResourcePropertiesListActionProps => ({
                 handleDelete: (key: string, value: string) => dispatch<any>(removePropertyFromResourceForm(key, value, formName))
         })
-    )(List);
\ No newline at end of file
+    )(List);
index d2ff77e3d41777b1dad76806c6150b29037e8d64..400bb1e68724d88d58b0f42819d930755432dc1f 100644 (file)
@@ -16,6 +16,8 @@ import { mount, configure } from 'enzyme';
 import Adapter from 'enzyme-adapter-react-16';
 import CopyToClipboard from 'react-copy-to-clipboard';
 import { TokenDialogComponent } from './token-dialog';
+import { combineReducers, createStore } from 'redux';
+import { Provider } from 'react-redux';
 
 configure({ adapter: new Adapter() });
 
@@ -24,6 +26,7 @@ jest.mock('toggle-selection', () => () => () => null);
 describe('<CurrentTokenDialog />', () => {
   let props;
   let wrapper;
+  let store;
 
   beforeEach(() => {
     props = {
@@ -33,11 +36,25 @@ describe('<CurrentTokenDialog />', () => {
       open: true,
       dispatch: jest.fn(),
     };
+
+    const initialAuthState = {
+      localCluster: "zzzzz",
+      remoteHostsConfig: {},
+      sessions: {},
+    };
+
+    store = createStore(combineReducers({
+      auth: (state: any = initialAuthState, action: any) => state,
+    }));
   });
 
   describe('Get API Token dialog', () => {
     beforeEach(() => {
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>
+      );
     });
 
     it('should include API host and token', () => {
@@ -51,7 +68,10 @@ describe('<CurrentTokenDialog />', () => {
 
       const someDate = '2140-01-01T00:00:00.000Z'
       props.tokenExpiration = new Date(someDate);
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
       expect(wrapper.html()).toContain(props.tokenExpiration.toLocaleString());
     });
 
@@ -60,14 +80,20 @@ describe('<CurrentTokenDialog />', () => {
       expect(wrapper.html()).not.toContain('GET NEW TOKEN');
 
       props.canCreateNewTokens = true;
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
       expect(wrapper.html()).toContain('GET NEW TOKEN');
     });
   });
 
   describe('copy to clipboard button', () => {
     beforeEach(() => {
-      wrapper = mount(<TokenDialogComponent {...props} />);
+      wrapper = mount(
+        <Provider store={store}>
+          <TokenDialogComponent {...props} />
+        </Provider>);
     });
 
     it('should copy API TOKEN to the clipboard', () => {
diff --git a/src/views/process-panel/process-cmd-card.tsx b/src/views/process-panel/process-cmd-card.tsx
new file mode 100644 (file)
index 0000000..4143501
--- /dev/null
@@ -0,0 +1,133 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import React from 'react';
+import {
+    StyleRulesCallback,
+    WithStyles,
+    withStyles,
+    Card,
+    CardHeader,
+    IconButton,
+    CardContent,
+    Tooltip,
+    Typography,
+    Grid,
+} from '@material-ui/core';
+import { ArvadosTheme } from 'common/custom-theme';
+import { CloseIcon, CommandIcon, CopyIcon } from 'components/icon/icon';
+import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
+import { DefaultCodeSnippet } from 'components/default-code-snippet/default-code-snippet';
+import { Process } from 'store/processes/process';
+import shellescape from 'shell-escape';
+import CopyToClipboard from 'react-copy-to-clipboard';
+
+type CssRules = 'card' | 'content' | 'title' | 'header' | 'avatar' | 'iconHeader';
+
+const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
+    card: {
+        height: '100%'
+    },
+    header: {
+        paddingTop: theme.spacing.unit,
+        paddingBottom: theme.spacing.unit,
+    },
+    iconHeader: {
+        fontSize: '1.875rem',
+        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,
+        }
+    },
+    title: {
+        overflow: 'hidden',
+        paddingTop: theme.spacing.unit * 0.5
+    },
+});
+
+interface ProcessCmdCardDataProps {
+  process: Process;
+  onCopy: (text: string) => void;
+}
+
+type ProcessCmdCardProps = ProcessCmdCardDataProps & WithStyles<CssRules> & MPVPanelProps;
+
+export const ProcessCmdCard = withStyles(styles)(
+  ({
+    process,
+    onCopy,
+    classes,
+    doHidePanel,
+  }: ProcessCmdCardProps) => {
+    const command = process.containerRequest.command.map((v) =>
+      shellescape([v]) // Escape each arg separately
+    );
+
+    let formattedCommand = [...command];
+    formattedCommand.forEach((item, i, arr) => {
+      // Indent lines after the first
+      const indent = i > 0 ? '  ' : '';
+      // Escape newlines on every non-last arg when there are multiple lines
+      const lineBreak = arr.length > 1 && i < arr.length - 1 ? ' \\' : '';
+      arr[i] = `${indent}${item}${lineBreak}`;
+    });
+
+    return (
+      <Card className={classes.card}>
+        <CardHeader
+          className={classes.header}
+          classes={{
+            content: classes.title,
+            avatar: classes.avatar,
+          }}
+          avatar={<CommandIcon className={classes.iconHeader} />}
+          title={
+            <Typography noWrap variant="h6" color="inherit">
+              Command
+            </Typography>
+          }
+          action={
+            <Grid container direction="row" alignItems="center">
+              <Grid item>
+                <Tooltip title="Copy to clipboard" disableFocusListener>
+                  <IconButton>
+                    <CopyToClipboard
+                      text={command.join(" ")}
+                      onCopy={() => onCopy("Command copied to clipboard")}
+                    >
+                      <CopyIcon />
+                    </CopyToClipboard>
+                  </IconButton>
+                </Tooltip>
+              </Grid>
+              <Grid item>
+                {doHidePanel && (
+                  <Tooltip
+                    title={`Close Command Panel`}
+                    disableFocusListener
+                  >
+                    <IconButton onClick={doHidePanel}>
+                      <CloseIcon />
+                    </IconButton>
+                  </Tooltip>
+                )}
+              </Grid>
+            </Grid>
+          }
+        />
+        <CardContent className={classes.content}>
+          <DefaultCodeSnippet lines={formattedCommand} linked />
+        </CardContent>
+      </Card>
+    );
+  }
+);
index 1e3e5591e78341a4253ba2eb2442537b9923c70e..4af2c9cda75392cfd45f8707206913a5d56e3d95 100644 (file)
@@ -64,7 +64,7 @@ export const ProcessDetailsAttributes = withStyles(styles, { withTheme: true })(
             const mdSize = props.twoCol ? 6 : 12;
             return <Grid container>
                 <Grid item xs={12}>
-                    <ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} />
+                    <ProcessRuntimeStatus runtimeStatus={container?.runtimeStatus} containerCount={containerRequest.containerCount} />
                 </Grid>
                 {!props.hideProcessPanelRedundantFields && <Grid item xs={12} md={mdSize}>
                     <DetailsAttribute label='Type' value={resourceLabel(ResourceKind.PROCESS)} />
index ac409ec1873a25db7d039f5c779b665fcca09545..936b31a5497612999aab5a340ab33cf8bd7b04f4 100644 (file)
@@ -24,7 +24,8 @@ import {
     MaximizeIcon,
     TextDecreaseIcon,
     TextIncreaseIcon,
-    WordWrapIcon
+    WordWrapOffIcon,
+    WordWrapOnIcon,
 } from 'components/icon/icon';
 import { Process } from 'store/processes/process';
 import { MPVPanelProps } from 'components/multi-panel-view/multi-panel-view';
@@ -92,7 +93,7 @@ export const ProcessLogsCard = withStyles(styles)(
     ({ classes, process, filters, selectedFilter, lines,
         onLogFilterChange, navigateToLog, onCopy,
         doHidePanel, doMaximizePanel, panelMaximized, panelName }: ProcessLogsCardProps) => {
-        const [wordWrapToggle, setWordWrapToggle] = useState<boolean>(true);
+        const [wordWrap, setWordWrap] = useState<boolean>(true);
         const [fontSize, setFontSize] = useState<number>(3);
         const fontBaseSize = 10;
         const fontStepSize = 1;
@@ -130,9 +131,9 @@ export const ProcessLogsCard = withStyles(styles)(
                             </Tooltip>
                         </Grid>
                         <Grid item>
-                            <Tooltip title="Toggle word wrapping" disableFocusListener>
-                                <IconButton onClick={() => setWordWrapToggle(!wordWrapToggle)}>
-                                    <WordWrapIcon />
+                            <Tooltip title={`${wordWrap ? 'Disable' : 'Enable'} word wrapping`} disableFocusListener>
+                                <IconButton onClick={() => setWordWrap(!wordWrap)}>
+                                    {wordWrap ? <WordWrapOffIcon /> : <WordWrapOnIcon />}
                                 </IconButton>
                             </Tooltip>
                         </Grid>
@@ -166,7 +167,7 @@ export const ProcessLogsCard = withStyles(styles)(
                             spacing={24}
                             direction='column'>
                             <Grid className={classes.logViewer} item xs>
-                                <ProcessLogCodeSnippet fontSize={fontBaseSize+(fontStepSize*fontSize)} wordWrap={wordWrapToggle} lines={lines} />
+                                <ProcessLogCodeSnippet fontSize={fontBaseSize+(fontStepSize*fontSize)} wordWrap={wordWrap} lines={lines} />
                             </Grid>
                         </Grid>
                         : <DefaultView
index 6ea628e6b2192b7eaf1d80e2f54578eb69a4b44e..2b7391c294ec0ac2ffd120fac25085b00b574e5c 100644 (file)
@@ -13,10 +13,12 @@ import {
 import grey from '@material-ui/core/colors/grey';
 import { ArvadosTheme } from 'common/custom-theme';
 import { Link, Typography } from '@material-ui/core';
-import { navigateTo } from 'store/navigation/navigation-action';
+import { navigationNotAvailable } from 'store/navigation/navigation-action';
 import { Dispatch } from 'redux';
 import { connect, DispatchProp } from 'react-redux';
 import classNames from 'classnames';
+import { FederationConfig, getNavUrl } from 'routes/routes';
+import { RootState } from 'store/store';
 
 type CssRules = 'root' | 'wordWrap' | 'logText';
 
@@ -26,6 +28,9 @@ const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
         overflow: 'auto',
         backgroundColor: '#000',
         height: `calc(100% - ${theme.spacing.unit * 4}px)`, // so that horizontal scollbar is visible
+        "& a": {
+            color: theme.palette.primary.main,
+        },
     },
     logText: {
         padding: theme.spacing.unit * 0.5,
@@ -55,7 +60,11 @@ interface ProcessLogCodeSnippetProps {
     wordWrap?: boolean;
 }
 
-const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) => {
+interface ProcessLogCodeSnippetAuthProps {
+    auth: FederationConfig;
+}
+
+const renderLinks = (fontSize: number, auth: FederationConfig, dispatch: Dispatch) => (text: string) => {
     // Matches UUIDs & PDHs
     const REGEX = /[a-z0-9]{5}-[a-z0-9]{5}-[a-z0-9]{15}|[0-9a-f]{32}\+\d+/g;
     const links = text.match(REGEX);
@@ -67,7 +76,14 @@ const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) =>
         <React.Fragment key={index}>
             {part}
             {links[index] &&
-            <Link onClick={() => dispatch<any>(navigateTo(links[index]))}
+            <Link onClick={() => {
+                const url = getNavUrl(links[index], auth)
+                if (url) {
+                    window.open(`${window.location.origin}${url}`, '_blank');
+                } else {
+                    dispatch(navigationNotAvailable(links[index]));
+                }
+            }}
                 style={ {cursor: 'pointer'} }>
                 {links[index]}
             </Link>}
@@ -76,9 +92,13 @@ const renderLinks = (fontSize: number, dispatch: Dispatch) => (text: string) =>
     </Typography>;
 };
 
-export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
-    ({classes, lines, fontSize, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & DispatchProp) => {
-        const [followMode, setFollowMode] = useState<boolean>(false);
+const mapStateToProps = (state: RootState): ProcessLogCodeSnippetAuthProps => ({
+    auth: state.auth,
+});
+
+export const ProcessLogCodeSnippet = withStyles(styles)(connect(mapStateToProps)(
+    ({classes, lines, fontSize, auth, dispatch, wordWrap}: ProcessLogCodeSnippetProps & WithStyles<CssRules> & ProcessLogCodeSnippetAuthProps & DispatchProp) => {
+        const [followMode, setFollowMode] = useState<boolean>(true);
         const scrollRef = useRef<HTMLDivElement>(null);
 
         useEffect(() => {
@@ -92,7 +112,7 @@ export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
             <div ref={scrollRef} className={classes.root}
                 onScroll={(e) => {
                     const elem = e.target as HTMLDivElement;
-                    if (elem.scrollTop + elem.clientHeight >= elem.scrollHeight) {
+                    if (elem.scrollTop + (elem.clientHeight*1.1) >= elem.scrollHeight) {
                         setFollowMode(true);
                     } else {
                         setFollowMode(false);
@@ -101,7 +121,7 @@ export const ProcessLogCodeSnippet = withStyles(styles)(connect()(
                 { lines.map((line: string, index: number) =>
                 <Typography key={index} component="pre"
                     className={classNames(classes.logText, wordWrap ? classes.wordWrap : undefined)}>
-                    {renderLinks(fontSize, dispatch)(line)}
+                    {renderLinks(fontSize, auth, dispatch)(line)}
                 </Typography>
                 ) }
             </div>
index 4f95d0d88726f1b4354138946fb113ae749d786c..f8ff84304dcb3fb4acc7554ef0f26882ef9cc6d6 100644 (file)
@@ -15,6 +15,7 @@ import { ProcessDetailsCard } from './process-details-card';
 import { getProcessPanelLogs, ProcessLogsPanel } from 'store/process-logs-panel/process-logs-panel';
 import { ProcessLogsCard } from './process-log-card';
 import { FilterOption } from 'views/process-panel/process-log-form';
+import { ProcessCmdCard } from './process-cmd-card';
 
 type CssRules = 'root';
 
@@ -37,13 +38,14 @@ export interface ProcessPanelRootActionProps {
     cancelProcess: (uuid: string) => void;
     onLogFilterChange: (filter: FilterOption) => void;
     navigateToLog: (uuid: string) => void;
-    onLogCopyToClipboard: (uuid: string) => void;
+    onCopyToClipboard: (uuid: string) => void;
 }
 
 export type ProcessPanelRootProps = ProcessPanelRootDataProps & ProcessPanelRootActionProps & WithStyles<CssRules>;
 
 const panelsData: MPVPanelState[] = [
     {name: "Details"},
+    {name: "Command"},
     {name: "Logs", visible: true},
     {name: "Subprocesses"},
 ];
@@ -59,9 +61,14 @@ export const ProcessPanelRoot = withStyles(styles)(
                     cancelProcess={props.cancelProcess}
                 />
             </MPVPanelContent>
+            <MPVPanelContent forwardProps xs="auto" data-cy="process-cmd">
+                <ProcessCmdCard
+                    onCopy={props.onCopyToClipboard}
+                    process={process} />
+            </MPVPanelContent>
             <MPVPanelContent forwardProps xs maxHeight='50%' data-cy="process-logs">
                 <ProcessLogsCard
-                    onCopy={props.onLogCopyToClipboard}
+                    onCopy={props.onCopyToClipboard}
                     process={process}
                     lines={getProcessPanelLogs(processLogsPanel)}
                     selectedFilter={{
index de6b13b3163c7f2ffaebb2181f8e235b73dbc416..7afaa04d94b95f90e47181f2a449985c0879fd62 100644 (file)
@@ -36,7 +36,7 @@ const mapStateToProps = ({ router, resources, processPanel, processLogsPanel }:
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): ProcessPanelRootActionProps => ({
-    onLogCopyToClipboard: (message: string) => {
+    onCopyToClipboard: (message: string) => {
         dispatch<any>(snackbarActions.OPEN_SNACKBAR({
             message,
             hideDuration: 2000,
index d25682f6e1d8b59a07d4c91c4e8a469bd119504b..0902f15bdcca963a77369b970ff0e2d959cb31f0 100644 (file)
@@ -5,8 +5,7 @@
 import { Dispatch } from "redux";
 import { connect } from "react-redux";
 import { navigateTo } from 'store/navigation/navigation-action';
-// import { openContextMenu, resourceKindToContextMenuKind } from 'store/context-menu/context-menu-actions';
-// import { ResourceKind } from 'models/resource';
+import { openSearchResultsContextMenu } from 'store/context-menu/context-menu-actions';
 import { loadDetailsPanel } from 'store/details-panel/details-panel-action';
 import { SearchResultsPanelView } from 'views/search-results-panel/search-results-panel-view';
 import { RootState } from 'store/store';
@@ -42,7 +41,9 @@ const mapStateToProps = (rootState: RootState) => {
 };
 
 const mapDispatchToProps = (dispatch: Dispatch): SearchResultsPanelActionProps => ({
-    onContextMenu: (event, resourceUuid) => { return; },
+    onContextMenu: (event, resourceUuid) => {
+        dispatch<any>(openSearchResultsContextMenu(event, resourceUuid));
+    },
     onDialogOpen: (ownerUuid: string) => { return; },
     onItemClick: (resourceUuid: string) => {
         dispatch<any>(loadDetailsPanel(resourceUuid));
index 28fae4cd6b1b5a4b7a668d798c55b3271ad4c9f5..a6c49e348495e6f48a21e1d8a99e6a78016a1e83 100644 (file)
@@ -33,7 +33,6 @@ import { MoveProjectDialog } from 'views-components/dialog-forms/move-project-di
 import { MoveCollectionDialog } from 'views-components/dialog-forms/move-collection-dialog';
 import { FilesUploadCollectionDialog } from 'views-components/dialog-forms/files-upload-collection-dialog';
 import { PartialCopyCollectionDialog } from 'views-components/dialog-forms/partial-copy-collection-dialog';
-import { ProcessCommandDialog } from 'views-components/process-command-dialog/process-command-dialog';
 import { RemoveProcessDialog } from 'views-components/process-remove-dialog/process-remove-dialog';
 import { MainContentBar } from 'views-components/main-content-bar/main-content-bar';
 import { Grid } from '@material-ui/core';
@@ -241,7 +240,6 @@ export const WorkbenchPanel =
             <PublicKeyDialog />
             <PartialCopyCollectionDialog />
             <PartialCopyToCollectionDialog />
-            <ProcessCommandDialog />
             <ProcessInputDialog />
             <RestoreCollectionVersionDialog />
             <RemoveApiClientAuthorizationDialog />
index 0a9a0fc42adcf369da404fee334726d4b46bb05e..367ccecd3512d90f261adc30b85405aac3f6176a 100755 (executable)
@@ -114,6 +114,7 @@ coproc arvboot (~/go/bin/arvados-server boot \
     -type test \
     -config ${TMPDIR}/arvados.yml \
     -no-workbench1 \
+    -no-workbench2 \
     -own-temporary-database \
     -timeout 20m 2> ${ARVADOS_LOG})
 trap cleanup ERR EXIT
index 13ea553a0166a9b05bed5c5af8897dda21efab20..6dfb5b18b8aae518b209c72c9888a5fbb65d00cc 100644 (file)
--- a/yarn.lock
+++ b/yarn.lock
@@ -2817,10 +2817,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"@types/shell-quote@npm:1.6.0":
-  version: 1.6.0
-  resolution: "@types/shell-quote@npm:1.6.0"
-  checksum: 5d9f4e35c8df32d9994f8ae2f1a1fe8a6b7ee96794f803e0904ceae7ad7255a214954e85cd75bd847fe77458d3746430522e87237438f223b7d72a23c4928c0e
+"@types/shell-escape@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "@types/shell-escape@npm:0.2.0"
+  checksum: 020696ed313eeb02deb2abcc581e8b570be6f9ee662892339965b524bb4fbdc9a97b6520d914117740ec11147b0b1aa52358b8e03fa214c2da99743adb196853
   languageName: node
   linkType: hard
 
@@ -3600,13 +3600,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-filter@npm:~0.0.0":
-  version: 0.0.1
-  resolution: "array-filter@npm:0.0.1"
-  checksum: 0e9afdf5e248c45821c6fe1232071a13a3811e1902c2c2a39d12e4495e8b0b25739fd95bffbbf9884b9693629621f6077b4ae16207b8f23d17710fc2465cebbb
-  languageName: node
-  linkType: hard
-
 "array-find-index@npm:^1.0.1":
   version: 1.0.2
   resolution: "array-find-index@npm:1.0.2"
@@ -3648,20 +3641,6 @@ __metadata:
   languageName: node
   linkType: hard
 
-"array-map@npm:~0.0.0":
-  version: 0.0.0
-  resolution: "array-map@npm:0.0.0"
-  checksum: 30d73fdc99956c8bd70daea40db5a7d78c5c2c75a03c64fc77904885e79adf7d5a0595076534f4e58962d89435f0687182ac929e65634e3d19931698cbac8149
-  languageName: node
-  linkType: hard
-
-"array-reduce@npm:~0.0.0":
-  version: 0.0.0
-  resolution: "array-reduce@npm:0.0.0"
-  checksum: d6226325271f477e3dd65b4d40db8597735b8d08bebcca4972e52d3c173d6c697533664fa8865789ea2d076bdaf1989bab5bdfbb61598be92074a67f13057c3a
-  languageName: node
-  linkType: hard
-
 "array-union@npm:^1.0.1":
   version: 1.0.2
   resolution: "array-union@npm:1.0.2"
@@ -3769,7 +3748,7 @@ __metadata:
     "@types/redux-devtools": 3.0.44
     "@types/redux-form": 7.4.12
     "@types/redux-mock-store": 1.0.2
-    "@types/shell-quote": 1.6.0
+    "@types/shell-escape": ^0.2.0
     "@types/sinon": 7.5
     "@types/uuid": 3.4.4
     axios: ^0.21.1
@@ -3829,7 +3808,7 @@ __metadata:
     redux-thunk: 2.3.0
     reselect: 4.0.0
     set-value: 2.0.1
-    shell-quote: 1.6.1
+    shell-escape: ^0.2.0
     sinon: 7.3
     ts-mock-imports: 1.3.7
     tslint: 5.20.0
@@ -16502,15 +16481,10 @@ __metadata:
   languageName: node
   linkType: hard
 
-"shell-quote@npm:1.6.1":
-  version: 1.6.1
-  resolution: "shell-quote@npm:1.6.1"
-  dependencies:
-    array-filter: ~0.0.0
-    array-map: ~0.0.0
-    array-reduce: ~0.0.0
-    jsonify: ~0.0.0
-  checksum: 982a4fdf2d474f0dc40885de4222f100ba457d7c75d46b532bf23b01774b8617bc62522c6825cb1fa7dd4c54c18e9dcbae7df2ca8983101841b6f2e6a7cacd2f
+"shell-escape@npm:^0.2.0":
+  version: 0.2.0
+  resolution: "shell-escape@npm:0.2.0"
+  checksum: 0d87f1ae22ad22a74e148348ceaf64721e3024f83c90afcfb527318ce10ece654dd62e103dd89a242f2f4e4ce3cecdef63e3d148c40e5fabca8ba6c508f97d9f
   languageName: node
   linkType: hard