});
});
});
-});
\ 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}`)
+ });
+
+ });
+ });
+});
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()}]`);
addMenuActionSet(ContextMenuKind.FILTER_GROUP_ADMIN, filterGroupAdminActionSet);
addMenuActionSet(ContextMenuKind.PERMISSION_EDIT, permissionEditActionSet);
addMenuActionSet(ContextMenuKind.WORKFLOW, workflowActionSet);
+addMenuActionSet(ContextMenuKind.SEARCH_RESULTS, searchResultsActionSet);
storeRedirects();
}
};
-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)) {
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();
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';
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,
+ }));
+ }
+ };
// 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
+};
--- /dev/null
+// 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));
+ }
+ },
+ ]
+];
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);
}
//
// SPDX-License-Identifier: AGPL-3.0
-import { sanitizeToken, getClipboardUrl, getInlineFileUrl } from "./helpers";
+import { sanitizeToken, getCollectionItemClipboardUrl, getInlineFileUrl } from "./helpers";
describe('helpers', () => {
// given
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');
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;
PERMISSION_EDIT = "PermissionEdit",
LINK = "Link",
WORKFLOW = "Workflow",
+ SEARCH_RESULTS = "SearchResults"
}
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';
};
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));