```
{
"API_HOST": "string",
- "VOCABULARY_URL": "string"
+ "VOCABULARY_URL": "string",
+ "FILE_VIEWERS_CONFIG_URL": "string",
}
```
Local path, or any URL that allows cross-origin requests. See
[Vocabulary JSON file example](public/vocabulary-example.json).
+### FILE_VIEWERS_CONFIG_URL
+Local path, or any URL that allows cross-origin requests. See:
+
+[File viewers config file example](public/file-viewers-example.json)
+
+[File viewers config scheme](src/models/file-viewers-config.ts)
+
### Licensing
Arvados is Free Software. See COPYING for information about Arvados Free
--- /dev/null
+[
+ {
+ "name": "File browser",
+ "extensions": [
+ ".txt",
+ ".zip"
+ ],
+ "url": "https://doc.arvados.org",
+ "filePathParam": "filePath",
+ "iconUrl": "https://material.io/tools/icons/static/icons/baseline-next_week-24px.svg"
+ },
+ {
+ "name": "Collection browser",
+ "extensions": [],
+ "collections": true,
+ "url": "https://doc.arvados.org",
+ "filePathParam": "collectionPath"
+ },
+ {
+ "name": "Universal browser",
+ "collections": true,
+ "url": "https://doc.arvados.org",
+ "filePathParam": "filePath"
+ }
+]
\ No newline at end of file
websocketUrl: string;
workbenchUrl: string;
vocabularyUrl: string;
+ fileViewersConfigUrl: string;
}
export const fetchConfig = () => {
.catch(() => Promise.resolve(getDefaultConfig()))
.then(config => Axios
.get<Config>(getDiscoveryURL(config.API_HOST))
- .then(response => ({
+ .then(response => ({
// TODO: After tests delete `|| '/vocabulary-example.json'`
- config: {...response.data, vocabularyUrl: config.VOCABULARY_URL || '/vocabulary-example.json' },
- apiHost: config.API_HOST,
+ // TODO: After tests delete `|| '/file-viewers-example.json'`
+ config: {
+ ...response.data,
+ vocabularyUrl: config.VOCABULARY_URL || '/vocabulary-example.json',
+ fileViewersConfigUrl: config.FILE_VIEWERS_CONFIG_URL || '/file-viewers-example.json'
+ },
+ apiHost: config.API_HOST,
})));
};
websocketUrl: '',
workbenchUrl: '',
vocabularyUrl: '',
+ fileViewersConfigUrl: '',
...config
});
interface ConfigJSON {
API_HOST: string;
VOCABULARY_URL: string;
+ FILE_VIEWERS_CONFIG_URL: string;
}
const getDefaultConfig = (): ConfigJSON => ({
API_HOST: process.env.REACT_APP_ARVADOS_API_HOST || "",
VOCABULARY_URL: "",
+ FILE_VIEWERS_CONFIG_URL: "",
});
const getDiscoveryURL = (apiHost: string) => `${window.location.protocol}//${apiHost}/discovery/v1/apis/arvados/v1/rest`;
{item.name}
</ListItemText>}
</ListItem>)}
- {groupIndex < items.length - 1 && <Divider />}
+ {
+ items[groupIndex + 1] &&
+ items[groupIndex + 1].length > 0 &&
+ <Divider />
+ }
</React.Fragment>)}
</List>
</Popover>;
import Mail from '@material-ui/icons/Mail';
import MoveToInbox from '@material-ui/icons/MoveToInbox';
import Notifications from '@material-ui/icons/Notifications';
+import OpenInNew from '@material-ui/icons/OpenInNew';
import People from '@material-ui/icons/People';
import Person from '@material-ui/icons/Person';
import PersonAdd from '@material-ui/icons/PersonAdd';
export const MoveToIcon: IconType = (props) => <Input {...props} />;
export const NewProjectIcon: IconType = (props) => <CreateNewFolder {...props} />;
export const NotificationIcon: IconType = (props) => <Notifications {...props} />;
+export const OpenIcon: IconType = (props) => <OpenInNew {...props} />;
export const OutputIcon: IconType = (props) => <MoveToInbox {...props} />;
export const PaginationDownIcon: IconType = (props) => <ArrowDropDown {...props} />;
export const PaginationLeftArrowIcon: IconType = (props) => <ChevronLeft {...props} />;
import { groupActionSet } from '~/views-components/context-menu/action-sets/group-action-set';
import { groupMemberActionSet } from '~/views-components/context-menu/action-sets/group-member-action-set';
import { linkActionSet } from '~/views-components/context-menu/action-sets/link-action-set';
+import { loadFileViewersConfig } from '~/store/file-viewers/file-viewers-actions';
console.log(`Starting arvados [${getBuildInfo()}]`);
store.dispatch(setCurrentTokenDialogApiHost(apiHost));
store.dispatch(setUuidPrefix(config.uuidPrefix));
store.dispatch(loadVocabulary);
+ store.dispatch(loadFileViewersConfig);
const TokenComponent = (props: any) => <ApiToken authService={services.authService} {...props} />;
const MainPanelComponent = (props: any) => <MainPanel {...props} />;
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+export type FileViewerList = FileViewer[];
+
+export interface FileViewer {
+ /**
+ * Name is used as a label in file's context menu
+ */
+ name: string;
+
+ /**
+ * Limits files for which viewer is enabled
+ * If not given, viewer will be enabled for all files
+ * Viewer is enabled if file name ends with an extension.
+ *
+ * Example: `['.zip', '.tar.gz', 'bam']`
+ */
+ extensions?: string[];
+
+ /**
+ * Determines whether a viewer is enabled for collections.
+ */
+ collections?: boolean;
+
+ /**
+ * URL that redirects to a viewer
+ * Example: `https://bam-viewer.com`
+ */
+ url: string;
+
+ /**
+ * Name of a search param that will be used to send file's path to a viewer
+ * Example:
+ *
+ * `{ filePathParam: 'filePath' }`
+ *
+ * `https://bam-viewer.com?filePath=/path/to/file`
+ */
+ filePathParam: string;
+
+ /**
+ * Icon that will display next to a label
+ */
+ iconUrl?: string;
+}
: this.webdavClient.defaults.baseURL;
return {
...file,
- url: baseUrl + file.url + '?api_token=' + this.authService.getApiToken()
+ url: baseUrl + file.url
};
}
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import Axios from 'axios';
+import { FileViewerList } from '~/models/file-viewers-config';
+
+export class FileViewersConfigService {
+ constructor(
+ private url: string
+ ) { }
+
+ get() {
+ return Axios
+ .get<FileViewerList>(this.url)
+ .then(response => response.data);
+ }
+}
import { AuthorizedKeysService } from '~/services/authorized-keys-service/authorized-keys-service';
import { VocabularyService } from '~/services/vocabulary-service/vocabulary-service';
import { NodeService } from '~/services/node-service/node-service';
+import { FileViewersConfigService } from '~/services/file-viewers-config-service/file-viewers-config-service';
export type ServiceRepository = ReturnType<typeof createServices>;
const tagService = new TagService(linkService);
const searchService = new SearchService();
const vocabularyService = new VocabularyService(config.vocabularyUrl);
+ const fileViewersConfig = new FileViewersConfigService(config.fileViewersConfigUrl);
return {
ancestorsService,
containerRequestService,
containerService,
favoriteService,
+ fileViewersConfig,
groupsService,
keepService,
linkService,
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { Dispatch } from 'redux';
+import { ServiceRepository } from '~/services/services';
+import { propertiesActions } from '~/store/properties/properties-actions';
+import { FILE_VIEWERS_PROPERTY_NAME, DEFAULT_FILE_VIEWERS } from '~/store/file-viewers/file-viewers-selectors';
+import { FileViewerList } from '~/models/file-viewers-config';
+
+export const loadFileViewersConfig = async (dispatch: Dispatch, _: {}, { fileViewersConfig }: ServiceRepository) => {
+
+ let config: FileViewerList;
+ try{
+ config = await fileViewersConfig.get();
+ } catch (e){
+ config = DEFAULT_FILE_VIEWERS;
+ }
+
+ dispatch(propertiesActions.SET_PROPERTY({
+ key: FILE_VIEWERS_PROPERTY_NAME,
+ value: config,
+ }));
+
+};
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import { PropertiesState, getProperty } from '~/store/properties/properties';
+import { FileViewerList } from '~/models/file-viewers-config';
+
+export const FILE_VIEWERS_PROPERTY_NAME = 'fileViewers';
+
+export const DEFAULT_FILE_VIEWERS: FileViewerList = [];
+export const getFileViewers = (state: PropertiesState) =>
+ getProperty<FileViewerList>(FILE_VIEWERS_PROPERTY_NAME)(state) || DEFAULT_FILE_VIEWERS;
import { RenameIcon, RemoveIcon } 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 { FileViewerActions } from '~/views-components/context-menu/actions/file-viewer-actions';
export const collectionFilesItemActionSet: ContextMenuActionSet = [[{
execute: (dispatch, resource) => {
dispatch<any>(openFileRemoveDialog(resource.uuid));
}
+}], [{
+ component: FileViewerActions,
+ execute: () => { return; },
}]];
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+import * as React from "react";
+import { ListItemText, ListItem, ListItemIcon, Icon } from "@material-ui/core";
+import { RootState } from '~/store/store';
+import { getNodeValue } from '~/models/tree';
+import { CollectionDirectory, CollectionFile, CollectionFileType } from '~/models/collection-file';
+import { FileViewerList, FileViewer } from '~/models/file-viewers-config';
+import { getFileViewers } from '~/store/file-viewers/file-viewers-selectors';
+import { connect } from 'react-redux';
+import { OpenIcon } from '~/components/icon/icon';
+
+interface FileViewerActionProps {
+ fileUrl: string;
+ viewers: FileViewerList;
+}
+
+const mapStateToProps = (state: RootState): FileViewerActionProps => {
+ const { resource } = state.contextMenu;
+ if (resource) {
+ const file = getNodeValue(resource.uuid)(state.collectionPanelFiles);
+ if (file) {
+ const fileViewers = getFileViewers(state.properties);
+ return {
+ fileUrl: file.url,
+ viewers: fileViewers.filter(enabledViewers(file)),
+ };
+ }
+ }
+ return {
+ fileUrl: '',
+ viewers: [],
+ };
+};
+
+const enabledViewers = (file: CollectionFile | CollectionDirectory) =>
+ ({ extensions, collections }: FileViewer) => {
+ if (collections && file.type === CollectionFileType.DIRECTORY) {
+ return true;
+ } else if (extensions) {
+ return extensions.some(extension => file.name.endsWith(extension));
+ } else {
+ return true;
+ }
+ };
+
+const fillViewerUrl = (fileUrl: string, { url, filePathParam }: FileViewer) => {
+ const viewerUrl = new URL(url);
+ viewerUrl.searchParams.append(filePathParam, fileUrl);
+ return viewerUrl.href;
+};
+
+export const FileViewerActions = connect(mapStateToProps)(
+ ({ fileUrl, viewers, onClick }: FileViewerActionProps & { onClick: () => void }) =>
+ <>
+ {viewers.map(viewer =>
+ <ListItem
+ button
+ component='a'
+ key={viewer.name}
+ style={{ textDecoration: 'none' }}
+ href={fillViewerUrl(fileUrl, viewer)}
+ onClick={onClick}
+ target='_blank'>
+ <ListItemIcon>
+ {
+ viewer.iconUrl
+ ? <Icon>
+ <img src={viewer.iconUrl} />
+ </Icon>
+ : <OpenIcon />
+ }
+ </ListItemIcon>
+ <ListItemText>
+ {viewer.name}
+ </ListItemText>
+ </ListItem>
+ )}
+ </>);