.click()
cy.get('[data-cy=context-menu]')
.should('contain', 'Add to favorites')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection')
- .type('{esc}'); // Collapse the options menu
+ .and(`${isWritable ? '' : 'not.'}contain`, 'Edit collection');
+ cy.get('body').click(); // Collapse the menu avoiding details panel expansion
cy.get('[data-cy=collection-properties-panel]')
.should('contain', 'someKey')
.and('contain', 'someValue')
cy.get('[data-cy=upload-button]')
.should(`${isWritable ? '' : 'not.'}contain`, 'Upload data');
});
+ cy.get('[data-cy=collection-files-panel]')
+ .contains('bar').rightclick();
+ cy.get('[data-cy=context-menu]')
+ .should('contain', 'Download')
+ .and('contain', 'Open in new tab')
+ .and('contain', 'Copy to clipboard')
+ .and(`${isWritable ? '' : 'not.'}contain`, 'Rename')
+ .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
+ cy.get('body').click(); // Collapse the menu
// Hamburger 'more options' menu button
cy.get('[data-cy=collection-files-panel-options-btn]')
.click()
cy.get('[data-cy=context-menu]')
// .should('contain', 'Download selected')
.should(`${isWritable ? '' : 'not.'}contain`, 'Remove selected')
- .type('{esc}'); // Collapse the options menu
+ cy.get('body').click(); // Collapse the menu
// File item 'more options' button
cy.get('[data-cy=file-item-options-btn')
.click()
cy.get('[data-cy=context-menu]')
.should('contain', 'Download')
- .and(`${isWritable ? '' : 'not.'}contain`, 'Remove')
- .type('{esc}'); // Collapse
+ .and(`${isWritable ? '' : 'not.'}contain`, 'Remove');
+ cy.get('body').click(); // Collapse the menu
})
})
})
//
// SPDX-License-Identifier: AGPL-3.0
-describe('Collection panel tests', function() {
+describe('Favorites tests', function() {
let activeUser;
let adminUser;
})
}, null, activeUser.token, true);
// Should log the user out.
- cy.get('[data-cy=breadcrumb-first]').click();
+ cy.visit('/');
cy.get('div#root').should('contain', 'Please log in');
})
export class Config {
baseUrl: string;
keepWebServiceUrl: string;
+ keepWebInlineServiceUrl: string;
remoteHosts: {
[key: string]: string
};
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;
export const mockConfig = (config: Partial<Config>): Config => ({
baseUrl: "",
keepWebServiceUrl: "",
+ keepWebInlineServiceUrl: "",
remoteHosts: {},
rootUrl: "",
uuidPrefix: "",
}
}
}
- return "";
+ return "0 B";
};
export const formatTime = (time: number, seconds?: boolean) => {
--- /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 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`);
+ });
+ });
+});
--- /dev/null
+// 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}`;
+ }
+ }
+};
const styles: StyleRulesCallback<CssRules> = (theme: ArvadosTheme) => ({
attribute: {
- display: 'flex',
- alignItems: 'flex-start'
+ marginBottom: ".6 rem"
},
label: {
boxSizing: 'border-box',
color: theme.palette.grey["500"],
- width: '40%'
+ width: '100%'
},
value: {
boxSizing: 'border-box',
- width: '60%',
alignItems: 'flex-start'
},
lowercaseValue: {
textTransform: 'lowercase'
},
link: {
- width: '60%',
color: theme.palette.primary.main,
textDecoration: 'none',
overflowWrap: 'break-word',
},
copyIcon: {
marginLeft: theme.spacing.unit,
- fontSize: '1.125rem',
color: theme.palette.grey["500"],
- cursor: 'pointer'
+ cursor: 'pointer',
+ display: 'inline',
+ '& svg': {
+ fontSize: '1rem'
+ }
}
});
valueNode = value;
}
return <Typography component="div" className={classes.attribute}>
- <Typography component="span" className={classnames([classes.label, classLabel])}>{label}</Typography>
+ <Typography component="div" className={classnames([classes.label, classLabel])}>{label}</Typography>
<Typography
onClick={onValueClick}
- component="span"
+ component="div"
className={classnames([classes.value, classValue, { [classes.lowercaseValue]: lowercaseValue }])}>
{valueNode}
{children}
{linkToUuid && <Tooltip title="Copy">
- <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
- <CopyIcon className={classes.copyIcon} />
- </CopyToClipboard>
+ <span className={classes.copyIcon}>
+ <CopyToClipboard text={linkToUuid || ""} onCopy={() => this.onCopy("Copied")}>
+ <CopyIcon />
+ </CopyToClipboard>
+ </span>
</Tooltip>}
</Typography>
</Typography>;
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();
}
}
});
- const store = configureStore(history, services);
+ const store = configureStore(history, services, config);
store.subscribe(initListener(history, store, services, config));
store.dispatch(initAuth(config));
? 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 {
configFromDD = {
baseUrl: normalizeURLPath(dd.baseUrl),
keepWebServiceUrl: dd.keepWebServiceUrl,
+ keepWebInlineServiceUrl: dd.keepWebInlineServiceUrl,
remoteHosts: dd.remoteHosts,
rootUrl: dd.rootUrl,
uuidPrefix: dd.uuidPrefix,
let store: RootStore;
let services: ServiceRepository;
+ const config: any = {};
const actions: ApiActions = {
progressFn: (id: string, working: boolean) => { },
errorFn: (id: string, message: string) => { }
beforeEach(() => {
axiosMock.reset();
services = createServices(mockConfig({}), actions, axiosInst);
- store = configureStore(createBrowserHistory(), services);
+ store = configureStore(createBrowserHistory(), services, config);
localStorage.clear();
importMocks = [];
});
.reply(200, {
baseUrl: "https://xc59z.arvadosapi.com/arvados/v1",
keepWebServiceUrl: "",
+ keepWebInlineServiceUrl: "",
remoteHosts: {},
rootUrl: "https://xc59z.arvadosapi.com",
uuidPrefix: "xc59z",
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) => { }
beforeEach(() => {
axiosInst = Axios.create({ headers: {} });
services = createServices(mockConfig({}), actions, axiosInst);
- store = configureStore(createBrowserHistory(), services);
+ store = configureStore(createBrowserHistory(), services, config);
localStorage.clear();
});
--- /dev/null
+// 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));
+ } else if (kind === ResourceKind.PROJECT) {
+ dispatch(openInNewTabActions.OPEN_PROJECT_IN_NEW_TAB(uuid));
+ }
+};
\ No newline at end of file
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";
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' &&
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(
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),
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 { 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 ||
+ resource.menuKind === ContextMenuKind.READONLY_COLLECTION_FILES_ITEM)) {
+ const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
+ if (file) {
+ return {
+ href: file.url.replace(keepWebServiceUrl, ''),
+ kind: 'file',
+ currentCollectionUuid
+ };
+ }
+ }
+ return {};
+};
+
+export const CollectionCopyToClipboardAction = connect(mapStateToProps)(CopyToClipboardAction);
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) => {
const { resource } = state.contextMenu;
const currentCollectionUuid = state.collectionPanel.item ? state.collectionPanel.item.uuid : '';
- if (resource && resource.menuKind === ContextMenuKind.COLLECTION_FILES_ITEM) {
+ if (resource && (
+ resource.menuKind === ContextMenuKind.COLLECTION_FILES_ITEM ||
+ resource.menuKind === ContextMenuKind.READONLY_COLLECTION_FILES_ITEM)) {
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 {};
};
export const CollectionFileViewerAction = connect(mapStateToProps)(FileViewerAction);
--- /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
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}>
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;
const currentCollectionUuid = state.collectionPanel.item ? state.collectionPanel.item.uuid : '';
- if (resource && resource.menuKind === ContextMenuKind.COLLECTION_FILES_ITEM) {
+ if (resource && (
+ resource.menuKind === ContextMenuKind.COLLECTION_FILES_ITEM ||
+ resource.menuKind === ContextMenuKind.READONLY_COLLECTION_FILES_ITEM)) {
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
};
const files = filterCollectionFilesBySelection(state.collectionPanelFiles, true);
return {
href: files.map(file => file.url),
- download: files.map(file => file.name),
kind: 'files',
currentCollectionUuid
};
--- /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
// 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);
--- /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://example.com/c=zzzzz/t=v2/a/b/LIMS/1.html';
+
+ describe('sanitizeToken', () => {
+ it('should sanitize token from the url', () => {
+ // when
+ const result = sanitizeToken(url);
+
+ // then
+ expect(result).toBe('https://example.com/c=zzzzz/LIMS/1.html?api_token=v2/a/b');
+ });
+ });
+
+ describe('getClipboardUrl', () => {
+ it('should add redirectTo query param', () => {
+ // when
+ const result = getClipboardUrl(url);
+
+ // then
+ expect(result).toBe('http://localhost?redirectTo=https://example.com/c=zzzzz/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 [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}`;
+};
}
private getCollectionInfo() {
- return <CollectionDetailsAttributes item={this.item} />;
+ return <CollectionDetailsAttributes twoCol={false} item={this.item} />;
}
private getVersionBrowser() {
import { RootState } from '~/store/store';
import { MoreOptionsIcon, CollectionIcon, ReadOnlyIcon, ExpandIcon, CollectionOldVersionIcon } from '~/components/icon/icon';
import { DetailsAttribute } from '~/components/details-attribute/details-attribute';
-import { CollectionResource } from '~/models/collection';
+import { CollectionResource, getCollectionUrl } from '~/models/collection';
import { CollectionPanelFiles } from '~/views-components/collection-panel-files/collection-panel-files';
import { CollectionTagForm } from './collection-tag-form';
import { deleteCollectionTag, navigateToProcess, collectionPanelActions } from '~/store/collection-panel/collection-panel-action';
import { getUserUuid } from '~/common/getuser';
import { getProgressIndicator } from '~/store/progress-indicator/progress-indicator-reducer';
import { COLLECTION_PANEL_LOAD_FILES, loadCollectionFiles, COLLECTION_PANEL_LOAD_FILES_THRESHOLD } from '~/store/collection-panel/collection-panel-files/collection-panel-files-actions';
+import { Link } from 'react-router-dom';
type CssRules = 'root'
| 'filesCard'
if (item.ownerUuid === currentUserUUID) {
isWritable = true;
} else {
- const itemOwner = getResource<GroupResource|UserResource>(item.ownerUuid)(state.resources);
+ const itemOwner = getResource<GroupResource | UserResource>(item.ownerUuid)(state.resources);
if (itemOwner) {
isWritable = itemOwner.writableBy.indexOf(currentUserUUID || '') >= 0;
}
<Grid container justify="space-between">
<Grid item xs={11}><span>
<IconButton onClick={this.openCollectionDetails}>
- { isOldVersion
- ? <CollectionOldVersionIcon className={classes.iconHeader} />
- : <CollectionIcon className={classes.iconHeader} /> }
+ {isOldVersion
+ ? <CollectionOldVersionIcon className={classes.iconHeader} />
+ : <CollectionIcon className={classes.iconHeader} />}
</IconButton>
- <IllegalNamingWarning name={item.name}/>
+ <IllegalNamingWarning name={item.name} />
<span>
{item.name}
{isWritable ||
- <Tooltip title="Read-only">
- <ReadOnlyIcon data-cy="read-only-icon" className={classes.readOnlyIcon} />
- </Tooltip>
+ <Tooltip title="Read-only">
+ <ReadOnlyIcon data-cy="read-only-icon" className={classes.readOnlyIcon} />
+ </Tooltip>
}
</span>
</span></Grid>
- <Grid item xs={1} style={{textAlign: "right"}}>
+ <Grid item xs={1} style={{ textAlign: "right" }}>
<Tooltip title="Actions" disableFocusListener>
<IconButton
data-cy='collection-panel-options-btn'
<Typography variant="caption">
{item.description}
</Typography>
- <CollectionDetailsAttributes item={item} classes={classes} />
+ <CollectionDetailsAttributes item={item} classes={classes} twoCol={true} />
{(item.properties.container_request || item.properties.containerRequest) &&
<span onClick={() => dispatch<any>(navigateToProcess(item.properties.container_request || item.properties.containerRequest))}>
<DetailsAttribute classLabel={classes.link} label='Link to process' />
</span>
}
{isOldVersion &&
- <Typography className={classes.warningLabel} variant="caption">
- This is an old version. Copy it as a new one if you need to make changes. Go to the current version if you need to share it.
- </Typography>
+ <Typography className={classes.warningLabel} variant="caption">
+ This is an old version. Make a copy to make changes. Go to the <Link to={getCollectionUrl(item.currentVersionUuid)}>head version</Link> for sharing options.
+ </Typography>
}
</Grid>
</Grid>
<CollectionTagForm />
</Grid>}
<Grid item xs={12}>
- { Object.keys(item.properties).length > 0
- ? Object.keys(item.properties).map(k =>
- Array.isArray(item.properties[k])
- ? item.properties[k].map((v: string) =>
- getPropertyChip(
- k, v,
- isWritable
- ? this.handleDelete(k, item.properties[k])
- : undefined,
- classes.tag))
- : getPropertyChip(
- k, item.properties[k],
- isWritable
- ? this.handleDelete(k, item.properties[k])
- : undefined,
- classes.tag)
- )
- : <div className={classes.centeredLabel}>No properties set on this collection.</div>
- }
+ {Object.keys(item.properties).length > 0
+ ? Object.keys(item.properties).map(k =>
+ Array.isArray(item.properties[k])
+ ? item.properties[k].map((v: string) =>
+ getPropertyChip(
+ k, v,
+ isWritable
+ ? this.handleDelete(k, item.properties[k])
+ : undefined,
+ classes.tag))
+ : getPropertyChip(
+ k, item.properties[k],
+ isWritable
+ ? this.handleDelete(k, item.properties[k])
+ : undefined,
+ classes.tag)
+ )
+ : <div className={classes.centeredLabel}>No properties set on this collection.</div>
+ }
</Grid>
</Grid>
</ExpansionPanelDetails>
dispatch(collectionPanelActions.LOAD_BIG_COLLECTIONS(true));
dispatch<any>(loadCollectionFiles(this.props.item.uuid));
}
- } />
+ } />
</div>
</div>
: null;
)
);
-export const CollectionDetailsAttributes = (props: {item: CollectionResource, classes?: Record<CssRules, string>}) => {
+export const CollectionDetailsAttributes = (props: { item: CollectionResource, twoCol: boolean, classes?: Record<CssRules, string> }) => {
const item = props.item;
- const classes = props.classes || {label: '', value: ''};
+ const classes = props.classes || { label: '', value: '' };
const isOldVersion = item && item.currentVersionUuid !== item.uuid;
- return <span>
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label={isOldVersion ? "This version's UUID" : "Collection UUID"}
- linkToUuid={item.uuid} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label={isOldVersion ? "This version's PDH" : "Portable data hash"}
- linkToUuid={item.portableDataHash} />
+ const mdSize = props.twoCol ? 6 : 12;
+ return <Grid container>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label={isOldVersion ? "This version's UUID" : "Collection UUID"}
+ linkToUuid={item.uuid} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label={isOldVersion ? "This version's PDH" : "Portable data hash"}
+ linkToUuid={item.portableDataHash} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Owner' linkToUuid={item.ownerUuid} />
+ </Grid>
+
{isOldVersion &&
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Most recent version'
- linkToUuid={item.currentVersionUuid} />
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Head version'
+ linkToUuid={item.currentVersionUuid} />
+ </Grid>
}
- <DetailsAttribute label='Last modified' value={formatDate(item.modifiedAt)} />
- <DetailsAttribute label='Created at' value={formatDate(item.createdAt)} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Version number' value={item.version} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Number of files' value={item.fileCount} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Content size' value={formatFileSize(item.fileSizeTotal)} />
- <DetailsAttribute classLabel={classes.label} classValue={classes.value}
- label='Owner' linkToUuid={item.ownerUuid} />
- </span>;
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Version number' value={item.version} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Created at' value={formatDate(item.createdAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute label='Last modified' value={formatDate(item.modifiedAt)} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Number of files' value={item.fileCount} />
+ </Grid>
+ <Grid item xs={12} md={mdSize}>
+ <DetailsAttribute classLabel={classes.label} classValue={classes.value}
+ label='Content size' value={formatFileSize(item.fileSizeTotal)} />
+ </Grid>
+ </Grid>;
};