[arvados-workbench2] created: 2.4.2-3-g252d5bb1

git repository hosting git at public.arvados.org
Tue Sep 20 14:40:57 UTC 2022


        at  252d5bb166c13ed3094777a6f4981fce05c5b193 (commit)


commit 252d5bb166c13ed3094777a6f4981fce05c5b193
Author: Stephen Smith <stephen at curii.com>
Date:   Tue Sep 6 09:56:26 2022 -0400

    Merge branch '19383-advanced-dialog' into main. Closes #19383
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/project.spec.js b/cypress/integration/project.spec.js
index b2f6f33d..9c5e791c 100644
--- a/cypress/integration/project.spec.js
+++ b/cypress/integration/project.spec.js
@@ -301,7 +301,7 @@ describe('Project tests', function() {
 
                     cy.get('main').contains(projectName).rightclick();
 
-                    cy.get('[data-cy=context-menu]').contains('Advanced').click();
+                    cy.get('[data-cy=context-menu]').contains('API Details').click();
 
                     cy.get('[role=tablist]').contains('METADATA').click();
 
diff --git a/cypress/integration/search.spec.js b/cypress/integration/search.spec.js
index 2216c067..da33c7df 100644
--- a/cypress/integration/search.spec.js
+++ b/cypress/integration/search.spec.js
@@ -241,7 +241,7 @@ describe('Search tests', function() {
             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('API Details');
                 cy.contains('Copy to clipboard');
                 cy.contains('Open in new tab');
                 cy.contains('View details');
diff --git a/cypress/integration/user-profile.spec.js b/cypress/integration/user-profile.spec.js
index 7d21249c..d91dbb0b 100644
--- a/cypress/integration/user-profile.spec.js
+++ b/cypress/integration/user-profile.spec.js
@@ -76,7 +76,7 @@ describe('User profile tests', function() {
     }) {
         cy.get('[data-cy=user-profile-panel-options-btn]').click();
         cy.get('[data-cy=context-menu]').within(() => {
-            cy.get('[role=button]').contains('Advanced');
+            cy.get('[role=button]').contains('API Details');
 
             cy.get('[role=button]').should(account ? 'contain' : 'not.contain', 'Account Settings');
             cy.get('[role=button]').should(activate ? 'contain' : 'not.contain', 'Activate User');
diff --git a/src/components/code-snippet/code-snippet.tsx b/src/components/code-snippet/code-snippet.tsx
index 83c378b8..5a5a7041 100644
--- a/src/components/code-snippet/code-snippet.tsx
+++ b/src/components/code-snippet/code-snippet.tsx
@@ -30,6 +30,7 @@ export interface CodeSnippetDataProps {
     className?: string;
     apiResponse?: boolean;
     linked?: boolean;
+    children?: JSX.Element;
 }
 
 interface CodeSnippetAuthProps {
@@ -43,11 +44,12 @@ const mapStateToProps = (state: RootState): CodeSnippetAuthProps => ({
 });
 
 export const CodeSnippet = withStyles(styles)(connect(mapStateToProps)(
-    ({ classes, lines, linked, className, apiResponse, dispatch, auth }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
+    ({ classes, lines, linked, className, apiResponse, dispatch, auth, children }: CodeSnippetProps & CodeSnippetAuthProps & DispatchProp) =>
         <Typography
         component="div"
         className={classNames(classes.root, className)}>
             <Typography className={apiResponse ? classes.space : className} component="pre">
+                {children}
                 {linked ?
                     lines.map((line, index) => <React.Fragment key={index}>{renderLinks(auth, dispatch)(line)}{`\n`}</React.Fragment>) :
                     lines.join('\n')
diff --git a/src/store/advanced-tab/advanced-tab.tsx b/src/store/advanced-tab/advanced-tab.tsx
index 61fd705a..ac088f02 100644
--- a/src/store/advanced-tab/advanced-tab.tsx
+++ b/src/store/advanced-tab/advanced-tab.tsx
@@ -26,8 +26,9 @@ import React from 'react';
 
 export const ADVANCED_TAB_DIALOG = 'advancedTabDialog';
 
-interface AdvancedTabDialogData {
-    apiResponse: any;
+export interface AdvancedTabDialogData {
+    uuid: string;
+    apiResponse: JSX.Element;
     metadata: ListResults<LinkResource> | string;
     user: UserResource | string;
     pythonHeader: string;
@@ -290,7 +291,7 @@ interface AdvancedTabData {
     uuid: string;
     metadata: ListResults<LinkResource> | string;
     user: UserResource | string;
-    apiResponseKind: any;
+    apiResponseKind: (apiResponse) => JSX.Element;
     data: AdvanceResponseData;
     resourceKind: AdvanceResourceKind;
     resourcePrefix: AdvanceResourcePrefix;
@@ -370,7 +371,7 @@ const stringify = (item: string | null | number | boolean) =>
 const stringifyObject = (item: any) =>
     JSON.stringify(item, null, 2) || 'null';
 
-const containerRequestApiResponse = (apiResponse: ContainerRequestResource) => {
+const containerRequestApiResponse = (apiResponse: ContainerRequestResource): JSX.Element => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, state, requestingContainerUuid, containerUuid,
         containerCountMax, mounts, runtimeConstraints, containerImage, environment, cwd, command, outputPath, priority, expiresAt, filters, containerCount,
         useExisting, schedulingParameters, outputUuid, logUuid, outputName, outputTtl } = apiResponse;
@@ -409,7 +410,7 @@ const containerRequestApiResponse = (apiResponse: ContainerRequestResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const collectionApiResponse = (apiResponse: CollectionResource) => {
+const collectionApiResponse = (apiResponse: CollectionResource): JSX.Element => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, properties, portableDataHash, replicationDesired,
         replicationConfirmedAt, replicationConfirmed, deleteAt, trashAt, isTrashed, storageClassesDesired,
         storageClassesConfirmed, storageClassesConfirmedAt, currentVersionUuid, version, preserveVersion, fileCount, fileSizeTotal } = apiResponse;
@@ -442,7 +443,7 @@ const collectionApiResponse = (apiResponse: CollectionResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const groupRequestApiResponse = (apiResponse: ProjectResource) => {
+const groupRequestApiResponse = (apiResponse: ProjectResource): JSX.Element => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, description, groupClass, trashAt, isTrashed, deleteAt, properties, writableBy } = apiResponse;
     const response = `
 "uuid": "${uuid}",
@@ -463,7 +464,7 @@ const groupRequestApiResponse = (apiResponse: ProjectResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const repositoryApiResponse = (apiResponse: RepositoryResource) => {
+const repositoryApiResponse = (apiResponse: RepositoryResource): JSX.Element => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, cloneUrls } = apiResponse;
     const response = `
 "uuid": "${uuid}",
@@ -478,7 +479,7 @@ const repositoryApiResponse = (apiResponse: RepositoryResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const sshKeyApiResponse = (apiResponse: SshKeyResource) => {
+const sshKeyApiResponse = (apiResponse: SshKeyResource): JSX.Element => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, name, authorizedUserUuid, expiresAt } = apiResponse;
     const response = `
 "uuid": "${uuid}",
@@ -493,7 +494,7 @@ const sshKeyApiResponse = (apiResponse: SshKeyResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource) => {
+const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource): JSX.Element => {
     const { uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid, hostname } = apiResponse;
     const response = `
 "hostname": ${stringify(hostname)},
@@ -508,7 +509,7 @@ const virtualMachineApiResponse = (apiResponse: VirtualMachinesResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const keepServiceApiResponse = (apiResponse: KeepServiceResource) => {
+const keepServiceApiResponse = (apiResponse: KeepServiceResource): JSX.Element => {
     const {
         uuid, readOnly, serviceHost, servicePort, serviceSslFlag, serviceType,
         ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid
@@ -529,7 +530,7 @@ const keepServiceApiResponse = (apiResponse: KeepServiceResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const userApiResponse = (apiResponse: UserResource) => {
+const userApiResponse = (apiResponse: UserResource): JSX.Element => {
     const {
         uuid, ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid,
         email, firstName, lastName, username, isActive, isAdmin, prefs, defaultOwnerUuid,
@@ -554,7 +555,7 @@ const userApiResponse = (apiResponse: UserResource) => {
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization) => {
+const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization): JSX.Element => {
     const {
         uuid, ownerUuid, apiToken, apiClientId, userId, createdByIpAddress, lastUsedByIpAddress,
         lastUsedAt, expiresAt, defaultOwnerUuid, scopes, updatedAt, createdAt
@@ -577,7 +578,7 @@ const apiClientAuthorizationApiResponse = (apiResponse: ApiClientAuthorization)
     return <span style={{ marginLeft: '-15px' }}>{'{'} {response} {'\n'} <span style={{ marginLeft: '-15px' }}>{'}'}</span></span>;
 };
 
-const linkApiResponse = (apiResponse: LinkResource) => {
+const linkApiResponse = (apiResponse: LinkResource): JSX.Element => {
     const {
         uuid, name, headUuid, properties, headKind, tailUuid, tailKind, linkClass,
         ownerUuid, createdAt, modifiedAt, modifiedByClientUuid, modifiedByUserUuid
diff --git a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
index f493df33..bc84ed2c 100644
--- a/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
+++ b/src/views-components/advanced-tab-dialog/advanced-tab-dialog.tsx
@@ -7,9 +7,11 @@ import { Dialog, DialogActions, Button, StyleRulesCallback, WithStyles, withStyl
 import { WithDialogProps } from 'store/dialog/with-dialog';
 import { withDialog } from "store/dialog/with-dialog";
 import { compose } from 'redux';
-import { ADVANCED_TAB_DIALOG } from "store/advanced-tab/advanced-tab";
+import { AdvancedTabDialogData, ADVANCED_TAB_DIALOG } from "store/advanced-tab/advanced-tab";
 import { DefaultCodeSnippet } from "components/default-code-snippet/default-code-snippet";
 import { MetadataTab } from 'views-components/advanced-tab-dialog/metadataTab';
+import { LinkResource } from "models/link";
+import { ListResults } from "services/common-service/common-service";
 
 type CssRules = 'content' | 'codeSnippet' | 'spacing';
 
@@ -34,7 +36,7 @@ export const AdvancedTabDialog = compose(
     withDialog(ADVANCED_TAB_DIALOG),
     withStyles(styles),
 )(
-    class extends React.Component<WithDialogProps<any> & WithStyles<CssRules>>{
+    class extends React.Component<WithDialogProps<AdvancedTabDialogData> & WithStyles<CssRules>>{
         state = {
             value: 0,
         };
@@ -67,7 +69,7 @@ export const AdvancedTabDialog = compose(
                 maxWidth="lg"
                 onClose={closeDialog}
                 onExit={() => this.setState({ value: 0 })} >
-                <DialogTitle>Advanced</DialogTitle>
+                <DialogTitle>API Details</DialogTitle>
                 <Tabs value={value} onChange={this.handleChange} fullWidth>
                     <Tab label="API RESPONSE" />
                     <Tab label="METADATA" />
@@ -78,8 +80,8 @@ export const AdvancedTabDialog = compose(
                 <DialogContent className={classes.content}>
                     {value === 0 && <div>{dialogContentExample(apiResponse, classes)}</div>}
                     {value === 1 && <div>
-                        {metadata !== '' && metadata.items.length > 0 ?
-                            <MetadataTab items={metadata.items} uuid={uuid} />
+                        {metadata !== '' && (metadata as ListResults<LinkResource>).items.length > 0 ?
+                            <MetadataTab items={(metadata as ListResults<LinkResource>).items} uuid={uuid} />
                             : dialogContentHeader('(No metadata links found)')}
                     </div>}
                     {value === 2 && dialogContent(pythonHeader, pythonExample, classes)}
@@ -110,8 +112,14 @@ const dialogContentHeader = (header: string) =>
         {header}
     </DialogContentText>;
 
-const dialogContentExample = (example: string, classes: any) =>
-    <DefaultCodeSnippet
+const dialogContentExample = (example: JSX.Element | string, classes: any) => {
+    // Pass string to lines param or JSX to child props
+    const stringData = example && (example as string).length ? (example as string) : undefined;
+    return <DefaultCodeSnippet
         apiResponse
         className={classes.codeSnippet}
-        lines={[example]} />;
\ No newline at end of file
+        lines={stringData ? [stringData] : []}
+    >
+        {example as JSX.Element || null}
+    </DefaultCodeSnippet>;
+}
diff --git a/src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts b/src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts
index 3394b211..aeaa6a22 100644
--- a/src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts
+++ b/src/views-components/context-menu/action-sets/api-client-authorization-action-set.ts
@@ -17,7 +17,7 @@ export const apiClientAuthorizationActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openApiClientAuthorizationAttributesDialog(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
diff --git a/src/views-components/context-menu/action-sets/collection-action-set.ts b/src/views-components/context-menu/action-sets/collection-action-set.ts
index 9b0efac0..edfaa3cd 100644
--- a/src/views-components/context-menu/action-sets/collection-action-set.ts
+++ b/src/views-components/context-menu/action-sets/collection-action-set.ts
@@ -78,7 +78,7 @@ const commonActionSet: ContextMenuActionSet = [[
     },
     {
         icon: AdvancedIcon,
-        name: "Advanced",
+        name: "API Details",
         execute: (dispatch, resource) => {
             dispatch<any>(openAdvancedTabDialog(resource.uuid));
         }
diff --git a/src/views-components/context-menu/action-sets/group-action-set.ts b/src/views-components/context-menu/action-sets/group-action-set.ts
index 874a601b..f573af69 100644
--- a/src/views-components/context-menu/action-sets/group-action-set.ts
+++ b/src/views-components/context-menu/action-sets/group-action-set.ts
@@ -20,7 +20,7 @@ export const groupActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openGroupAttributes(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, resource) => {
         dispatch<any>(openAdvancedTabDialog(resource.uuid));
diff --git a/src/views-components/context-menu/action-sets/group-member-action-set.ts b/src/views-components/context-menu/action-sets/group-member-action-set.ts
index b7215c70..37aa35c0 100644
--- a/src/views-components/context-menu/action-sets/group-member-action-set.ts
+++ b/src/views-components/context-menu/action-sets/group-member-action-set.ts
@@ -14,7 +14,7 @@ export const groupMemberActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openGroupMemberAttributes(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, resource) => {
         dispatch<any>(openAdvancedTabDialog(resource.uuid));
@@ -25,4 +25,4 @@ export const groupMemberActionSet: ContextMenuActionSet = [[{
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openRemoveGroupMemberDialog(uuid));
     }
-}]];
\ No newline at end of file
+}]];
diff --git a/src/views-components/context-menu/action-sets/keep-service-action-set.ts b/src/views-components/context-menu/action-sets/keep-service-action-set.ts
index b2d30baf..820d1978 100644
--- a/src/views-components/context-menu/action-sets/keep-service-action-set.ts
+++ b/src/views-components/context-menu/action-sets/keep-service-action-set.ts
@@ -14,7 +14,7 @@ export const keepServiceActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openKeepServiceAttributesDialog(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
diff --git a/src/views-components/context-menu/action-sets/link-action-set.ts b/src/views-components/context-menu/action-sets/link-action-set.ts
index 0b70ba9b..929a65a9 100644
--- a/src/views-components/context-menu/action-sets/link-action-set.ts
+++ b/src/views-components/context-menu/action-sets/link-action-set.ts
@@ -14,7 +14,7 @@ export const linkActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openLinkAttributesDialog(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
diff --git a/src/views-components/context-menu/action-sets/process-resource-action-set.ts b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
index f17d74c9..56cfee85 100644
--- a/src/views-components/context-menu/action-sets/process-resource-action-set.ts
+++ b/src/views-components/context-menu/action-sets/process-resource-action-set.ts
@@ -69,7 +69,7 @@ export const readOnlyProcessResourceActionSet: ContextMenuActionSet = [[
     },
     {
         icon: AdvancedIcon,
-        name: "Advanced",
+        name: "API Details",
         execute: (dispatch, resource) => {
             dispatch<any>(openAdvancedTabDialog(resource.uuid));
         }
diff --git a/src/views-components/context-menu/action-sets/project-action-set.ts b/src/views-components/context-menu/action-sets/project-action-set.ts
index a079bf4f..e352d0c4 100644
--- a/src/views-components/context-menu/action-sets/project-action-set.ts
+++ b/src/views-components/context-menu/action-sets/project-action-set.ts
@@ -52,7 +52,7 @@ export const readOnlyProjectActionSet: ContextMenuActionSet = [[
     },
     {
         icon: AdvancedIcon,
-        name: "Advanced",
+        name: "API Details",
         execute: (dispatch, resource) => {
             dispatch<any>(openAdvancedTabDialog(resource.uuid));
         }
diff --git a/src/views-components/context-menu/action-sets/repository-action-set.ts b/src/views-components/context-menu/action-sets/repository-action-set.ts
index fdd9d288..12fec7c4 100644
--- a/src/views-components/context-menu/action-sets/repository-action-set.ts
+++ b/src/views-components/context-menu/action-sets/repository-action-set.ts
@@ -21,7 +21,7 @@ export const repositoryActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openSharingDialog(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, resource) => {
         dispatch<any>(openAdvancedTabDialog(resource.uuid));
diff --git a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts
index 587a05bc..d1a94cd3 100644
--- a/src/views-components/context-menu/action-sets/ssh-key-action-set.ts
+++ b/src/views-components/context-menu/action-sets/ssh-key-action-set.ts
@@ -14,7 +14,7 @@ export const sshKeyActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openSshKeyAttributesDialog(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
diff --git a/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts b/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts
index b3d893b4..020ff5c7 100644
--- a/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts
+++ b/src/views-components/context-menu/action-sets/trashed-collection-action-set.ts
@@ -25,7 +25,7 @@ export const trashedCollectionActionSet: ContextMenuActionSet = [[
     },
     {
         icon: AdvancedIcon,
-        name: "Advanced",
+        name: "API Details",
         execute: (dispatch, resource) => {
             dispatch<any>(openAdvancedTabDialog(resource.uuid));
         }
diff --git a/src/views-components/context-menu/action-sets/user-action-set.ts b/src/views-components/context-menu/action-sets/user-action-set.ts
index c298e1ab..c00b7f1f 100644
--- a/src/views-components/context-menu/action-sets/user-action-set.ts
+++ b/src/views-components/context-menu/action-sets/user-action-set.ts
@@ -32,7 +32,7 @@ export const userActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openUserProjects(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));
diff --git a/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts b/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts
index 721a6a2f..be9567cd 100644
--- a/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts
+++ b/src/views-components/context-menu/action-sets/virtual-machine-action-set.ts
@@ -14,7 +14,7 @@ export const virtualMachineActionSet: ContextMenuActionSet = [[{
         dispatch<any>(openVirtualMachineAttributes(uuid));
     }
 }, {
-    name: "Advanced",
+    name: "API Details",
     icon: AdvancedIcon,
     execute: (dispatch, { uuid }) => {
         dispatch<any>(openAdvancedTabDialog(uuid));

commit 5a657d63ec7a37617a2448c6b1272476298f1e04
Author: Stephen Smith <stephen at curii.com>
Date:   Mon Aug 1 10:34:43 2022 -0400

    Merge branch '19079-search-results-open-newtab' into main. Closes #19079
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/cypress/integration/search.spec.js b/cypress/integration/search.spec.js
index 5434ca24..2216c067 100644
--- a/cypress/integration/search.spec.js
+++ b/cypress/integration/search.spec.js
@@ -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 at 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}`)
+            });
+
+        });
+    });
+});
diff --git a/src/index.tsx b/src/index.tsx
index 03840d49..5d939d36 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -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()}]`);
 
@@ -104,6 +105,7 @@ addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
 addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
 addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
 addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
+addMenuActionSet(ContextMenuKind.SEARCH_RESULTS, searchResultsActionSet);
 
 storeRedirects();
 
diff --git a/src/routes/routes.ts b/src/routes/routes.ts
index 50689ec3..22c8f4c8 100644
--- a/src/routes/routes.ts
+++ b/src/routes/routes.ts
@@ -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();
diff --git a/src/store/context-menu/context-menu-actions.ts b/src/store/context-menu/context-menu-actions.ts
index 3e239fee..e00b65b3 100644
--- a/src/store/context-menu/context-menu-actions.ts
+++ b/src/store/context-menu/context-menu-actions.ts
@@ -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';
@@ -267,3 +267,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,
+            }));
+        }
+    };
diff --git a/src/store/open-in-new-tab/open-in-new-tab.actions.ts b/src/store/open-in-new-tab/open-in-new-tab.actions.ts
index 94aec140..c465aae8 100644
--- a/src/store/open-in-new-tab/open-in-new-tab.actions.ts
+++ b/src/store/open-in-new-tab/open-in-new-tab.actions.ts
@@ -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
+};
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
index 00000000..e916a105
--- /dev/null
+++ b/src/views-components/context-menu/action-sets/search-results-action-set.ts
@@ -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));
+            }
+        },
+    ]
+];
diff --git a/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx b/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx
index c3408740..50ed20fd 100644
--- a/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx
+++ b/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx
@@ -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);
         }
 
diff --git a/src/views-components/context-menu/actions/helpers.test.ts b/src/views-components/context-menu/actions/helpers.test.ts
index b3b7f7f8..7776d0e5 100644
--- a/src/views-components/context-menu/actions/helpers.test.ts
+++ b/src/views-components/context-menu/actions/helpers.test.ts
@@ -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');
diff --git a/src/views-components/context-menu/actions/helpers.ts b/src/views-components/context-menu/actions/helpers.ts
index f196074d..9140e457 100644
--- a/src/views-components/context-menu/actions/helpers.ts
+++ b/src/views-components/context-menu/actions/helpers.ts
@@ -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;
diff --git a/src/views-components/context-menu/context-menu.tsx b/src/views-components/context-menu/context-menu.tsx
index 4766259a..a8e7fd02 100644
--- a/src/views-components/context-menu/context-menu.tsx
+++ b/src/views-components/context-menu/context-menu.tsx
@@ -111,4 +111,5 @@ export enum ContextMenuKind {
     PERMISSION_EDIT = "PermissionEdit",
     LINK = "Link",
     WORKFLOW = "Workflow",
+    SEARCH_RESULTS = "SearchResults"
 }
diff --git a/src/views/search-results-panel/search-results-panel.tsx b/src/views/search-results-panel/search-results-panel.tsx
index d25682f6..0902f15b 100644
--- a/src/views/search-results-panel/search-results-panel.tsx
+++ b/src/views/search-results-panel/search-results-panel.tsx
@@ -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));

commit f778abd3ea3c3a525419ed1bc2c0c63edb335405
Author: Stephen Smith <stephen at curii.com>
Date:   Thu Aug 25 11:27:32 2022 -0400

    Merge branch '19421-restore-old-redirect-key' into main. Closes #19421
    
    Arvados-DCO-1.1-Signed-off-by: Stephen Smith <stephen at curii.com>

diff --git a/src/common/redirect-to.ts b/src/common/redirect-to.ts
index d8fecde4..73c94843 100644
--- a/src/common/redirect-to.ts
+++ b/src/common/redirect-to.ts
@@ -7,6 +7,7 @@ import { Config } from './config';
 
 export const REDIRECT_TO_DOWNLOAD_KEY = 'redirectToDownload';
 export const REDIRECT_TO_PREVIEW_KEY = 'redirectToPreview';
+export const REDIRECT_TO_KEY = 'redirectTo';
 
 const getRedirectKeyFromUrl = (href: string): string | null => {
     switch (true) {
@@ -14,6 +15,8 @@ const getRedirectKeyFromUrl = (href: string): string | null => {
             return REDIRECT_TO_DOWNLOAD_KEY;
         case href.indexOf(REDIRECT_TO_PREVIEW_KEY) > -1:
             return REDIRECT_TO_PREVIEW_KEY;
+        case href.indexOf(`${REDIRECT_TO_KEY}=`) > -1:
+            return REDIRECT_TO_KEY;
         default:
             return null;
     }
@@ -32,8 +35,11 @@ export const storeRedirects = () => {
     const { location: { href }, localStorage } = window;
     const redirectKey = getRedirectKeyFromUrl(href);
 
-    if (localStorage && redirectKey) {
-        localStorage.setItem(redirectKey, href.split(`${redirectKey}=`)[1]);
+    // Change old redirectTo -> redirectToPreview when storing redirect
+    const redirectStoreKey = redirectKey === REDIRECT_TO_KEY ? REDIRECT_TO_PREVIEW_KEY : redirectKey;
+
+    if (localStorage && redirectKey && redirectStoreKey) {
+        localStorage.setItem(redirectStoreKey, href.split(`${redirectKey}=`)[1]);
     }
 };
 

-----------------------------------------------------------------------


hooks/post-receive
-- 




More information about the arvados-commits mailing list