import React, { useEffect, useState } from 'react'; import PropTypes from 'prop-types'; import { useNavigate } from 'react-router-dom'; import { useTranslation } from 'react-i18next'; import { useLocation } from 'react-router'; import { SidePanel, ErrorBoundary, UserPreferences, AboutModal, Header, useModal, LoadingIndicatorProgress, } from '@ohif/ui'; import i18n from '@ohif/i18n'; import { ServicesManager, HangingProtocolService, hotkeys, CommandsManager, } from '@ohif/core'; import { useAppConfig } from '@state'; import Toolbar from '../Toolbar/Toolbar'; const { availableLanguages, defaultLanguage, currentLanguage } = i18n; function ViewerLayout({ // From Extension Module Params extensionManager, servicesManager, hotkeysManager, commandsManager, // From Modes viewports, ViewportGridComp, leftPanels = [], rightPanels = [], leftPanelDefaultClosed = false, rightPanelDefaultClosed = false, }): React.FunctionComponent { const [appConfig] = useAppConfig(); const navigate = useNavigate(); const location = useLocation(); const onClickReturnButton = () => { const { pathname } = location; const dataSourceIdx = pathname.indexOf('/', 1); // const search = // dataSourceIdx === -1 // ? undefined // : `datasources=${pathname.substring(dataSourceIdx + 1)}`; // Todo: Handle parameters in a better way. const query = new URLSearchParams(window.location.search); const configUrl = query.get('configUrl'); const dataSourceName = pathname.substring(dataSourceIdx + 1); const existingDataSource = extensionManager.getDataSources(dataSourceName); const searchQuery = new URLSearchParams(); if (dataSourceIdx !== -1 && existingDataSource) { searchQuery.append('datasources', pathname.substring(dataSourceIdx + 1)); } if (configUrl) { searchQuery.append('configUrl', configUrl); } navigate({ pathname: '/', search: decodeURIComponent(searchQuery.toString()), }); }; const { t } = useTranslation(); const { show, hide } = useModal(); const [showLoadingIndicator, setShowLoadingIndicator] = useState( appConfig.showLoadingIndicator ); const { hangingProtocolService } = servicesManager.services; const { hotkeyDefinitions, hotkeyDefaults } = hotkeysManager; const versionNumber = process.env.VERSION_NUMBER; const commitHash = process.env.COMMIT_HASH; const menuOptions = [ { title: t('Header:About'), icon: 'info', onClick: () => show({ content: AboutModal, title: 'About OHIF Viewer', contentProps: { versionNumber, commitHash }, }), }, { title: t('Header:Preferences'), icon: 'settings', onClick: () => show({ title: t('UserPreferencesModal:User Preferences'), content: UserPreferences, contentProps: { hotkeyDefaults: hotkeysManager.getValidHotkeyDefinitions( hotkeyDefaults ), hotkeyDefinitions, currentLanguage: currentLanguage(), availableLanguages, defaultLanguage, onCancel: () => { hotkeys.stopRecord(); hotkeys.unpause(); hide(); }, onSubmit: ({ hotkeyDefinitions, language }) => { i18n.changeLanguage(language.value); hotkeysManager.setHotkeys(hotkeyDefinitions); hide(); }, onReset: () => hotkeysManager.restoreDefaultBindings(), hotkeysModule: hotkeys, }, }), }, ]; if (appConfig.oidc) { menuOptions.push({ title: t('Header:Logout'), icon: 'power-off', onClick: async () => { navigate( `/logout?redirect_uri=${encodeURIComponent(window.location.href)}` ); }, }); } /** * Set body classes (tailwindcss) that don't allow vertical * or horizontal overflow (no scrolling). Also guarantee window * is sized to our viewport. */ useEffect(() => { document.body.classList.add('bg-black'); document.body.classList.add('overflow-hidden'); return () => { document.body.classList.remove('bg-black'); document.body.classList.remove('overflow-hidden'); }; }, []); const getComponent = id => { const entry = extensionManager.getModuleEntry(id); if (!entry) { throw new Error( `${id} is not a valid entry for an extension module, please check your configuration or make sure the extension is registered.` ); } let content; if (entry && entry.component) { content = entry.component; } else { throw new Error( `No component found from extension ${id}. Check the reference string to the extension in your Mode configuration` ); } return { entry, content }; }; const getPanelData = id => { const { content, entry } = getComponent(id); return { id: entry.id, iconName: entry.iconName, iconLabel: entry.iconLabel, label: entry.label, name: entry.name, content, }; }; useEffect(() => { const { unsubscribe } = hangingProtocolService.subscribe( HangingProtocolService.EVENTS.PROTOCOL_CHANGED, // Todo: right now to set the loading indicator to false, we need to wait for the // hangingProtocolService to finish applying the viewport matching to each viewport, // however, this might not be the only approach to set the loading indicator to false. we need to explore this further. () => { setShowLoadingIndicator(false); } ); return () => { unsubscribe(); }; }, [hangingProtocolService]); const getViewportComponentData = viewportComponent => { const { entry } = getComponent(viewportComponent.namespace); return { component: entry.component, displaySetsToDisplay: viewportComponent.displaySetsToDisplay, }; }; const leftPanelComponents = leftPanels.map(getPanelData); const rightPanelComponents = rightPanels.map(getPanelData); const viewportComponents = viewports.map(getViewportComponentData); return (
{showLoadingIndicator && ( )} {/* LEFT SIDEPANELS */} {leftPanelComponents.length ? ( ) : null} {/* TOOLBAR + GRID */}
{rightPanelComponents.length ? ( ) : null}
); } ViewerLayout.propTypes = { // From extension module params extensionManager: PropTypes.shape({ getModuleEntry: PropTypes.func.isRequired, }).isRequired, commandsManager: PropTypes.instanceOf(CommandsManager), servicesManager: PropTypes.instanceOf(ServicesManager), // From modes leftPanels: PropTypes.array, rightPanels: PropTypes.array, leftPanelDefaultClosed: PropTypes.bool.isRequired, rightPanelDefaultClosed: PropTypes.bool.isRequired, /** Responsible for rendering our grid of viewports; provided by consuming application */ children: PropTypes.oneOfType([PropTypes.node, PropTypes.func]).isRequired, }; export default ViewerLayout;