Arvados-DCO-1.1-Signed-off-by: Daniel Kutyła <daniel.kutyla@contractors.roche.com>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { storeRedirects, handleRedirects } from './redirect-to';
+
+describe('redirect-to', () => {
+ const { location } = window;
+ const redirectTo = 'http://localhost/test123';
+ const locationTemplate = {
+ hash: '',
+ hostname: '',
+ origin: '',
+ host: '',
+ pathname: '',
+ port: '80',
+ protocol: 'http',
+ search: '',
+ reload: () => {},
+ replace: () => {},
+ assign: () => {},
+ ancestorOrigins: [],
+ href: '',
+ };
+
+ afterAll((): void => {
+ window.location = location;
+ });
+
+ describe('storeRedirects', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = {
+ ...locationTemplate,
+ href: `${location.href}?redirectTo=${redirectTo}`,
+ } as any;
+ Object.defineProperty(window, 'sessionStorage', {
+ value: {
+ setItem: jest.fn(),
+ },
+ writable: true
+ });
+ });
+
+ it('should store redirectTo in the session storage', () => {
+ // when
+ storeRedirects();
+
+ // then
+ expect(window.sessionStorage.setItem).toHaveBeenCalledWith('redirectTo', redirectTo);
+ });
+ });
+
+ describe('handleRedirects', () => {
+ beforeEach(() => {
+ delete window.location;
+ window.location = {
+ ...locationTemplate,
+ href: `${location.href}?redirectTo=${redirectTo}`,
+ } as any;;
+ Object.defineProperty(window, 'sessionStorage', {
+ value: {
+ getItem: () => redirectTo,
+ removeItem: jest.fn(),
+ },
+ writable: true
+ });
+ });
+
+ it('should redirect to page when it is present in session storage', () => {
+ // given
+ const token = 'testToken';
+
+ // when
+ handleRedirects(token);
+
+ // then
+ expect(window.location.href).toBe(`${redirectTo}?api_token=${token}`);
+ });
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+const REDIRECT_TO_KEY = 'redirectTo';
+
+export const storeRedirects = () => {
+ if (window.location.href.indexOf(REDIRECT_TO_KEY) > -1) {
+ const { location: { href }, sessionStorage } = window;
+ const redirectUrl = href.split(`${REDIRECT_TO_KEY}=`)[1];
+
+ if (sessionStorage) {
+ sessionStorage.setItem(REDIRECT_TO_KEY, redirectUrl);
+ }
+ }
+};
+
+export const handleRedirects = (token: string) => {
+ const { sessionStorage } = window;
+
+ if (sessionStorage && sessionStorage.getItem(REDIRECT_TO_KEY)) {
+ const redirectUrl = sessionStorage.getItem(REDIRECT_TO_KEY);
+ sessionStorage.removeItem(REDIRECT_TO_KEY);
+ window.location.href = `${redirectUrl}?api_token=${token}`;
+ }
+};
\ No newline at end of file
import StarBorder from '@material-ui/icons/StarBorder';
import Warning from '@material-ui/icons/Warning';
import VpnKey from '@material-ui/icons/VpnKey';
+import LinkOutlined from '@material-ui/icons/LinkOutlined';
// Import FontAwesome icons
import { library } from '@fortawesome/fontawesome-svg-core';
export const UsedByIcon: IconType = (props) => <Folder {...props} />;
export const WorkflowIcon: IconType = (props) => <Code {...props} />;
export const WarningIcon: IconType = (props) => <Warning style={{ color: '#fbc02d', height: '30px', width: '30px' }} {...props} />;
+export const Link: IconType = (props) => <LinkOutlined {...props} />;
import { projectAdminActionSet } from '~/views-components/context-menu/action-sets/project-admin-action-set';
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';
console.log(`Starting arvados [${getBuildInfo()}]`);
addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
+storeRedirects();
+
fetchConfig()
.then(({ config, apiHost }) => {
const history = createBrowserHistory();
import { routerMiddleware, routerReducer } from "react-router-redux";
import thunkMiddleware from 'redux-thunk';
import { History } from "history";
+import { handleRedirects } from '../common/redirect-to';
import { authReducer } from "./auth/auth-reducer";
import { authMiddleware } from "./auth/auth-middleware";
const subprocessMiddleware = dataExplorerMiddleware(
new SubprocessMiddlewareService(services, SUBPROCESS_PANEL_ID)
);
+ const redirectToMiddleware = (store: any) => (next: any) => (action: any) => {
+ const state = store.getState();
+
+ if (state.auth && state.auth.apiToken) {
+ const { apiToken } = state.auth;
+ handleRedirects(apiToken);
+ }
+
+ return next(action);
+ };
const middlewares: Middleware[] = [
routerMiddleware(history),
apiClientAuthorizationMiddlewareService,
publicFavoritesMiddleware,
collectionsContentAddress,
- subprocessMiddleware
+ subprocessMiddleware,
];
- const enhancer = composeEnhancers(applyMiddleware(...middlewares));
+ const enhancer = composeEnhancers(applyMiddleware(redirectToMiddleware, ...middlewares));
return createStore(rootReducer, enhancer);
}
import { DownloadCollectionFileAction } from "../actions/download-collection-file-action";
import { openFileRemoveDialog, openRenameFileDialog } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
import { CollectionFileViewerAction } from '~/views-components/context-menu/actions/collection-file-viewer-action';
-
+import { CollectionCopyToClipboardAction } from "../actions/collection-copy-to-clipboard-action";
export const readOnlyCollectionFilesItemActionSet: ContextMenuActionSet = [[
{
{
component: CollectionFileViewerAction,
execute: () => { return; },
+ },
+ {
+ component: CollectionCopyToClipboardAction,
+ execute: () => { return; },
}
]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { connect } from "react-redux";
+import { RootState } from "../../../store/store";
+import { getNodeValue } from "~/models/tree";
+import { CollectionFileType } from "~/models/collection-file";
+import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
+import { CopyToClipboardAction } from "./copy-to-clipboard-action";
+
+const mapStateToProps = (state: RootState) => {
+ const { resource } = state.contextMenu;
+ const currentCollectionUuid = state.collectionPanel.item ? state.collectionPanel.item.uuid : '';
+ if (resource && resource.menuKind === ContextMenuKind.COLLECTION_FILES_ITEM) {
+ const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
+ if (file) {
+ return {
+ href: file.url,
+ download: file.type === CollectionFileType.DIRECTORY ? undefined : file.name,
+ kind: 'file',
+ currentCollectionUuid
+ };
+ }
+ } else {
+ return ;
+ }
+ return ;
+};
+
+export const CollectionCopyToClipboardAction = connect(mapStateToProps)(CopyToClipboardAction);
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { shallow, configure } from 'enzyme';
+import { ListItem } from "@material-ui/core";
+import * as Adapter from 'enzyme-adapter-react-16';
+import { CopyToClipboardAction } from './copy-to-clipboard-action';
+
+configure({ adapter: new Adapter() });
+
+jest.mock('copy-to-clipboard', () => jest.fn());
+
+describe('CopyToClipboardAction', () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ onClick: jest.fn(),
+ href: 'https://collections.ardev.roche.com/c=ardev-4zz18-k0hamvtwyit6q56/t=1ha4ykd3w14ed19b2gh3uyjrjup38vsx27x1utwdne0bxcfg5d/LIMS/1.html',
+ };
+ });
+
+ it('should render properly and handle click', () => {
+ // when
+ const wrapper = shallow(<CopyToClipboardAction {...props} />);
+ wrapper.find(ListItem).simulate('click');
+
+ // then
+ expect(wrapper).not.toBeUndefined();
+
+ // and
+ expect(props.onClick).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import * as copy from 'copy-to-clipboard';
+import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
+import { Link } from "~/components/icon/icon";
+import { getClipboardUrl } 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);
+ copy(clipboardUrl);
+ }
+
+ if (props.onClick) {
+ props.onClick();
+ }
+ };
+
+ return props.href
+ ? <ListItem button onClick={copyToClipboard}>
+ <ListItemIcon>
+ <Link />
+ </ListItemIcon>
+ <ListItemText>
+ Copy to clipboard
+ </ListItemText>
+ </ListItem>
+ : null;
+};
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from 'react';
+import { shallow, configure } from 'enzyme';
+import * as Adapter from 'enzyme-adapter-react-16';
+import { FileViewerAction } from './file-viewer-action';
+
+configure({ adapter: new Adapter() });
+
+describe('FileViewerAction', () => {
+ let props;
+
+ beforeEach(() => {
+ props = {
+ onClick: jest.fn(),
+ href: 'https://collections.ardev.roche.com/c=ardev-4zz18-k0hamvtwyit6q56/t=1ha4ykd3w14ed19b2gh3uyjrjup38vsx27x1utwdne0bxcfg5d/LIMS/1.html',
+ };
+ });
+
+ it('should render properly and handle click', () => {
+ // when
+ const wrapper = shallow(<FileViewerAction {...props} />);
+ wrapper.find('a').simulate('click');
+
+ // then
+ expect(wrapper).not.toBeUndefined();
+
+ // and
+ expect(props.onClick).toHaveBeenCalled();
+ });
+});
\ No newline at end of file
import * as React from "react";
import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
import { OpenIcon } from "~/components/icon/icon";
+import { sanitizeToken } from "./helpers";
export const FileViewerAction = (props: { href?: any, download?: any, onClick?: () => void, kind?: string, currentCollectionUuid?: string; }) => {
const fileProps = props.download ? { download: props.download } : {};
return props.href
? <a
style={{ textDecoration: 'none' }}
- href={props.href}
+ href={sanitizeToken(props.href)}
target="_blank"
onClick={props.onClick}
{...fileProps}>
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { sanitizeToken, getClipboardUrl } from "./helpers";
+
+describe('helpers', () => {
+ // given
+ const url = 'https://collections.ardev.roche.com/c=ardev-4zz18-k0hamvtwyit6q56/t=1ha4ykd3w14ed19b2gh3uyjrjup38vsx27x1utwdne0bxcfg5d/LIMS/1.html';
+
+ describe('sanitizeToken', () => {
+ it('should sanitize token from the url', () => {
+ // when
+ const result = sanitizeToken(url);
+
+ // then
+ expect(result).toBe('https://collections.ardev.roche.com/c=ardev-4zz18-k0hamvtwyit6q56/LIMS/1.html?api_token=1ha4ykd3w14ed19b2gh3uyjrjup38vsx27x1utwdne0bxcfg5d');
+ });
+ });
+
+ describe('getClipboardUrl', () => {
+ it('should add redirectTo query param', () => {
+ // when
+ const result = getClipboardUrl(url);
+
+ // then
+ expect(result).toBe('http://localhost?redirectTo=https://collections.ardev.roche.com/c=ardev-4zz18-k0hamvtwyit6q56/LIMS/1.html');
+ });
+ });
+});
\ No newline at end of file
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export const sanitizeToken = (href: string, tokenAsQueryParam: boolean = true): string => {
+ const [prefix, suffix] = href.split('/t=');
+ const [token, ...rest] = suffix.split('/');
+
+ return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `?api_token=${token}` : ''}`;
+};
+
+export const getClipboardUrl = (href: string): string => {
+ const { origin } = window.location;
+
+ return `${origin}?redirectTo=${sanitizeToken(href, false)}`;
+};
\ No newline at end of file