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