16719: Merge branch 'master' into 16719-collection-version-basic-ui
authorLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 20 Oct 2020 21:32:23 +0000 (18:32 -0300)
committerLucas Di Pentima <lucas@di-pentima.com.ar>
Tue, 20 Oct 2020 21:32:23 +0000 (18:32 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <lucas@di-pentima.com.ar>

22 files changed:
src/common/config.ts
src/common/redirect-to.test.ts [new file with mode: 0644]
src/common/redirect-to.ts [new file with mode: 0644]
src/components/icon/icon.tsx
src/index.tsx
src/services/collection-service/collection-service.ts
src/store/auth/auth-action-session.ts
src/store/auth/auth-action.test.ts
src/store/auth/auth-middleware.test.ts
src/store/open-in-new-tab/open-in-new-tab.actions.ts [new file with mode: 0644]
src/store/store.ts
src/views-components/context-menu/action-sets/collection-files-item-action-set.ts
src/views-components/context-menu/actions/collection-copy-to-clipboard-action.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/collection-file-viewer-action.tsx
src/views-components/context-menu/actions/copy-to-clipboard-action.test.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/copy-to-clipboard-action.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/download-action.tsx
src/views-components/context-menu/actions/download-collection-file-action.tsx
src/views-components/context-menu/actions/file-viewer-action.test.tsx [new file with mode: 0644]
src/views-components/context-menu/actions/file-viewer-action.tsx
src/views-components/context-menu/actions/helpers.test.ts [new file with mode: 0644]
src/views-components/context-menu/actions/helpers.ts [new file with mode: 0644]

index afbeb5aecce884a1ba98a9c505ce7d2fdd299c13..146ca90acf62de05f7c4a0214f74701eb6c97f0f 100644 (file)
@@ -89,6 +89,7 @@ export interface ClusterConfigJSON {
 export class Config {
     baseUrl: string;
     keepWebServiceUrl: string;
+    keepWebInlineServiceUrl: string;
     remoteHosts: {
         [key: string]: string
     };
@@ -114,6 +115,7 @@ export const buildConfig = (clusterConfig: ClusterConfigJSON): Config => {
     config.workbench2Url = clusterConfigJSON.Services.Workbench2.ExternalURL;
     config.workbenchUrl = clusterConfigJSON.Services.Workbench1.ExternalURL;
     config.keepWebServiceUrl = clusterConfigJSON.Services.WebDAVDownload.ExternalURL;
+    config.keepWebInlineServiceUrl = clusterConfigJSON.Services.WebDAV.ExternalURL;
     config.loginCluster = clusterConfigJSON.Login.LoginCluster;
     config.clusterConfig = clusterConfigJSON;
     config.apiRevision = 0;
@@ -249,6 +251,7 @@ export const mockClusterConfigJSON = (config: Partial<ClusterConfigJSON>): Clust
 export const mockConfig = (config: Partial<Config>): Config => ({
     baseUrl: "",
     keepWebServiceUrl: "",
+    keepWebInlineServiceUrl: "",
     remoteHosts: {},
     rootUrl: "",
     uuidPrefix: "",
diff --git a/src/common/redirect-to.test.ts b/src/common/redirect-to.test.ts
new file mode 100644 (file)
index 0000000..e25d7be
--- /dev/null
@@ -0,0 +1,82 @@
+// 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 config: any = {
+        keepWebServiceUrl: 'http://localhost',
+        keepWebServiceInlineUrl: 'http://localhost'
+    };
+    const redirectTo = '/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, 'localStorage', {
+                value: {
+                    setItem: jest.fn(),
+                },
+                writable: true
+            });
+        });
+
+        it('should store redirectTo in the session storage', () => {
+            // when
+            storeRedirects();
+
+            // then
+            expect(window.localStorage.setItem).toHaveBeenCalledWith('redirectTo', redirectTo);
+        });
+    });
+
+    describe('handleRedirects', () => {
+        beforeEach(() => {
+            delete window.location;
+            window.location = {
+                ...locationTemplate,
+                href: `${location.href}?redirectTo=${redirectTo}`,
+            } as any;;
+            Object.defineProperty(window, 'localStorage', {
+                value: {
+                    getItem: () => redirectTo,
+                    removeItem: jest.fn(),
+                },
+                writable: true
+            });
+        });
+
+        it('should redirect to page when it is present in session storage', () => {
+            // when
+            handleRedirects("abcxyz", config);
+
+            // then
+            expect(window.location.href).toBe(`${config.keepWebServiceUrl}${redirectTo}?api_token=abcxyz`);
+        });
+    });
+});
diff --git a/src/common/redirect-to.ts b/src/common/redirect-to.ts
new file mode 100644 (file)
index 0000000..f5ece21
--- /dev/null
@@ -0,0 +1,32 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Config } from './config';
+
+const REDIRECT_TO_KEY = 'redirectTo';
+
+export const storeRedirects = () => {
+    if (window.location.href.indexOf(REDIRECT_TO_KEY) > -1) {
+        const { location: { href }, localStorage } = window;
+        const redirectUrl = href.split(`${REDIRECT_TO_KEY}=`)[1];
+
+        if (localStorage) {
+            localStorage.setItem(REDIRECT_TO_KEY, redirectUrl);
+        }
+    }
+};
+
+export const handleRedirects = (token: string, config: Config) => {
+    const { localStorage } = window;
+    const { keepWebServiceUrl } = config;
+
+    if (localStorage && localStorage.getItem(REDIRECT_TO_KEY)) {
+        const redirectUrl = localStorage.getItem(REDIRECT_TO_KEY);
+        localStorage.removeItem(REDIRECT_TO_KEY);
+        if (redirectUrl) {
+            const sep = redirectUrl.indexOf("?") > -1 ? "&" : "?";
+            window.location.href = `${keepWebServiceUrl}${redirectUrl}${sep}api_token=${token}`;
+        }
+    }
+};
index 18adb5abbf375f586bb3158207cb72d11548c30a..55c3c5a50f44759f10bbee681299b5ebe92cc32a 100644 (file)
@@ -56,6 +56,7 @@ import Star from '@material-ui/icons/Star';
 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';
