import React, { FunctionComponent, useEffect } from 'react'; import { Route, Redirect, Switch } from 'react-router-dom'; import { inject } from 'mobx-react'; import Container from 'appShell/App/Container'; import { handleIndexDO, handleCaseDO, restoreRouteAfterRedirect, handleStudyDO, handleLinkOut, handleEncodedRedirect, redirectTo, } from './shared/lib/redirectHelpers'; import PageNotFound from './shared/components/pageNotFound/PageNotFound'; import { parse } from 'qs'; import _ from 'lodash'; /* HOW TO ADD A NEW ROUTE * 1. Import the "page" component using SuspenseWrapper and React.lazy as seen in imports below * 2. Add a Route element with `component` prop set to the component, wrapped in LocationValidationWrapper * which validates the parameters of a page and goes to an error page if it fails. Other wrappers like * ScrollToTop modify the page in other ways and may also be used here. */ // import page components here // NOTE: to lazy load these, we use the bundle loader. what we are importing are not the components but loaders // which are invoked at run time by the routes // webpack knows to 'split' the code into seperate bundles accordingly // see article http://henleyedition.com/implicit-code-splitting-with-react-router-and-webpack/ const PatientViewPage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/patientView/PatientViewPage')) ); const ResultsViewPage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/resultsView/ResultsViewPage')) ); import TestimonialsPage from 'pages/staticPages/testimonialsPage/TestimonialsPage'; import GroupComparisonLoading from './pages/groupComparison/GroupComparisonLoading'; const DatasetPage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/staticPages/datasetView/DatasetPage')) ); const Homepage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/home/HomePage')) ); const StudyViewPage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/studyView/StudyViewPage')) ); const MutationMapperTool = SuspenseWrapper( React.lazy(() => // @ts-ignore import('./pages/staticPages/tools/mutationMapper/MutationMapperTool') ) ); const OncoprinterTool = SuspenseWrapper( React.lazy(() => // @ts-ignore import('./pages/staticPages/tools/oncoprinter/OncoprinterTool') ) ); const Visualize = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/staticPages/visualize/Visualize')) ); const InstallationMap = SuspenseWrapper( React.lazy(() => // @ts-ignore import('./pages/staticPages/installations/InstallationMap') ) ); const Software = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/staticPages/software/Software')) ); const GroupComparisonPage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/groupComparison/GroupComparisonPage')) ); const ErrorPage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/resultsView/ErrorPage')) ); const WebAPIPage = SuspenseWrapper( // @ts-ignore React.lazy(() => import('./pages/staticPages/webAPI/WebAPIPage')) ); import $ from 'jquery'; import { getBrowserWindow } from 'cbioportal-frontend-commons'; import { seekUrlHash } from 'shared/lib/seekUrlHash'; import { PagePath } from 'shared/enums/PagePaths'; import { LegacyResultsViewComparisonSubTab, ResultsViewComparisonSubTab, ResultsViewTab, } from 'pages/resultsView/ResultsViewPageHelpers'; import { StudyViewPageTabKeyEnum, StudyViewResourceTabPrefix, } from 'pages/studyView/StudyViewPageTabs'; import { PatientViewPageTabs, PatientViewResourceTabPrefix, } from 'pages/patientView/PatientViewPageTabs'; import { GroupComparisonTab } from 'pages/groupComparison/GroupComparisonTabs'; import { CLIN_ATTR_DATA_TYPE } from 'pages/resultsView/plots/PlotsTabUtils'; import { SpecialAttribute } from 'shared/cache/ClinicalDataCache'; import { AlterationTypeConstants } from 'shared/constants'; import { cnaGroup, mutationGroup, } from 'shared/lib/comparison/ComparisonStoreUtils'; import { MapValues } from 'shared/lib/TypeScriptUtils'; import { ResultsViewURLQuery } from 'pages/resultsView/ResultsViewURLWrapper'; import { EnumDeclaration, EnumType } from 'typescript'; function SuspenseWrapper(Component: any) { return (props: any) => ( ); } function LocationValidationWrapper( Component: any, validator: (params: any) => boolean, queryParamsAdjuster?: (oldParams: any) => any | undefined ) { return (props: any) => { if (props.location) { if (queryParamsAdjuster) { const query = parse(props.location.search, { depth: 0, ignoreQueryPrefix: true, }); const adjustedQuery = queryParamsAdjuster(query); if (adjustedQuery) { // redirect to adjusted query if there's a change const RedirectingBlankPage = getBlankPage(() => { redirectTo(adjustedQuery, props.location.pathname); }); return ; } } if ( !( validator(props.match.params) || customTabParamValidator(props.location) ) ) { return ; } } return ; }; } function ResultsViewQueryParamsAdjuster(oldParams: ResultsViewURLQuery) { let changeMade = false; const newParams = _.cloneDeep(oldParams); if ( newParams.comparison_subtab === LegacyResultsViewComparisonSubTab.MUTATIONS ) { newParams.comparison_subtab = ResultsViewComparisonSubTab.ALTERATIONS; newParams.comparison_selectedEnrichmentEventTypes = JSON.stringify([ ...mutationGroup, ]); changeMade = true; } else if ( newParams.comparison_subtab === LegacyResultsViewComparisonSubTab.CNA ) { newParams.comparison_subtab = ResultsViewComparisonSubTab.ALTERATIONS; newParams.comparison_selectedEnrichmentEventTypes = JSON.stringify([ ...cnaGroup, ]); changeMade = true; } // we used to call the structural variant alteration class "fusions" // we have generalized it to structural variants const profileRegex = /fusion/; if (profileRegex.test(oldParams.profileFilter)) { newParams.profileFilter = oldParams.profileFilter.replace( profileRegex, 'structural_variants' ); changeMade = true; } if (changeMade) { return newParams; } else { return undefined; } } function ScrollToTop(Component: any, onLoad = () => {}) { return (props: any) => { useEffect(() => { $(document).scrollTop(0); onLoad(); }, []); return ; }; } function GoToHashLink(Component: any) { return (props: any) => { useEffect(handleEnter, []); return ; }; } /** * Validates that the parameters either do not have * a tab parameter, or have a parameter that matches a * value in `tabEnum` * @param tabEnum a TypeScript string enum */ function tabParamValidator(tabEnum: any) { return function(params: any) { return !params.tab || Object.values(tabEnum).indexOf(params.tab) > -1; }; } function comparisonTabParamValidator() { // comparison tab includes generic assay tabs which is not predictable // validate tabs by checking it's degined in GroupComparisonTab // or validate generic assay tabs by checking if it starts with GroupComparisonTab.GENERIC_ASSAY_PREFIX return function(params: any) { return ( !params.tab || Object.values(GroupComparisonTab).indexOf(params.tab) > -1 || params.tab.startsWith(GroupComparisonTab.GENERIC_ASSAY_PREFIX) ); }; } /** * Validates results page and patient page custom tabs * @param location */ function customTabParamValidator(location: Location) { const patientViewRegex = new RegExp( `patient\/${PatientViewResourceTabPrefix}.+` ); const studyViewRegex = new RegExp(`study\/${StudyViewResourceTabPrefix}.+`); const customTabRegex = /.+\/customTab\d/; return ( patientViewRegex.test(location.pathname) || studyViewRegex.test(location.pathname) || customTabRegex.test(location.pathname) ); } var restoreRoute = inject('routing')(restoreRouteAfterRedirect); let getBlankPage = function(onLoad: any) { return (props: any) => { if (onLoad) { useEffect(() => { onLoad(); // make sure that useEffect argument doesn't return anything }, []); } return
; }; }; let redirectToNews: FunctionComponent = function() { getBrowserWindow().location = 'https://docs.cbioportal.org/news'; return null; }; const externalRedirect = function(url: string) { let redirectComp: FunctionComponent<{}> = function() { getBrowserWindow().location = url; return null; }; return redirectComp; }; /* when route changes, we want to: 1. in spa, deep links from url (#) don't work because content is loading and thus doesn't exist to link to at time url changes. seekHash is a somewhat dirty way of solving this issue 2, when there's no hash, we want to make sure we scroll to top because user considers herself on a "new page" */ function handleEnter() { const hash = getBrowserWindow().location.hash; if (hash.length > 0) { seekUrlHash(hash.replace('#', '')); } else { $(document).scrollTop(0); } } // we want to preload ResultsViewPage to prevent delay due to lazy loading bundle // note: because we bundle, and bundles are loaded async, this does NOT affect time to render of default route // results will load in background while user plays with query interface function preloadImportantComponents() { setTimeout(() => { //@ts-ignore import('./pages/resultsView/ResultsViewPage'); //@ts-ignore import('./pages/studyView/StudyViewPage'); }, 2000); } export const makeRoutes = () => { return ( {/* Redirect legacy survival route directly to survival tab in comparison */} { redirectTo( { comparison_subtab: 'survival' }, '/results/comparison' ); })} /> {/* Redirect legacy expression route directly to plots tab with mrna vs study */} { redirectTo( { plots_horz_selection: JSON.stringify({ dataType: CLIN_ATTR_DATA_TYPE, selectedDataSourceOption: SpecialAttribute.StudyOfOrigin, }), plots_vert_selection: JSON.stringify({ dataType: AlterationTypeConstants.MRNA_EXPRESSION, logScale: 'true', }), }, `/results/${ResultsViewTab.PLOTS}` ); })} /> {/* Redirect legacy enrichments route directly to mutations tab in comparison */} { redirectTo( { comparison_subtab: 'mutations' }, '/results/comparison' ); })} /> // legacy pages redirect to docs site ( ))} /> ); }; export default makeRoutes;