From: Michal Klobukowski Date: Mon, 17 Dec 2018 16:10:26 +0000 (+0100) Subject: Merge branch 'master' into 13540-add-possibility-to-open-files-in-third-party-apps X-Git-Tag: 1.4.0~71^2~17^2 X-Git-Url: https://git.arvados.org/arvados-workbench2.git/commitdiff_plain/ec33904efe960ec3e3bddb668d892463171e50bd?hp=0d0b7399b2e4906e26dbb5035bed033cec0646e9 Merge branch 'master' into 13540-add-possibility-to-open-files-in-third-party-apps refs #13540 Arvados-DCO-1.1-Signed-off-by: Michal Klobukowski --- diff --git a/README.md b/README.md index e8d77701..425d1787 100644 --- a/README.md +++ b/README.md @@ -41,7 +41,8 @@ Currently this configuration schema is supported: ``` { "API_HOST": "string", - "VOCABULARY_URL": "string" + "VOCABULARY_URL": "string", + "FILE_VIEWERS_CONFIG_URL": "string", } ``` @@ -49,6 +50,13 @@ Currently this configuration schema is supported: 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 diff --git a/public/file-viewers-example.json b/public/file-viewers-example.json new file mode 100644 index 00000000..27adb70b --- /dev/null +++ b/public/file-viewers-example.json @@ -0,0 +1,25 @@ +[ + { + "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 diff --git a/src/common/config.ts b/src/common/config.ts index b7b89bd9..db67ed8d 100644 --- a/src/common/config.ts +++ b/src/common/config.ts @@ -50,6 +50,7 @@ export interface Config { websocketUrl: string; workbenchUrl: string; vocabularyUrl: string; + fileViewersConfigUrl: string; } export const fetchConfig = () => { @@ -59,10 +60,15 @@ export const fetchConfig = () => { .catch(() => Promise.resolve(getDefaultConfig())) .then(config => Axios .get(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, }))); }; @@ -111,17 +117,20 @@ export const mockConfig = (config: Partial): Config => ({ 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`; diff --git a/src/components/context-menu/context-menu.tsx b/src/components/context-menu/context-menu.tsx index 4068251b..98456dad 100644 --- a/src/components/context-menu/context-menu.tsx +++ b/src/components/context-menu/context-menu.tsx @@ -53,7 +53,11 @@ export class ContextMenu extends React.PureComponent { {item.name} } )} - {groupIndex < items.length - 1 && } + { + items[groupIndex + 1] && + items[groupIndex + 1].length > 0 && + + } )} ; diff --git a/src/components/icon/icon.tsx b/src/components/icon/icon.tsx index c077f7a6..2bd16970 100644 --- a/src/components/icon/icon.tsx +++ b/src/components/icon/icon.tsx @@ -38,6 +38,7 @@ import MoreVert from '@material-ui/icons/MoreVert'; 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'; @@ -83,6 +84,7 @@ export const MoreOptionsIcon: IconType = (props) => ; export const MoveToIcon: IconType = (props) => ; export const NewProjectIcon: IconType = (props) => ; export const NotificationIcon: IconType = (props) => ; +export const OpenIcon: IconType = (props) => ; export const OutputIcon: IconType = (props) => ; export const PaginationDownIcon: IconType = (props) => ; export const PaginationLeftArrowIcon: IconType = (props) => ; diff --git a/src/index.tsx b/src/index.tsx index 508fa7c3..7cd4ae9a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -59,6 +59,7 @@ import { apiClientAuthorizationActionSet } from '~/views-components/context-menu 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()}]`); @@ -106,6 +107,7 @@ fetchConfig() store.dispatch(setCurrentTokenDialogApiHost(apiHost)); store.dispatch(setUuidPrefix(config.uuidPrefix)); store.dispatch(loadVocabulary); + store.dispatch(loadFileViewersConfig); const TokenComponent = (props: any) => ; const MainPanelComponent = (props: any) => ; diff --git a/src/models/file-viewers-config.ts b/src/models/file-viewers-config.ts new file mode 100644 index 00000000..e95116ba --- /dev/null +++ b/src/models/file-viewers-config.ts @@ -0,0 +1,47 @@ +// 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; +} diff --git a/src/services/collection-service/collection-service.ts b/src/services/collection-service/collection-service.ts index b0d5cb14..f0f25a2d 100644 --- a/src/services/collection-service/collection-service.ts +++ b/src/services/collection-service/collection-service.ts @@ -56,7 +56,7 @@ export class CollectionService extends TrashableResourceService(this.url) + .then(response => response.data); + } +} diff --git a/src/services/services.ts b/src/services/services.ts index 59fe2d47..78ea714b 100644 --- a/src/services/services.ts +++ b/src/services/services.ts @@ -30,6 +30,7 @@ import { RepositoriesService } from '~/services/repositories-service/repositorie 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; @@ -64,6 +65,7 @@ export const createServices = (config: Config, actions: ApiActions) => { const tagService = new TagService(linkService); const searchService = new SearchService(); const vocabularyService = new VocabularyService(config.vocabularyUrl); + const fileViewersConfig = new FileViewersConfigService(config.fileViewersConfigUrl); return { ancestorsService, @@ -76,6 +78,7 @@ export const createServices = (config: Config, actions: ApiActions) => { containerRequestService, containerService, favoriteService, + fileViewersConfig, groupsService, keepService, linkService, diff --git a/src/store/file-viewers/file-viewers-actions.ts b/src/store/file-viewers/file-viewers-actions.ts new file mode 100644 index 00000000..44bfd2fd --- /dev/null +++ b/src/store/file-viewers/file-viewers-actions.ts @@ -0,0 +1,25 @@ +// 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, + })); + +}; diff --git a/src/store/file-viewers/file-viewers-selectors.ts b/src/store/file-viewers/file-viewers-selectors.ts new file mode 100644 index 00000000..abc9d3d1 --- /dev/null +++ b/src/store/file-viewers/file-viewers-selectors.ts @@ -0,0 +1,12 @@ +// 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(FILE_VIEWERS_PROPERTY_NAME)(state) || DEFAULT_FILE_VIEWERS; 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 b5564891..94f702e8 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 @@ -6,6 +6,7 @@ import { ContextMenuActionSet } from "../context-menu-action-set"; 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 = [[{ @@ -23,4 +24,7 @@ export const collectionFilesItemActionSet: ContextMenuActionSet = [[{ execute: (dispatch, resource) => { dispatch(openFileRemoveDialog(resource.uuid)); } +}], [{ + component: FileViewerActions, + execute: () => { return; }, }]]; diff --git a/src/views-components/context-menu/actions/file-viewer-actions.tsx b/src/views-components/context-menu/actions/file-viewer-actions.tsx new file mode 100644 index 00000000..961e1828 --- /dev/null +++ b/src/views-components/context-menu/actions/file-viewer-actions.tsx @@ -0,0 +1,81 @@ +// 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 => + + + { + viewer.iconUrl + ? + + + : + } + + + {viewer.name} + + + )} + );