Unverified Commit 09cba961 authored by Imran Salam's avatar Imran Salam Committed by GitHub
Browse files

Instant download gene (#464)

parent 1ee42299
Pipeline #142020 passed with stages
in 7 minutes and 17 seconds
......@@ -4,7 +4,7 @@
display: grid;
grid-template-columns: [label] 120px [value] auto;
grid-column-gap: 10px;
margin-bottom:3px;
margin-bottom: 3px;
.label {
grid-column: label;
......@@ -17,8 +17,7 @@
}
}
.spaceAbove{
.spaceAbove {
margin-top: 30px;
}
......@@ -38,10 +37,32 @@
.featureSymbol {
font-weight: $bold;
}
.stableId{
.stableId {
font-weight: $bold;
}
.featureSymbol + .stableId{
.featureSymbol + .stableId {
margin-left: 12px;
font-weight: $light;
}
.downloadRow {
> .value {
display: flex;
}
}
.downloadLink {
color: $blue;
cursor: pointer;
}
.downloadWrapper {
margin-left: 40px;
position: relative;
}
.closeButton {
position: absolute;
right: 0;
top: 0;
}
......@@ -14,10 +14,11 @@
* limitations under the License.
*/
import React from 'react';
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { gql, useQuery } from '@apollo/client';
import { Pick3 } from 'ts-multipick';
import classNames from 'classnames';
import * as urlFor from 'src/shared/helpers/urlHelper';
import { getFormattedLocation } from 'src/shared/helpers/formatters/regionFormatter';
......@@ -28,7 +29,9 @@ import {
} from 'src/shared/state/ens-object/ensObjectHelpers';
import { getBrowserActiveEnsObject } from 'src/content/app/browser/browserSelectors';
import InstantDownloadGene from 'src/shared/components/instant-download/instant-download-gene/InstantDownloadGene';
import ViewInApp from 'src/shared/components/view-in-app/ViewInApp';
import CloseButton from 'src/shared/components/close-button/CloseButton';
import { EnsObjectGene } from 'src/shared/state/ens-object/ensObjectTypes';
import { FullGene } from 'src/shared/types/thoas/gene';
......@@ -75,6 +78,7 @@ type Gene = Pick<
const GeneSummary = () => {
const ensObjectGene = useSelector(getBrowserActiveEnsObject) as EnsObjectGene;
const [shouldShowDownload, showDownload] = useState(false);
const { data, loading } = useQuery<{ gene: Gene }>(GENE_QUERY, {
variables: {
......@@ -105,6 +109,8 @@ const GeneSummary = () => {
entityId: focusId
});
const rowClasses = classNames(styles.row, styles.spaceAbove);
return (
<div>
<div className={styles.row}>
......@@ -122,13 +128,13 @@ const GeneSummary = () => {
</div>
</div>
<div className={`${styles.row} ${styles.spaceAbove}`}>
<div className={rowClasses}>
<div className={styles.label}>Gene name</div>
<div className={styles.value}>{gene.name}</div>
</div>
{gene.alternative_symbols.length > 0 && (
<div className={`${styles.row} ${styles.spaceAbove}`}>
<div className={rowClasses}>
<div className={styles.label}>Synonyms</div>
<div className={styles.value}>
{gene.alternative_symbols.join(', ')}
......@@ -136,13 +142,36 @@ const GeneSummary = () => {
</div>
)}
<div className={`${styles.row} ${styles.spaceAbove}`}>
<div className={rowClasses}>
<div className={styles.value}>
{`${gene.transcripts.length} transcripts`}
</div>
</div>
<div className={`${styles.row} ${styles.spaceAbove}`}>
<div className={classNames(rowClasses, styles.downloadRow)}>
<div className={styles.value}>
<div
className={styles.downloadLink}
onClick={() => showDownload(!shouldShowDownload)}
>
Download
</div>
{shouldShowDownload && (
<div className={styles.downloadWrapper}>
<InstantDownloadGene
genomeId={ensObjectGene.genome_id}
gene={{ id: gene.stable_id, so_term: gene.so_term }}
/>
<CloseButton
className={styles.closeButton}
onClick={() => showDownload(false)}
/>
</div>
)}
</div>
</div>
<div className={rowClasses}>
<div className={styles.value}>
<ViewInApp links={{ entityViewer: { url: entityViewerUrl } }} />
</div>
......
......@@ -44,7 +44,7 @@ import EntityViewerSidebarModal from 'src/content/app/entity-viewer/shared/compo
import EntityViewerTopbar from './shared/components/entity-viewer-topbar/EntityViewerTopbar';
import ExampleLinks from './components/example-links/ExampleLinks';
import GeneView from './gene-view/GeneView';
import GeneViewSideBar from './gene-view/components/gene-view-sidebar/GeneViewSideBar';
import GeneViewSidebar from './gene-view/components/gene-view-sidebar/GeneViewSideBar';
import GeneViewSidebarTabs from './gene-view/components/gene-view-sidebar-tabs/GeneViewSidebarTabs';
import styles from './EntityViewer.scss';
......@@ -84,7 +84,7 @@ const EntityViewer = () => {
const SideBarContent = isSidebarModalOpen ? (
<EntityViewerSidebarModal />
) : (
<GeneViewSideBar />
<GeneViewSidebar />
);
return (
......
......@@ -49,7 +49,7 @@ import { View } from 'src/content/app/entity-viewer/state/gene-view/view/geneVie
import transcriptsListStyles from '../DefaultTranscriptsList.scss';
import styles from './TranscriptsListItemInfo.scss';
type Gene = Pick<FullGene, 'unversioned_stable_id'>;
type Gene = Pick<FullGene, 'unversioned_stable_id' | 'stable_id'>;
type Transcript = Pick<
FullTranscript,
'stable_id' | 'unversioned_stable_id' | 'symbol' | 'so_term'
......@@ -208,10 +208,10 @@ const renderInstantDownload = ({
<InstantDownloadTranscript
genomeId={genomeId}
transcript={{
id: transcript.unversioned_stable_id,
id: transcript.stable_id,
so_term: transcript.so_term
}}
gene={{ id: gene.unversioned_stable_id }}
gene={{ id: gene.stable_id }}
/>
</div>
);
......
......@@ -36,6 +36,7 @@ import Tabs, { Tab } from 'src/shared/components/tabs/Tabs';
import styles from './GeneViewSidebarTabs.scss';
const tabsData: Tab[] = [];
Object.values(SidebarTabName).forEach((value) =>
tabsData.push({
title: value
......
......@@ -15,33 +15,26 @@
*/
import React from 'react';
import { connect } from 'react-redux';
import { useSelector } from 'react-redux';
import GeneOverview from 'src/content/app/entity-viewer/gene-view/components/gene-view-sidebar/overview/GeneOverview';
import GeneExternalReferences from 'src/content/app/entity-viewer/gene-view/components/gene-view-sidebar/external-references/GeneExternalReferences';
import { getEntityViewerSidebarTabName } from 'src/content/app/entity-viewer/state/sidebar/entityViewerSidebarSelectors';
import { RootState } from 'src/store';
import { SidebarTabName } from 'src/content/app/entity-viewer/state/sidebar/entityViewerSidebarState';
type Props = {
activeTabName: SidebarTabName | null;
};
const GeneViewSidebar = () => {
const activeTabName = useSelector(getEntityViewerSidebarTabName);
const GeneViewSidebar = (props: Props) => {
return (
<>
{props.activeTabName === SidebarTabName.OVERVIEW && <GeneOverview />}
{props.activeTabName === SidebarTabName.EXTERNAL_REFERENCES && (
{activeTabName === SidebarTabName.OVERVIEW && <GeneOverview />}
{activeTabName === SidebarTabName.EXTERNAL_REFERENCES && (
<GeneExternalReferences />
)}
</>
);
};
const mapStateToProps = (state: RootState) => ({
activeTabName: getEntityViewerSidebarTabName(state)
});
export default connect(mapStateToProps)(GeneViewSidebar);
export default GeneViewSidebar;
......@@ -15,13 +15,55 @@
*/
import React from 'react';
import { useSelector } from 'react-redux';
import { gql, useQuery } from '@apollo/client';
const EntityViewerSidebarDownloads = () => (
<section>
<h3>Downloads</h3>
<p>Export your browser configurations as images or data</p>
<p>Not ready yet &hellip;</p>
</section>
);
import {
getEntityViewerActiveEntityId,
getEntityViewerActiveGenomeId
} from 'src/content/app/entity-viewer/state/general/entityViewerGeneralSelectors';
import { parseEnsObjectId } from 'src/shared/state/ens-object/ensObjectHelpers';
import InstantDownloadGene from 'src/shared/components/instant-download/instant-download-gene/InstantDownloadGene';
import styles from './EntityViewerDownloads.scss';
const QUERY = gql`
query Gene($genomeId: String!, $entityId: String!) {
gene(byId: { genome_id: $genomeId, stable_id: $entityId }) {
stable_id
so_term
}
}
`;
const EntityViewerSidebarDownloads = () => {
const genomeId = useSelector(getEntityViewerActiveGenomeId);
const geneId = useSelector(getEntityViewerActiveEntityId);
const entityId = geneId ? parseEnsObjectId(geneId).objectId : null;
const { data } = useQuery<{ gene: { stable_id: string; so_term: string } }>(
QUERY,
{
variables: { genomeId, entityId }
}
);
if (!data) {
return null;
}
return (
<section className={styles.container}>
<h3>Download</h3>
<InstantDownloadGene
genomeId={genomeId as string}
gene={{ id: data.gene.stable_id, so_term: data.gene.so_term }}
/>
</section>
);
};
export default EntityViewerSidebarDownloads;
/**
* 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';
const PersonalData = () => {
return (
<section className="personaData">
<h3>Personal Data</h3>
<p>Upload your own data to be displayed in the browser</p>
<p>Not ready yet &hellip;</p>
</section>
);
};
export default PersonalData;
......@@ -69,7 +69,6 @@ export const EntityViewerSidebarToolstrip = () => {
status={Status.DISABLED}
description="Search"
className={styles.sidebarIcon}
key="search"
onClick={noop}
image={searchIcon}
/>
......@@ -90,11 +89,10 @@ export const EntityViewerSidebarToolstrip = () => {
image={shareIcon}
/>
<ImageButton
status={Status.DISABLED}
description="Downloads"
status={getViewIconStatus(SidebarModalView.DOWNLOADS)}
description="Download"
className={styles.sidebarIcon}
key="downloads"
onClick={noop}
onClick={() => toggleModalView(SidebarModalView.DOWNLOADS)}
image={downloadIcon}
/>
</>
......
......@@ -27,7 +27,7 @@ import { isEntityViewerSidebarOpen } from 'src/content/app/entity-viewer/state/s
import {
EntityViewerSidebarGenomeState,
SidebarTabName,
SidebarStatus,
ToggleStatus,
EntityViewerSidebarUIState,
SidebarModalView
} from './entityViewerSidebarState';
......@@ -59,7 +59,7 @@ export const setSidebarTabName = (
};
export const toggleSidebar = (
status?: SidebarStatus
status?: ToggleStatus
): ThunkAction<void, any, null, Action<string>> => (
dispatch,
getState: () => RootState
......
......@@ -27,10 +27,10 @@ export enum SidebarTabName {
export enum SidebarModalView {
SEARCH = 'search',
BOOKMARKS = 'bookmarks',
DOWNLOADS = 'downloads'
DOWNLOADS = 'download'
}
export type SidebarStatus = Status.OPEN | Status.CLOSED;
export type ToggleStatus = Status.OPEN | Status.CLOSED;
export type EntityViewerSidebarState = Readonly<{
[genomeId: string]: EntityViewerSidebarGenomeState;
......@@ -43,7 +43,7 @@ export type EntityViewerSidebarUIState = {
};
export type EntityViewerSidebarGenomeState = Readonly<{
status: SidebarStatus;
status: ToggleStatus;
selectedTabName: SidebarTabName;
entities: {
[entityId: string]: {
......
/**
* 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 { wrap } from 'comlink';
import downloadAsFile from 'src/shared/helpers/downloadAsFile';
import { TranscriptOptions } from '../instant-download-transcript/InstantDownloadTranscript';
import { fetchGeneSequenceMetadata } from './fetchSequenceChecksums';
import { WorkerApi } from 'src/shared/workers/sequenceFetcher.worker';
import {
getGenomicSequenceData,
prepareDownloadParameters
} from './fetchForTranscript';
type GeneOptions = {
transcript: Partial<TranscriptOptions>;
gene: {
genomicSequence: boolean;
};
};
type FetchPayload = {
genomeId: string;
geneId: string;
options: GeneOptions;
};
export const fetchForGene = async (payload: FetchPayload) => {
const {
genomeId,
geneId,
options: { transcript: transcriptOptions, gene: geneOptions }
} = payload;
const geneSequenceData = await fetchGeneSequenceMetadata({
genomeId,
geneId
});
const sequenceDownloadParams = geneSequenceData.transcripts.flatMap(
(transcript) =>
prepareDownloadParameters({
transcriptSequenceData: transcript,
options: transcriptOptions
})
);
if (geneOptions.genomicSequence) {
sequenceDownloadParams.unshift(
getGenomicSequenceData(
geneSequenceData.stable_id,
geneSequenceData.unversioned_stable_id
)
);
}
const worker = new Worker('src/shared/workers/sequenceFetcher.worker', {
type: 'module'
});
const service = wrap<WorkerApi>(worker);
const sequences = await service.downloadSequences(sequenceDownloadParams);
worker.terminate();
downloadAsFile(sequences, `${geneId}.fasta`, {
type: 'text/x-fasta'
});
};
......@@ -70,6 +70,15 @@ type PrepareDownloadParametersParams = {
options: ProteinOptions;
};
// map of field names received from component to field names returned when fetching checksums
const labelTypeToSequenceType: Record<
ProteinOption,
keyof Omit<TranscriptSequenceMetadata, 'stable_id' | 'unversioned_stable_id'>
> = {
proteinSequence: 'protein',
cds: 'cds'
};
const prepareDownloadParameters = (params: PrepareDownloadParametersParams) => {
const { transcriptSequenceData } = params;
return proteinOptionsOrder
......@@ -88,12 +97,3 @@ const prepareDownloadParameters = (params: PrepareDownloadParametersParams) => {
})
.filter(Boolean) as SingleSequenceFetchParams[];
};
// map of field names received from component to field names returned when fetching checksums
const labelTypeToSequenceType: Record<
ProteinOption,
keyof TranscriptSequenceMetadata
> = {
proteinSequence: 'protein',
cds: 'cds'
};
......@@ -25,6 +25,7 @@ import {
} from 'src/shared/components/instant-download/instant-download-transcript/InstantDownloadTranscript';
import {
fetchTranscriptSequenceMetadata,
fetchGeneWithoutTranscriptsSequenceMetadata,
TranscriptSequenceMetadata
} from './fetchSequenceChecksums';
......@@ -59,13 +60,18 @@ export const fetchForTranscript = async (payload: FetchPayload) => {
transcriptId
});
const sequenceDownloadParams = prepareDownloadParameters({
transcriptId,
transcriptSequenceData,
options: transcriptOptions
});
if (geneOptions.genomicSequence) {
sequenceDownloadParams.push(getGenomicSequenceData(geneId));
const metadata = await fetchGeneWithoutTranscriptsSequenceMetadata({
genomeId,
geneId
});
sequenceDownloadParams.unshift(
getGenomicSequenceData(metadata.stable_id, metadata.unversioned_stable_id)
);
}
const worker = new Worker('src/shared/workers/sequenceFetcher.worker', {
......@@ -84,7 +90,6 @@ export const fetchForTranscript = async (payload: FetchPayload) => {
};
type PrepareDownloadParametersParams = {
transcriptId: string;
transcriptSequenceData: TranscriptSequenceMetadata;
options: Partial<TranscriptOptions>;
};
......@@ -92,27 +97,38 @@ type PrepareDownloadParametersParams = {
// map of field names received from component to field names returned when fetching checksums