@@ -139,3 +140,4 @@ export const UserPanelIcon: IconType = (props) => <Person {...props} />;
 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} />;
index a4353d4e0636c08916cee01dfe2149eebe16eb52..569656d9117874646b238616330e25d640aa932e 100644 (file)
@@ -62,6 +62,7 @@ import { processResourceAdminActionSet } from '~/views-components/context-menu/a
 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()}]`);
 
@@ -95,6 +96,8 @@ addMenuActionSet(ContextMenuKind.COLLECTION_ADMIN, collectionAdminActionSet);
 addMenuActionSet(ContextMenuKind.PROCESS_ADMIN, processResourceAdminActionSet);
 addMenuActionSet(ContextMenuKind.PROJECT_ADMIN, projectAdminActionSet);
 
+storeRedirects();
+
 fetchConfig()
     .then(({ config, apiHost }) => {
         const history = createBrowserHistory();
@@ -122,7 +125,7 @@ fetchConfig()
                 }
             }
         });
-        const store = configureStore(history, services);
+        const store = configureStore(history, services, config);
 
         store.subscribe(initListener(history, store, services, config));
         store.dispatch(initAuth(config));
index 90441a645f49d92d622fb0dd689fdaee9604bc51..0aa0aa84de4f476866c60cccb68b5dc2acc9bcb9 100644 (file)
@@ -65,8 +65,8 @@ export class CollectionService extends TrashableResourceService<CollectionResour
             ? this.webdavClient.defaults.baseURL.slice(0, -1)
             : this.webdavClient.defaults.baseURL;
         const apiToken = this.authService.getApiToken();
-        const splittedApiToken = apiToken ? apiToken.split('/') : [];
-        const userApiToken = `/t=${splittedApiToken[2]}/`;
+        const encodedApiToken = apiToken ? encodeURI(apiToken) : '';
+        const userApiToken = `/t=${encodedApiToken}/`;
         const splittedPrevFileUrl = file.url.split('/');
         const url = `${baseUrl}/${splittedPrevFileUrl[1]}${userApiToken}${splittedPrevFileUrl.slice(2).join('/')}`;
         return {
index ed2e18b2db39079705805501ed2cc4cc9bb18be6..a02f922df435d9f0b1c9065ef9a9e9f4a317ebd6 100644 (file)
@@ -27,6 +27,7 @@ const getClusterConfig = async (origin: string, apiClient: AxiosInstance): Promi
         configFromDD = {
             baseUrl: normalizeURLPath(dd.baseUrl),
             keepWebServiceUrl: dd.keepWebServiceUrl,
+            keepWebInlineServiceUrl: dd.keepWebInlineServiceUrl,
             remoteHosts: dd.remoteHosts,
             rootUrl: dd.rootUrl,
             uuidPrefix: dd.uuidPrefix,
index 83a699a7d2121d7118c2a37ac08cec1f3dc9733c..13575d44d5ce1ab65196fd0dab4bbf43ecb464a2 100644 (file)
@@ -23,6 +23,7 @@ describe('auth-actions', () => {
 
     let store: RootStore;
     let services: ServiceRepository;
+    const config: any = {};
     const actions: ApiActions = {
         progressFn: (id: string, working: boolean) => { },
         errorFn: (id: string, message: string) => { }
@@ -32,7 +33,7 @@ describe('auth-actions', () => {
     beforeEach(() => {
         axiosMock.reset();
         services = createServices(mockConfig({}), actions, axiosInst);
-        store = configureStore(createBrowserHistory(), services);
+        store = configureStore(createBrowserHistory(), services, config);
         localStorage.clear();
         importMocks = [];
     });
@@ -62,6 +63,7 @@ describe('auth-actions', () => {
             .reply(200, {
                 baseUrl: "https://xc59z.arvadosapi.com/arvados/v1",
                 keepWebServiceUrl: "",
+                keepWebInlineServiceUrl: "",
                 remoteHosts: {},
                 rootUrl: "https://xc59z.arvadosapi.com",
                 uuidPrefix: "xc59z",
index 1fe34381d941372a79b0b3835703895807cffc29..bcc942e1ee76e85f338ed34bc2db80437715d1ef 100644 (file)
@@ -18,6 +18,7 @@ describe("AuthMiddleware", () => {
     let store: RootStore;
     let services: ServiceRepository;
     let axiosInst: AxiosInstance;
+    const config: any = {};
     const actions: ApiActions = {
         progressFn: (id: string, working: boolean) => { },
         errorFn: (id: string, message: string) => { }
@@ -26,7 +27,7 @@ describe("AuthMiddleware", () => {
     beforeEach(() => {
         axiosInst = Axios.create({ headers: {} });
         services = createServices(mockConfig({}), actions, axiosInst);
-        store = configureStore(createBrowserHistory(), services);
+        store = configureStore(createBrowserHistory(), services, config);
         localStorage.clear();
     });
 
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
new file mode 100644 (file)
index 0000000..42bdc4c
--- /dev/null
@@ -0,0 +1,28 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { ResourceKind } from '~/models/resource';
+import { unionize, ofType } from '~/common/unionize';
+
+export const openInNewTabActions = unionize({
+    COPY_STORE: ofType<{}>(),
+    OPEN_COLLECTION_IN_NEW_TAB: ofType<string>(),
+    OPEN_PROJECT_IN_NEW_TAB: ofType<string>()
+});
+
+export const openInNewTabAction = (resource: any) => (dispatch: Dispatch) => {
+    const { uuid, kind } = resource;
+
+    dispatch(openInNewTabActions.COPY_STORE());
+
+    if (kind === ResourceKind.COLLECTION) {
+        dispatch(openInNewTabActions.OPEN_COLLECTION_IN_NEW_TAB(uuid));
+    }
+    if (kind === ResourceKind.PROJECT) {
+        dispatch(openInNewTabActions.OPEN_PROJECT_IN_NEW_TAB(uuid));
+    }
+
+    console.log(uuid);
+};
\ No newline at end of file
index 030b657662e8b2631f069b18775aa1c97f71f396..7beb099c1cf9c8f32ba818d064c6d2938b422b57 100644 (file)
@@ -6,6 +6,7 @@ import { createStore, applyMiddleware, compose, Middleware, combineReducers, Sto
 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";
@@ -68,6 +69,7 @@ import { ownerNameReducer } from '~/store/owner-name/owner-name-reducer';
 import { SubprocessMiddlewareService } from '~/store/subprocess-panel/subprocess-panel-middleware-service';
 import { SUBPROCESS_PANEL_ID } from '~/store/subprocess-panel/subprocess-panel-actions';
 import { ALL_PROCESSES_PANEL_ID } from './all-processes-panel/all-processes-panel-action';
+import { Config } from '~/common/config';
 
 const composeEnhancers =
     (process.env.NODE_ENV === 'development' &&
@@ -79,7 +81,7 @@ export type RootState = ReturnType<ReturnType<typeof createRootReducer>>;
 
 export type RootStore = Store<RootState, Action> & { dispatch: Dispatch<any> };
 
-export function configureStore(history: History, services: ServiceRepository): RootStore {
+export function configureStore(history: History, services: ServiceRepository, config: Config): RootStore {
     const rootReducer = createRootReducer(services);
 
     const projectPanelMiddleware = dataExplorerMiddleware(
@@ -130,6 +132,15 @@ export function configureStore(history: History, services: ServiceRepository): R
     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) {
+            handleRedirects(state.auth.apiToken, config);
+        }
+
+        return next(action);
+    };
 
     const middlewares: Middleware[] = [
         routerMiddleware(history),
@@ -150,9 +161,9 @@ export function configureStore(history: History, services: ServiceRepository): R
         apiClientAuthorizationMiddlewareService,
         publicFavoritesMiddleware,
         collectionsContentAddress,
-        subprocessMiddleware
+        subprocessMiddleware,
     ];
-    const enhancer = composeEnhancers(applyMiddleware(...middlewares));
+    const enhancer = composeEnhancers(applyMiddleware(redirectToMiddleware, ...middlewares));
     return createStore(rootReducer, enhancer);
 }
 
index b900d1864dd3490710cb39a061caff1b548417cb..6ce62ca942c55e738f79b2b47a295bd7cc220d1a 100644 (file)
@@ -7,7 +7,7 @@ import { RemoveIcon, RenameIcon } from "~/components/icon/icon";
 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 = [[
     {
@@ -17,6 +17,10 @@ export const readOnlyCollectionFilesItemActionSet: ContextMenuActionSet = [[
     {
         component: CollectionFileViewerAction,
         execute: () => { return; },
+    },
+    {
+        component: CollectionCopyToClipboardAction,
+        execute: () => { return; },
     }
 ]];
 
diff --git a/src/views-components/context-menu/actions/collection-copy-to-clipboard-action.tsx b/src/views-components/context-menu/actions/collection-copy-to-clipboard-action.tsx
new file mode 100644 (file)
index 0000000..f6038b8
--- /dev/null
@@ -0,0 +1,30 @@
+// 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 { 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 : '';
+    const { keepWebServiceUrl } = state.auth.config;
+    if (resource && resource.menuKind === ContextMenuKind.COLLECTION_FILES_ITEM) {
+        const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
+        if (file) {
+            return {
+                href: file.url.replace(keepWebServiceUrl, ''),
+                kind: 'file',
+                currentCollectionUuid
+            };
+        }
+    } else {
+        return ;
+    }
+    return ;
+};
+
+export const CollectionCopyToClipboardAction = connect(mapStateToProps)(CopyToClipboardAction);
index 0a202daf21085513acc2ae62f00c43e866393d4c..f75da23869666e248fe1f04d91cd0e500f0e1a21 100644 (file)
@@ -6,7 +6,6 @@ import { connect } from "react-redux";
 import { RootState } from "../../../store/store";
 import { FileViewerAction } from '~/views-components/context-menu/actions/file-viewer-action';
 import { getNodeValue } from "~/models/tree";
-import { CollectionFileType } from "~/models/collection-file";
 import { ContextMenuKind } from '~/views-components/context-menu/context-menu';
 
 const mapStateToProps = (state: RootState) => {
@@ -16,16 +15,15 @@ const mapStateToProps = (state: RootState) => {
         const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
         if (file) {
             return {
-                href: file.url,
-                download: file.type === CollectionFileType.DIRECTORY ? undefined : file.name,
+                href: file.url.replace(state.auth.config.keepWebServiceUrl, state.auth.config.keepWebInlineServiceUrl),
                 kind: 'file',
                 currentCollectionUuid
             };
         }
     } else {
-        return ;
+        return;
     }
-    return ;
+    return;
 };
 
 export const CollectionFileViewerAction = connect(mapStateToProps)(FileViewerAction);
diff --git a/src/views-components/context-menu/actions/copy-to-clipboard-action.test.tsx b/src/views-components/context-menu/actions/copy-to-clipboard-action.test.tsx
new file mode 100644 (file)
index 0000000..1ada703
--- /dev/null
@@ -0,0 +1,36 @@
+// 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
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
new file mode 100644 (file)
index 0000000..31ef4b9
--- /dev/null
@@ -0,0 +1,33 @@
+// 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
index 7468954fdd97d293837dd8edfebbc0d5a62a241a..86694c8b5f7b1de7bfa179dd59e2e7b5a0790499 100644 (file)
@@ -47,7 +47,7 @@ export const DownloadAction = (props: { href?: any, download?: any, onClick?: ()
     return props.href || props.kind === 'files'
         ? <a
             style={{ textDecoration: 'none' }}
-            href={props.kind === 'files' ? undefined : `${props.href}?disposition=attachment`}
+            href={props.kind === 'files' ? undefined : `${props.href}&disposition=attachment`}
             onClick={props.onClick}
             {...downloadProps}>
             <ListItem button onClick={() => props.kind === 'files' ? createZip(props.href, props.download) : undefined}>
index aadc1d11a14c26c4aa89fccdf52fdf1d39d7f001..3e4e4a0ba7929418ebc80694768716eb072d4c4d 100644 (file)
@@ -6,9 +6,9 @@ import { connect } from "react-redux";
 import { RootState } from "../../../store/store";
 import { DownloadAction } from "./download-action";
 import { getNodeValue } from "../../../models/tree";
-import { CollectionFileType } from "../../../models/collection-file";
 import { ContextMenuKind } from '../context-menu';
 import { filterCollectionFilesBySelection } from "~/store/collection-panel/collection-panel-files/collection-panel-files-state";
+import { sanitizeToken } from "./helpers";
 
 const mapStateToProps = (state: RootState) => {
     const { resource } = state.contextMenu;
@@ -17,8 +17,7 @@ const mapStateToProps = (state: RootState) => {
         const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
         if (file) {
             return {
-                href: file.url,
-                download: file.type === CollectionFileType.DIRECTORY ? undefined : file.name,
+                href: sanitizeToken(file.url, true),
                 kind: 'file',
                 currentCollectionUuid
             };
@@ -27,7 +26,6 @@ const mapStateToProps = (state: RootState) => {
         const files = filterCollectionFilesBySelection(state.collectionPanelFiles, true);
         return {
             href: files.map(file => file.url),
-            download: files.map(file => file.name),
             kind: 'files',
             currentCollectionUuid
         };
diff --git a/src/views-components/context-menu/actions/file-viewer-action.test.tsx b/src/views-components/context-menu/actions/file-viewer-action.test.tsx
new file mode 100644 (file)
index 0000000..fa455de
--- /dev/null
@@ -0,0 +1,33 @@
+// 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
index 20dcece400a2955aeaeeb42ebe55280464aa8bac..a631424e67bde5f46dbca5e370c3e6bdaa70eb29 100644 (file)
@@ -3,27 +3,40 @@
 // SPDX-License-Identifier: AGPL-3.0
 
 import * as React from "react";
+import { connect } from 'react-redux';
 import { ListItemIcon, ListItemText, ListItem } from "@material-ui/core";
 import { OpenIcon } from "~/components/icon/icon";
+import { sanitizeToken } from "./helpers";
+import { RootState } from "~/store/store";
 
-export const FileViewerAction = (props: { href?: any, download?: any, onClick?: () => void, kind?: string, currentCollectionUuid?: string; }) => {
-    const fileProps = props.download ? { download: props.download } : {};
+export const FileViewerAction = (props: any) => {
+    const {
+        keepWebServiceUrl,
+        keepWebInlineServiceUrl,
+    } = props;
 
     return props.href
         ? <a
             style={{ textDecoration: 'none' }}
-            href={props.href}
+            href={sanitizeToken(props.href.replace(keepWebServiceUrl, keepWebInlineServiceUrl), true)}
             target="_blank"
-            onClick={props.onClick}
-            {...fileProps}>
+            onClick={props.onClick}>
             <ListItem button>
-                    <ListItemIcon>
-                        <OpenIcon />
-                    </ListItemIcon>
+                <ListItemIcon>
+                    <OpenIcon />
+                </ListItemIcon>
                 <ListItemText>
                     Open in new tab
-                </ListItemText>
+                    </ListItemText>
             </ListItem>
         </a>
         : null;
-};
\ No newline at end of file
+};
+
+const mapStateToProps = ({ auth }: RootState): any => ({
+    keepWebServiceUrl: auth.config.keepWebServiceUrl,
+    keepWebInlineServiceUrl: auth.config.keepWebInlineServiceUrl,
+});
+
+
+export default connect(mapStateToProps, null)(FileViewerAction);
diff --git a/src/views-components/context-menu/actions/helpers.test.ts b/src/views-components/context-menu/actions/helpers.test.ts
new file mode 100644 (file)
index 0000000..9750a1c
--- /dev/null
@@ -0,0 +1,30 @@
+// 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=v2/arlog-gj3su-stk5unu8570brvs/fryzaq6z1ow1npak5nngldtkoup918isrvlualf134uf1fbtd/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=v2/arlog-gj3su-stk5unu8570brvs/fryzaq6z1ow1npak5nngldtkoup918isrvlualf134uf1fbtd');
+        });
+    });
+
+    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
diff --git a/src/views-components/context-menu/actions/helpers.ts b/src/views-components/context-menu/actions/helpers.ts
new file mode 100644 (file)
index 0000000..8dfcaca
--- /dev/null
@@ -0,0 +1,19 @@
+// 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 [token1, token2, token3, ...rest] = suffix.split('/');
+    const token = `${token1}/${token2}/${token3}`;
+    const sep = href.indexOf("?") > -1 ? "&" : "?";
+
+    return `${[prefix, ...rest].join('/')}${tokenAsQueryParam ? `${sep}api_token=${token}` : ''}`;
+};
+
+export const getClipboardUrl = (href: string): string => {
+    const { origin } = window.location;
+    const url = sanitizeToken(href, false);
+
+    return `${origin}?redirectTo=${url}`;
+};