From: Lucas Di Pentima Date: Tue, 20 Oct 2020 21:32:23 +0000 (-0300) Subject: 16719: Merge branch 'master' into 16719-collection-version-basic-ui X-Git-Tag: 2.1.1~7^2~5 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/a9f2ce9838cf6a215cbc108073d033bc3811bdf5?hp=b5c368012c8719fb1b5493dfed2443c4974d4f91 16719: Merge branch 'master' into 16719-collection-version-basic-ui Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima --- diff --git a/src/common/config.ts b/src/common/config.ts index afbeb5ae..146ca90a 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -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): Clust export const mockConfig = (config: Partial): 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 index 00000000..e25d7be9 --- /dev/null +++ b/src/common/redirect-to.test.ts @@ -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 index 00000000..f5ece21b --- /dev/null +++ b/src/common/redirect-to.ts @@ -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}`; + } + } +}; diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index 18adb5ab..55c3c5a5 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -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) => ; export const UsedByIcon: IconType = (props) => ; export const WorkflowIcon: IconType = (props) => ; export const WarningIcon: IconType = (props) => ; +export const Link: IconType = (props) => ; diff --git a/src/index.tsx b/src/index.tsx index a4353d4e..569656d9 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -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)); diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts index 90441a64..0aa0aa84 100644 --- a/src/services/collection-service/collection-service.ts +++ b/src/services/collection-service/collection-service.ts @@ -65,8 +65,8 @@ export class CollectionService extends TrashableResourceService { 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", diff --git a/src/store/auth/auth-middleware.test.ts b/src/store/auth/auth-middleware.test.ts index 1fe34381..bcc942e1 100644 --- a/src/store/auth/auth-middleware.test.ts +++ b/src/store/auth/auth-middleware.test.ts @@ -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 index 00000000..42bdc4cc --- /dev/null +++ b/src/store/open-in-new-tab/open-in-new-tab.actions.ts @@ -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(), + OPEN_PROJECT_IN_NEW_TAB: ofType() +}); + +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 diff --git a/src/store/store.ts b/src/store/store.ts index 030b6576..7beb099c 100644 --- a/src/store/store.ts +++ b/src/store/store.ts @@ -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>; export type RootStore = Store & { dispatch: Dispatch }; -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); } diff --git a/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts b/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts index b900d186..6ce62ca9 100644 --- a/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts +++ b/src/views-components/context-menu/action-sets/collection-files-item-action-set.ts @@ -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 index 00000000..f6038b80 --- /dev/null +++ b/src/views-components/context-menu/actions/collection-copy-to-clipboard-action.tsx @@ -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); diff --git a/src/views-components/context-menu/actions/collection-file-viewer-action.tsx b/src/views-components/context-menu/actions/collection-file-viewer-action.tsx index 0a202daf..f75da238 100644 --- a/src/views-components/context-menu/actions/collection-file-viewer-action.tsx +++ b/src/views-components/context-menu/actions/collection-file-viewer-action.tsx @@ -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 index 00000000..1ada703b --- /dev/null +++ b/src/views-components/context-menu/actions/copy-to-clipboard-action.test.tsx @@ -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(); + 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 index 00000000..31ef4b97 --- /dev/null +++ b/src/views-components/context-menu/actions/copy-to-clipboard-action.tsx @@ -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 + ? + + + + + Copy to clipboard + + + : null; +}; \ No newline at end of file diff --git a/src/views-components/context-menu/actions/download-action.tsx b/src/views-components/context-menu/actions/download-action.tsx index 7468954f..86694c8b 100644 --- a/src/views-components/context-menu/actions/download-action.tsx +++ b/src/views-components/context-menu/actions/download-action.tsx @@ -47,7 +47,7 @@ export const DownloadAction = (props: { href?: any, download?: any, onClick?: () return props.href || props.kind === 'files' ? props.kind === 'files' ? createZip(props.href, props.download) : undefined}> diff --git a/src/views-components/context-menu/actions/download-collection-file-action.tsx b/src/views-components/context-menu/actions/download-collection-file-action.tsx index aadc1d11..3e4e4a0b 100644 --- a/src/views-components/context-menu/actions/download-collection-file-action.tsx +++ b/src/views-components/context-menu/actions/download-collection-file-action.tsx @@ -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 index 00000000..fa455def --- /dev/null +++ b/src/views-components/context-menu/actions/file-viewer-action.test.tsx @@ -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(); + wrapper.find('a').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/file-viewer-action.tsx b/src/views-components/context-menu/actions/file-viewer-action.tsx index 20dcece4..a631424e 100644 --- a/src/views-components/context-menu/actions/file-viewer-action.tsx +++ b/src/views-components/context-menu/actions/file-viewer-action.tsx @@ -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 ? + onClick={props.onClick}> - - - + + + Open in new tab - + : 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 index 00000000..9750a1cc --- /dev/null +++ b/src/views-components/context-menu/actions/helpers.test.ts @@ -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 index 00000000..8dfcaca0 --- /dev/null +++ b/src/views-components/context-menu/actions/helpers.ts @@ -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}`; +};