Unverified Commit 0050ba95 authored by Imran Salam's avatar Imran Salam Committed by GitHub
Browse files

Use Refget and thoas for instant download (#437)

parent c77d4d95
Pipeline #128088 passed with stages
in 4 minutes and 36 seconds
...@@ -24,12 +24,11 @@ import ZmenuContent from './ZmenuContent'; ...@@ -24,12 +24,11 @@ import ZmenuContent from './ZmenuContent';
import { createZmenuContent } from 'tests/fixtures/browser'; import { createZmenuContent } from 'tests/fixtures/browser';
jest.mock('./ZmenuContent', () => () => <div>ZmenuContent</div>); jest.mock('./ZmenuContent', () => () => <div>ZmenuContent</div>);
jest.mock('./ZmenuInstantDownload', () => () => (
<div>ZmenuInstantDownload</div>
));
describe('<Zmenu />', () => { describe('<Zmenu />', () => {
afterEach(() => {
jest.resetAllMocks();
});
const defaultProps: ZmenuProps = { const defaultProps: ZmenuProps = {
anchor_coordinates: { anchor_coordinates: {
x: 490, x: 490,
...@@ -47,6 +46,8 @@ describe('<Zmenu />', () => { ...@@ -47,6 +46,8 @@ describe('<Zmenu />', () => {
let wrapper: any; let wrapper: any;
beforeEach(() => { beforeEach(() => {
jest.resetAllMocks();
wrapper = mount(<Zmenu {...defaultProps} />); wrapper = mount(<Zmenu {...defaultProps} />);
}); });
......
...@@ -31,6 +31,7 @@ type Props = { ...@@ -31,6 +31,7 @@ type Props = {
}; };
const ZmenuInstantDownload = (props: Props) => { const ZmenuInstantDownload = (props: Props) => {
const genomeId = getGenomeId(props.id);
const transcriptId = getStableId(props.id); const transcriptId = getStableId(props.id);
const params = { const params = {
endpoint: `/lookup/id/${transcriptId}?content-type=application/json;expand=1`, endpoint: `/lookup/id/${transcriptId}?content-type=application/json;expand=1`,
...@@ -58,6 +59,7 @@ const ZmenuInstantDownload = (props: Props) => { ...@@ -58,6 +59,7 @@ const ZmenuInstantDownload = (props: Props) => {
return ( return (
<InstantDownloadTranscript <InstantDownloadTranscript
genomeId={genomeId}
{...preparePayload(data as TranscriptInResponse)} {...preparePayload(data as TranscriptInResponse)}
layout="vertical" layout="vertical"
/> />
...@@ -65,6 +67,7 @@ const ZmenuInstantDownload = (props: Props) => { ...@@ -65,6 +67,7 @@ const ZmenuInstantDownload = (props: Props) => {
}; };
// TODO: we may want to move this to a common helper file that deals with messaging with Genome Browser // TODO: we may want to move this to a common helper file that deals with messaging with Genome Browser
const getGenomeId = (id: string) => id.split(':').shift();
const getStableId = (id: string) => id.split(':').pop(); const getStableId = (id: string) => id.split(':').pop();
const preparePayload = (transcript: TranscriptInResponse) => { const preparePayload = (transcript: TranscriptInResponse) => {
......
...@@ -15,12 +15,12 @@ ...@@ -15,12 +15,12 @@
*/ */
import React, { useEffect } from 'react'; import React, { useEffect } from 'react';
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client'; import { ApolloProvider } from '@apollo/client';
import { connect } from 'react-redux'; import { useSelector, useDispatch } from 'react-redux';
import { replace, Replace } from 'connected-react-router'; import { replace } from 'connected-react-router';
import { useParams } from 'react-router-dom'; import { useParams } from 'react-router-dom';
import { BreakpointWidth } from 'src/global/globalConfig'; import { client } from 'src/gql-client';
import * as urlFor from 'src/shared/helpers/urlHelper'; import * as urlFor from 'src/shared/helpers/urlHelper';
import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers'; import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers';
...@@ -43,46 +43,24 @@ import GeneView from './gene-view/GeneView'; ...@@ -43,46 +43,24 @@ 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 GeneViewSidebarTabs from './gene-view/components/gene-view-sidebar-tabs/GeneViewSidebarTabs';
import { RootState } from 'src/store';
import { SidebarStatus } from 'src/content/app/entity-viewer/state/sidebar/entityViewerSidebarState';
import styles from './EntityViewer.scss'; import styles from './EntityViewer.scss';
type Props = {
isSidebarOpen: boolean;
activeGenomeId: string | null;
activeEntityId: string | null;
viewportWidth: BreakpointWidth;
replace: Replace;
setDataFromUrl: (params: EntityViewerParams) => void;
toggleSidebar: (status?: SidebarStatus) => void;
};
export type EntityViewerParams = { export type EntityViewerParams = {
genomeId?: string; genomeId?: string;
entityId?: string; entityId?: string;
}; };
const client = new ApolloClient({ const EntityViewer = () => {
uri: '/thoas', const activeGenomeId = useSelector(getEntityViewerActiveGenomeId);
cache: new InMemoryCache({ const activeEntityId = useSelector(getEntityViewerActiveEntityId);
typePolicies: { const isSidebarOpen = useSelector(isEntityViewerSidebarOpen);
Gene: { const viewportWidth = useSelector(getBreakpointWidth);
keyFields: ['stable_id'],
fields: { const dispatch = useDispatch();
slice: { const onSidebarToggle = () => dispatch(toggleSidebar());
merge: false
}
}
}
}
})
});
const EntityViewer = (props: Props) => {
const params: EntityViewerParams = useParams(); // NOTE: will likely cause a problem when server-side rendering const params: EntityViewerParams = useParams(); // NOTE: will likely cause a problem when server-side rendering
const { genomeId, entityId } = params; const { genomeId, entityId } = params;
const { activeGenomeId, activeEntityId } = props;
useEffect(() => { useEffect(() => {
if (activeGenomeId && activeEntityId && !entityId) { if (activeGenomeId && activeEntityId && !entityId) {
...@@ -91,9 +69,11 @@ const EntityViewer = (props: Props) => { ...@@ -91,9 +69,11 @@ const EntityViewer = (props: Props) => {
genomeId: activeGenomeId, genomeId: activeGenomeId,
entityId: entityIdForUrl entityId: entityIdForUrl
}); });
props.replace(replacementUrl);
dispatch(replace(replacementUrl));
} }
props.setDataFromUrl(params);
dispatch(setDataFromUrl(params));
}, [params.genomeId, params.entityId]); }, [params.genomeId, params.entityId]);
return ( return (
...@@ -109,10 +89,10 @@ const EntityViewer = (props: Props) => { ...@@ -109,10 +89,10 @@ const EntityViewer = (props: Props) => {
sidebarContent={<GeneViewSideBar />} sidebarContent={<GeneViewSideBar />}
sidebarNavigation={<GeneViewSidebarTabs />} sidebarNavigation={<GeneViewSidebarTabs />}
sidebarToolstripContent={<EntityViewerSidebarToolstrip />} sidebarToolstripContent={<EntityViewerSidebarToolstrip />}
isSidebarOpen={props.isSidebarOpen} isSidebarOpen={isSidebarOpen}
onSidebarToggle={props.toggleSidebar} onSidebarToggle={onSidebarToggle}
isDrawerOpen={false} isDrawerOpen={false}
viewportWidth={props.viewportWidth} viewportWidth={viewportWidth}
/> />
) : ( ) : (
<ExampleLinks /> <ExampleLinks />
...@@ -122,19 +102,4 @@ const EntityViewer = (props: Props) => { ...@@ -122,19 +102,4 @@ const EntityViewer = (props: Props) => {
); );
}; };
const mapStateToProps = (state: RootState) => { export default EntityViewer;
return {
activeGenomeId: getEntityViewerActiveGenomeId(state),
activeEntityId: getEntityViewerActiveEntityId(state),
isSidebarOpen: isEntityViewerSidebarOpen(state),
viewportWidth: getBreakpointWidth(state)
};
};
const mapDispatchToProps = {
replace,
setDataFromUrl,
toggleSidebar
};
export default connect(mapStateToProps, mapDispatchToProps)(EntityViewer);
...@@ -28,10 +28,22 @@ import ViewInApp from 'src/shared/components/view-in-app/ViewInApp'; ...@@ -28,10 +28,22 @@ import ViewInApp from 'src/shared/components/view-in-app/ViewInApp';
import { createGene } from 'tests/fixtures/entity-viewer/gene'; import { createGene } from 'tests/fixtures/entity-viewer/gene';
import { createTranscript } from 'tests/fixtures/entity-viewer/transcript'; import { createTranscript } from 'tests/fixtures/entity-viewer/transcript';
jest.mock('@apollo/client', () => ({
gql: jest.fn(),
useQuery: jest.fn(() => ({
data: null,
loading: true
}))
}));
jest.mock('src/shared/components/view-in-app/ViewInApp', () => () => ( jest.mock('src/shared/components/view-in-app/ViewInApp', () => () => (
<div>ViewInApp</div> <div>ViewInApp</div>
)); ));
jest.mock('src/shared/components/instant-download', () => ({
InstantDownloadTranscript: () => <div>InstantDownloadTranscript</div>
}));
const transcript = createTranscript(); const transcript = createTranscript();
const gene = createGene({ transcripts: [transcript] }); const gene = createGene({ transcripts: [transcript] });
const expandDownload = false; const expandDownload = false;
......
...@@ -34,9 +34,10 @@ import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers ...@@ -34,9 +34,10 @@ import { buildFocusIdForUrl } from 'src/shared/state/ens-object/ensObjectHelpers
import { InstantDownloadTranscript } from 'src/shared/components/instant-download'; import { InstantDownloadTranscript } from 'src/shared/components/instant-download';
import ViewInApp from 'src/shared/components/view-in-app/ViewInApp'; import ViewInApp from 'src/shared/components/view-in-app/ViewInApp';
import CloseButton from 'src/shared/components/close-button/CloseButton';
import { toggleTranscriptDownload } from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSlice'; import { toggleTranscriptDownload } from 'src/content/app/entity-viewer/state/gene-view/transcripts/geneViewTranscriptsSlice';
import { clearExpandedProteins } from 'src/content/app/entity-viewer/state/gene-view/proteins/geneViewProteinsSlice'; import { clearExpandedProteins } from 'src/content/app/entity-viewer/state/gene-view/proteins/geneViewProteinsSlice';
import CloseButton from 'src/shared/components/close-button/CloseButton';
import { Gene } from 'src/content/app/entity-viewer/types/gene'; import { Gene } from 'src/content/app/entity-viewer/types/gene';
import { Transcript } from 'src/content/app/entity-viewer/types/transcript'; import { Transcript } from 'src/content/app/entity-viewer/types/transcript';
...@@ -151,7 +152,7 @@ export const TranscriptsListItemInfo = ( ...@@ -151,7 +152,7 @@ export const TranscriptsListItemInfo = (
</span> </span>
)} )}
</div> </div>
{props.expandDownload && renderInstantDownload(props)} {props.expandDownload && renderInstantDownload({ ...props, genomeId })}
</div> </div>
<div className={transcriptsListStyles.right}> <div className={transcriptsListStyles.right}>
<div className={styles.transcriptName}> <div className={styles.transcriptName}>
...@@ -166,12 +167,16 @@ export const TranscriptsListItemInfo = ( ...@@ -166,12 +167,16 @@ export const TranscriptsListItemInfo = (
}; };
const renderInstantDownload = ({ const renderInstantDownload = ({
transcript,
gene, gene,
transcript genomeId
}: TranscriptsListItemInfoProps) => { }: TranscriptsListItemInfoProps & {
genomeId: string;
}) => {
return ( return (
<div className={styles.download}> <div className={styles.download}>
<InstantDownloadTranscript <InstantDownloadTranscript
genomeId={genomeId}
transcript={{ transcript={{
id: transcript.unversioned_stable_id, id: transcript.unversioned_stable_id,
so_term: transcript.so_term so_term: transcript.so_term
......
...@@ -15,6 +15,7 @@ ...@@ -15,6 +15,7 @@
*/ */
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { useParams } from 'react-router-dom';
import set from 'lodash/fp/set'; import set from 'lodash/fp/set';
import { CircleLoader } from 'src/shared/components/loader/Loader'; import { CircleLoader } from 'src/shared/components/loader/Loader';
...@@ -64,6 +65,9 @@ const addProteinDomains = ( ...@@ -64,6 +65,9 @@ const addProteinDomains = (
const ProteinsListItemInfo = (props: Props) => { const ProteinsListItemInfo = (props: Props) => {
const { transcript, trackLength } = props; const { transcript, trackLength } = props;
const params: { [key: string]: string } = useParams();
const { genomeId } = params;
const [ const [
transcriptWithProteinDomains, transcriptWithProteinDomains,
setTranscriptWithProteinDomains setTranscriptWithProteinDomains
...@@ -199,6 +203,7 @@ const ProteinsListItemInfo = (props: Props) => { ...@@ -199,6 +203,7 @@ const ProteinsListItemInfo = (props: Props) => {
)} )}
<div className={styles.downloadWrapper}> <div className={styles.downloadWrapper}>
<InstantDownloadProtein <InstantDownloadProtein
genomeId={genomeId}
transcriptId={transcript.unversioned_stable_id} transcriptId={transcript.unversioned_stable_id}
/> />
</div> </div>
......
...@@ -18,4 +18,5 @@ export type CDNA = { ...@@ -18,4 +18,5 @@ export type CDNA = {
start: number; start: number;
end: number; end: number;
length: number; length: number;
sequence_checksum?: string;
}; };
...@@ -21,4 +21,5 @@ export type CDS = { ...@@ -21,4 +21,5 @@ export type CDS = {
relative_end: number; relative_end: number;
protein_length: number; protein_length: number;
nucleotide_length: number; nucleotide_length: number;
sequence_checksum?: string;
}; };
...@@ -53,6 +53,7 @@ export type Product = { ...@@ -53,6 +53,7 @@ export type Product = {
length: number; length: number;
protein_domains: ProteinDomain[]; protein_domains: ProteinDomain[];
external_references: ExternalReference[]; external_references: ExternalReference[];
sequence_checksum?: string;
}; };
export type ProteinDomain = { export type ProteinDomain = {
......
/**
* 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 { ApolloClient, InMemoryCache } from '@apollo/client';
export const client = new ApolloClient({
uri: '/thoas',
cache: new InMemoryCache({
typePolicies: {
Gene: {
keyFields: ['stable_id'],
fields: {
slice: {
merge: false
}
}
}
}
})
});
...@@ -20,42 +20,60 @@ import { ...@@ -20,42 +20,60 @@ import {
ProteinOption, ProteinOption,
proteinOptionsOrder proteinOptionsOrder
} from 'src/shared/components/instant-download/instant-download-protein/InstantDownloadProtein'; } from 'src/shared/components/instant-download/instant-download-protein/InstantDownloadProtein';
import {
fetchTranscriptChecksums,
TranscriptChecksums
} from './fetchSequenceChecksums';
type FetchPayload = { type FetchPayload = {
genomeId: string;
transcriptId: string; transcriptId: string;
options: ProteinOptions; options: ProteinOptions;
}; };
export const fetchForProtein = async (payload: FetchPayload) => { export const fetchForProtein = async (payload: FetchPayload) => {
const { transcriptId, options } = payload; const { genomeId, transcriptId, options } = payload;
const urls = buildUrlsForProtein(transcriptId, options); const productGeneratingContext = await fetchTranscriptChecksums({
genomeId,
transcriptId
});
const urls = buildUrlsForProtein(productGeneratingContext, options);
const sequencePromises = urls.map((url) => const sequencePromises = urls.map((url) =>
fetch(url).then((response) => response.text()) fetch(url).then((response) => response.text())
); );
const sequences = await Promise.all(sequencePromises); const sequences = await Promise.all(sequencePromises);
const combinedFasta = sequences.join('\n'); const combinedFasta = sequences.join('\n\n');
downloadAsFile(combinedFasta, `${transcriptId}.fasta`, { downloadAsFile(combinedFasta, `${transcriptId}.fasta`, {
type: 'text/x-fasta' type: 'text/x-fasta'
}); });
}; };
const buildUrlsForProtein = (id: string, options: ProteinOptions) => { const buildUrlsForProtein = (
productGeneratingContext: TranscriptChecksums,
options: ProteinOptions
) => {
return options return options
? proteinOptionsOrder ? proteinOptionsOrder
.filter((option) => options[option]) .filter((option) => options[option])
.map((option) => buildFetchUrl(id, option)) .map((option) => buildFetchUrl(productGeneratingContext, option))
: []; : [];
}; };
const buildFetchUrl = (id: string, sequenceType: ProteinOption) => { const buildFetchUrl = (
const sequenceTypeToTypeParam: Record<ProteinOption, string> = { productGeneratingContext: TranscriptChecksums,
proteinSequence: 'protein', sequenceType: ProteinOption
) => {
const sequenceTypeToContextType: Record<ProteinOption, string> = {
proteinSequence: 'product',
cds: 'cds' cds: 'cds'
}; };
const typeParam = sequenceTypeToTypeParam[sequenceType]; const contextType = sequenceTypeToContextType[
sequenceType
] as keyof TranscriptChecksums;
const checksum = productGeneratingContext[contextType]?.sequence_checksum;
return `https://rest.ensembl.org/sequence/id/${id}?content-type=text/x-fasta&type=${typeParam}`; return `/refget/sequence/${checksum}?accept=text/x-fasta`;
}; };
...@@ -15,11 +15,16 @@ ...@@ -15,11 +15,16 @@
*/ */
import downloadAsFile from 'src/shared/helpers/downloadAsFile'; import downloadAsFile from 'src/shared/helpers/downloadAsFile';
import { import {
TranscriptOptions, TranscriptOptions,
TranscriptOption, TranscriptOption,
transcriptOptionsOrder transcriptOptionsOrder
} from 'src/shared/components/instant-download/instant-download-transcript/InstantDownloadTranscript'; } from 'src/shared/components/instant-download/instant-download-transcript/InstantDownloadTranscript';
import {
fetchTranscriptChecksums,
TranscriptChecksums
} from './fetchSequenceChecksums';
type Options = { type Options = {
transcript: Partial<TranscriptOptions>; transcript: Partial<TranscriptOptions>;
...@@ -29,26 +34,34 @@ type Options = { ...@@ -29,26 +34,34 @@ type Options = {
}; };
type FetchPayload = { type FetchPayload = {
transcriptId: string; genomeId: string;
geneId: string; geneId: string;
transcriptId: string;
options: Options; options: Options;
}; };