Unverified Commit 0c6c0f92 authored by Jyothish's avatar Jyothish Committed by GitHub
Browse files

Add interstitial screen for Genome Browser and Entity Viewer (#510)

* Building the interstitials for the apps
* Show the interstitial on the EntityViewer page
* Fix tests
* Update styles for the BrowserInterstitialInstructions component
* Add nullability to a storage getter
* Add hide the in-app-search input field in production environment
* Reorder imports
parent 9190a407
Pipeline #171804 passed with stages
in 11 minutes and 59 seconds
......@@ -44,6 +44,9 @@ jest.mock('./track-panel/TrackPanel', () => () => (
jest.mock('./browser-app-bar/BrowserAppBar', () => () => (
<div className="browserAppBar">BrowserAppBar</div>
));
jest.mock('./interstitial/BrowserInterstitial', () => () => (
<div className="browserInterstitial">BrowserInterstitial</div>
));
jest.mock('./track-panel/track-panel-bar/TrackPanelBar', () => () => (
<div className="trackPanelBar">TrackPanelBar</div>
));
......@@ -84,29 +87,17 @@ describe('<Browser />', () => {
);
describe('rendering', () => {
test('does not render when no activeGenomeId', () => {
it('renders an interstitial if no species is selected', () => {
const { container } = mountBrowserComponent({ activeGenomeId: null });
expect(container.innerHTML).toBeFalsy();
expect(container.querySelector('.browserInterstitial')).toBeTruthy();
});
test('renders links to example objects only if there is no selected focus feature', () => {
const { container, rerender } = mountBrowserComponent();
expect(container.querySelectorAll('.exampleLinks')).toHaveLength(1);
rerender(
<Browser
{...defaultProps}
browserQueryParams={{
focus: faker.lorem.words()
}}
/>
);
expect(container.querySelectorAll('.exampleLinks')).toHaveLength(0);
it('renders an interstitial if no feature has been selected', () => {
const { container } = mountBrowserComponent();
expect(container.querySelector('.browserInterstitial')).toBeTruthy();
});
test('renders the genome browser and track panel only when there is a selected focus feature', () => {
it('renders the genome browser and track panel only when there is a selected focus feature', () => {
const { container, rerender } = mountBrowserComponent();
expect(container.querySelectorAll('.browserImage')).toHaveLength(0);
......
......@@ -60,6 +60,7 @@ import Drawer from './drawer/Drawer';
import { StandardAppLayout } from 'src/shared/components/layout';
import ErrorBoundary from 'src/shared/components/error-boundary/ErrorBoundary';
import { NewTechError } from 'src/shared/components/error-screen';
import BrowserInterstitial from './interstitial/BrowserInterstitial';
import { RootState } from 'src/store';
import { ChrLocation } from './browserState';
......@@ -107,10 +108,6 @@ export const Browser = (props: BrowserProps) => {
const shouldShowNavBar =
props.browserActivated && props.browserNavOpenState && !isDrawerOpened;
if (!props.activeGenomeId) {
return null;
}
const mainContent = (
<>
{shouldShowNavBar && <BrowserNavBar />}
......@@ -122,7 +119,7 @@ export const Browser = (props: BrowserProps) => {
<ApolloProvider client={client}>
<div className={styles.browserInnerWrapper}>
<BrowserAppBar onSpeciesSelect={changeGenomeId} />
{props.browserQueryParams.focus ? (
{props.activeGenomeId && props.browserQueryParams.focus ? (
<StandardAppLayout
mainContent={mainContent}
sidebarContent={<TrackPanel />}
......@@ -137,7 +134,7 @@ export const Browser = (props: BrowserProps) => {
viewportWidth={props.viewportWidth}
/>
) : (
<ExampleObjectLinks {...props} />
<BrowserInterstitial />
)}
</div>
</ApolloProvider>
......
......@@ -62,10 +62,14 @@ const BrowserAppBar = (props: BrowserAppBarProps) => {
/>
);
const mainContent = props.activeGenomeId
? wrappedSpecies
: 'To start using this app...';
return (
<AppBar
appName={AppName.GENOME_BROWSER}
mainContent={wrappedSpecies}
mainContent={mainContent}
aside={<HelpPopupButton slug="genome-browser" />}
/>
);
......
@import 'src/styles/common';
// TODO: this panel is the same as in SpeciesSelector, and in EntityViewer.
// We should extract it in a component (or at least reuse the same CSS classes)
.topPanel {
background-color: $light-grey;
height: 235px;
padding: 65px 40px;
padding-left: $global-padding-left;
box-shadow: 0 3px 5px $global-box-shadow;
}
.searchField {
max-width: 485px;
height: 30px;
}
.exampleLinks {
display: flex;
flex-direction: column;
gap: 1rem;
margin-top: 40px;
padding-left: $global-padding-left;
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { Link } from 'react-router-dom';
import { getBrowserActiveGenomeId } from 'src/content/app/browser/browserSelectors';
import { getExampleEnsObjects } from 'src/shared/state/ens-object/ensObjectSelectors';
import {
parseEnsObjectId,
buildFocusIdForUrl
} from 'src/shared/state/ens-object/ensObjectHelpers';
import * as urlFor from 'src/shared/helpers/urlHelper';
import BrowserInterstitialInstructions from './browser-interstitial-instructions/BrowserInterstitialInstructions';
import InAppSearch from 'src/shared/components/in-app-search/InAppSearch';
import styles from './BrowserInterstitial.scss';
const BrowserInterstitial = () => {
const activeGenomeId = useSelector(getBrowserActiveGenomeId);
if (!activeGenomeId) {
return <BrowserInterstitialInstructions />;
}
return (
<div>
<div className={styles.topPanel}>
<InAppSearch className={styles.searchField} />
</div>
<ExampleLinks />
</div>
);
};
const ExampleLinks = () => {
const activeGenomeId = useSelector(getBrowserActiveGenomeId);
const ensObjects = useSelector(getExampleEnsObjects);
const links = ensObjects.map((exampleObject) => {
const parsedEnsObjectId = parseEnsObjectId(exampleObject.object_id);
const focusId = buildFocusIdForUrl(parsedEnsObjectId);
const path = urlFor.browser({
genomeId: activeGenomeId,
focus: focusId
});
return (
<div key={exampleObject.object_id}>
<Link to={path} replace>
Example {exampleObject.type}
</Link>
</div>
);
});
return <div className={styles.exampleLinks}>{links}</div>;
};
export default BrowserInterstitial;
@import 'src/styles/common';
$columnWidth: 230px;
$columnGap: 60px;
$rowGap: 60px;
$leftMargin: 40px;
.instructionsPanel {
display: flex;
background-color: $light-grey;
min-height: 235px;
padding: 29px $global-padding-left;
box-shadow: 0 3px 5px $global-box-shadow;
}
.instructionsWrapper {
margin-left: $leftMargin;
display: grid;
grid-template-columns: repeat(4, #{$columnWidth});
grid-column-gap: $columnGap;
grid-row-gap: $rowGap;
max-width: calc(4 * #{$columnWidth} + 3 * #{$columnGap});
}
.description {
margin: 25px 40px;
display: flex;
align-items: center;
.iconLabel {
margin-left: 12px;
}
}
.imageButtonIcon {
width: 32px;
height: 32px;
background-color: $dark-grey;
svg {
fill: $white;
}
}
.searchButtonIcon {
width: 30px;
height: 30px;
svg {
fill: $dark-grey;
}
}
.searchDescription {
margin: 14px 36px;
color: $dark-grey;
.exampleText {
margin: 10px 4px;
}
}
.speciesSelectorButton {
margin-left: 40px;
align-self: start;
justify-self: start;
}
@media (max-width: 2 * $global-padding-left + $leftMargin + 4 * $columnWidth + 3 * $columnGap) {
.instructionsWrapper {
grid-template-columns: repeat(2, #{$columnWidth});
}
.speciesSelectorButton {
margin-left: 0;
}
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useDispatch } from 'react-redux';
import { push } from 'connected-react-router';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { Step } from 'src/shared/components/step/Step';
import { ImageButton } from 'src/shared/components/image-button/ImageButton';
import { PrimaryButton } from 'src/shared/components/button/Button';
import { Status } from 'src/shared/types/status';
import { ReactComponent as SpeciesSelectorIcon } from 'static/img/launchbar/species-selector.svg';
import { ReactComponent as BrowserIcon } from 'static/img/launchbar/browser.svg';
import { ReactComponent as SearchIcon } from 'static/img/sidebar/search.svg';
import styles from './BrowserInterstitialInstructions.scss';
const BrowserInterstitialInstructions = () => {
const dispatch = useDispatch();
const goToSpeciesSelector = () => {
const url = urlFor.speciesSelector();
dispatch(push(url));
};
return (
<section className={styles.instructionsPanel}>
<div className={styles.instructionsWrapper}>
<div className={styles.stepWrapper}>
<Step count={1} title="Find and add a species" />
<div className={styles.description}>
<ImageButton
className={styles.imageButtonIcon}
status={Status.DISABLED}
image={SpeciesSelectorIcon}
/>
<div className={styles.iconLabel}>Species Selector</div>
</div>
</div>
<div className={styles.stepWrapper}>
<Step count={2} title="Return to this app" />
<div className={styles.description}>
<ImageButton
className={styles.imageButtonIcon}
status={Status.DISABLED}
image={BrowserIcon}
/>
<div className={styles.iconLabel}>Genome Browser</div>
</div>
</div>
<div className={styles.stepWrapper}>
<Step
count={3}
title="Use Search or the example links to view a gene or region"
/>
<div className={styles.searchDescription}>
<ImageButton
className={styles.searchButtonIcon}
status={Status.DISABLED}
image={SearchIcon}
/>
<div className={styles.exampleText}>Example gene</div>
<div className={styles.exampleText}>Example region</div>
</div>
</div>
<PrimaryButton
className={styles.speciesSelectorButton}
onClick={goToSpeciesSelector}
>
Go to Species Selector
</PrimaryButton>
</div>
</section>
);
};
export default BrowserInterstitialInstructions;
......@@ -45,7 +45,7 @@ import EntityViewerAppBar from './shared/components/entity-viewer-app-bar/Entity
import EntityViewerSidebarToolstrip from './shared/components/entity-viewer-sidebar/entity-viewer-sidebar-toolstrip/EntityViewerSidebarToolstrip';
import EntityViewerSidebarModal from 'src/content/app/entity-viewer/shared/components/entity-viewer-sidebar/entity-viewer-sidebar-modal/EntityViewerSidebarModal';
import EntityViewerTopbar from './shared/components/entity-viewer-topbar/EntityViewerTopbar';
import ExampleLinks from './components/example-links/ExampleLinks';
import EntityViewerInterstitial from './interstitial/EntityViewerInterstitial';
import GeneView from './gene-view/GeneView';
import GeneViewSidebar from './gene-view/components/gene-view-sidebar/GeneViewSideBar';
import GeneViewSidebarTabs from './gene-view/components/gene-view-sidebar-tabs/GeneViewSidebarTabs';
......@@ -104,7 +104,7 @@ const EntityViewer = () => {
viewportWidth={viewportWidth}
/>
) : (
<ExampleLinks />
<EntityViewerInterstitial />
)}
</div>
</ApolloProvider>
......
......@@ -2,14 +2,7 @@
.exampleLinks {
margin-top: 50px;
padding-left: 120px;
&__emptyTopbar {
height: 40px; // same as top bar height in StandardAppLayout
background: $light-grey;
box-shadow: 0 2px 3px $grey;
}
padding-left: 120px;
a {
margin-bottom: 30px;
......
......@@ -85,7 +85,6 @@ const ExampleLinks = () => {
return (
<div>
<div className={styles.exampleLinks__emptyTopbar} />
<div className={styles.exampleLinks}>
<Link to={path}>Example gene</Link>
</div>
......
@import 'src/styles/common';
// TODO: this panel is the same as in SpeciesSelector, and in EntityViewer.
// We should extract it in a component (or at least reuse the same CSS classes)
.topPanel {
background-color: $light-grey;
height: 235px;
padding: 65px 40px;
padding-left: $global-padding-left;
box-shadow: 0 3px 5px $global-box-shadow;
}
.searchField {
max-width: 485px;
height: 30px;
}
/**
* See the NOTICE file distributed with this work for additional information
* regarding copyright ownership.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { getEntityViewerActiveGenomeId } from 'src/content/app/entity-viewer/state/general/entityViewerGeneralSelectors';
import EntityViewerInterstitialInstructions from './entity-viewer-interstitial-instructions/EntityViewerInterstitialInstructions';
import ExampleLinks from 'src/content/app/entity-viewer/components/example-links/ExampleLinks';
import InAppSearch from 'src/shared/components/in-app-search/InAppSearch';
import styles from './EntityViewerInterstitial.scss';
const EntityViewerInterstitial = () => {
const activeGenomeId = useSelector(getEntityViewerActiveGenomeId);
if (!activeGenomeId) {
return <EntityViewerInterstitialInstructions />;
}
return (
<div>
<div className={styles.topPanel}>
<InAppSearch className={styles.searchField} />
</div>
<ExampleLinks />
</div>
);
};
export default EntityViewerInterstitial;
@import 'src/styles/common';
$columnWidth: 230px;
$columnGap: 60px;
$rowGap: 60px;
$leftMargin: 40px;
.instructionsPanel {
display: flex;
background-color: $light-grey;
min-height: 235px;
padding: 29px $global-padding-left;
box-shadow: 0 3px 5px $global-box-shadow;
}
.instructionsWrapper {
margin-left: $leftMargin;
display: grid;
grid-template-columns: repeat(4, #{$columnWidth});
grid-column-gap: $columnGap;
grid-row-gap: $rowGap;
max-width: calc(4 * #{$columnWidth} + 3 * #{$columnGap});
}
.description {
margin: 25px 40px;
display: flex;
align-items: center;
.iconLabel {
margin-left: 12px;
}
}
.imageButtonIcon {
width: 32px;
height: 32px;
background-color: $dark-grey;
svg {
fill: $white;
}
}
.searchButtonIcon {
width: 30px;
height: 30px;
svg {
fill: $dark-grey;
}
}
.searchDescription {
margin: 14px 36px;
color: $dark-grey;
.exampleText {
margin: 10px 4px;
}
}
.speciesSelectorButton {
margin-left: 40px;
align-self: start;
justify-self: start;
}
@media (max-width: 2 * $global-padding-left + $leftMargin + 4 * $columnWidth + 3 * $columnGap) {