Unverified Commit 2b1b6fc1 authored by Andrey Azov's avatar Andrey Azov Committed by GitHub
Browse files

Use RTK-query to get gene data for track panel (#563)

parent c5089410
Pipeline #191401 passed with stages
in 4 minutes and 44 seconds
......@@ -25,6 +25,7 @@
"express": "4.17.1",
"filesize": "7.0.0",
"graphql": "15.5.1",
"graphql-request": "3.5.0",
"http-proxy-middleware": "2.0.1",
"lodash": "4.17.21",
"query-string": "7.0.1",
......@@ -13297,8 +13298,7 @@
"node_modules/asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"node_modules/at-least-node": {
"version": "1.0.0",
......@@ -15922,7 +15922,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"dependencies": {
"delayed-stream": "~1.0.0"
},
......@@ -18198,7 +18197,6 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true,
"engines": {
"node": ">=0.4.0"
}
......@@ -20298,6 +20296,17 @@
"node": ">=0.10.0"
}
},
"node_modules/extract-files": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
"integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ==",
"engines": {
"node": "^10.17.0 || ^12.0.0 || >= 13.7.0"
},
"funding": {
"url": "https://github.com/sponsors/jaydenseric"
}
},
"node_modules/faker": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz",
......@@ -20978,7 +20987,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"dependencies": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
......@@ -21883,6 +21891,19 @@
"node": ">= 10.x"
}
},
"node_modules/graphql-request": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.5.0.tgz",
"integrity": "sha512-Io89QpfU4rqiMbqM/KwMBzKaDLOppi8FU8sEccCE4JqCgz95W9Q8bvxQ4NfPALLSMvg9nafgg8AkYRmgKSlukA==",
"dependencies": {
"cross-fetch": "^3.0.6",
"extract-files": "^9.0.0",
"form-data": "^3.0.0"
},
"peerDependencies": {
"graphql": "14.x || 15.x"
}
},
"node_modules/graphql-tag": {
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.4.tgz",
......@@ -52754,8 +52775,7 @@
"asynckit": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=",
"dev": true
"integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k="
},
"at-least-node": {
"version": "1.0.0",
......@@ -54848,7 +54868,6 @@
"version": "1.0.8",
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
"dev": true,
"requires": {
"delayed-stream": "~1.0.0"
}
......@@ -56671,8 +56690,7 @@
"delayed-stream": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=",
"dev": true
"integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk="
},
"delegates": {
"version": "1.0.0",
......@@ -58342,6 +58360,11 @@
}
}
},
"extract-files": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/extract-files/-/extract-files-9.0.0.tgz",
"integrity": "sha512-CvdFfHkC95B4bBBk36hcEmvdR2awOdhhVUYH6S/zrVj3477zven/fJMYg7121h4T1xHZC+tetUpubpAhxwI7hQ=="
},
"faker": {
"version": "5.5.3",
"resolved": "https://registry.npmjs.org/faker/-/faker-5.5.3.tgz",
......@@ -58867,7 +58890,6 @@
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz",
"integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==",
"dev": true,
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
......@@ -59560,6 +59582,16 @@
"resolved": "https://registry.npmjs.org/graphql/-/graphql-15.5.1.tgz",
"integrity": "sha512-FeTRX67T3LoE3LWAxxOlW2K3Bz+rMYAC18rRguK4wgXaTZMiJwSUwDmPFo3UadAKbzirKIg5Qy+sNJXbpPRnQw=="
},
"graphql-request": {
"version": "3.5.0",
"resolved": "https://registry.npmjs.org/graphql-request/-/graphql-request-3.5.0.tgz",
"integrity": "sha512-Io89QpfU4rqiMbqM/KwMBzKaDLOppi8FU8sEccCE4JqCgz95W9Q8bvxQ4NfPALLSMvg9nafgg8AkYRmgKSlukA==",
"requires": {
"cross-fetch": "^3.0.6",
"extract-files": "^9.0.0",
"form-data": "^3.0.0"
}
},
"graphql-tag": {
"version": "2.12.4",
"resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.12.4.tgz",
......@@ -47,6 +47,7 @@
"express": "4.17.1",
"filesize": "7.0.0",
"graphql": "15.5.1",
"graphql-request": "3.5.0",
"http-proxy-middleware": "2.0.1",
"lodash": "4.17.21",
"query-string": "7.0.1",
......
......@@ -21,6 +21,7 @@
.value {
grid-column: value;
// width: calc(100vw - 320px - 45px - 46px - 220px); // FIXME: decide whether we want to explicitly set the max-width
& > span + span {
margin-left: 10px;
......@@ -47,19 +48,24 @@
}
.value .featureDetails {
.transcriptQuality {
margin-left: 10px;
font-weight: $light;
}
.featureDetails {
display: flex;
flex-wrap: wrap;
align-items: center;
white-space: nowrap;
flex-wrap: nowrap;
overflow: hidden;
gap: 10px;
}
& > span:not(:nth-child(1)) {
margin-left: 40px;
}
& > span:nth-child(2) {
margin-left: 10px;
}
.featureDetail {
flex: 0 0 auto;
display: flex;
align-items: center;
flex-wrap: nowrap;
padding-right: 30px;
}
.featureSymbol {
......@@ -87,7 +93,7 @@
.downloadWrapper{
display: inline-block;
margin-left: 40px;
min-width: 600px;
min-width: 300px;
position: relative;
}
......
......@@ -25,7 +25,6 @@ import {
getDisplayStableId,
buildFocusIdForUrl
} from 'src/shared/state/ens-object/ensObjectHelpers';
import { getBrowserActiveEnsObject } from 'src/content/app/browser/browserSelectors';
import { getCommaSeparatedNumber } from 'src/shared/helpers/formatters/numberFormatter';
import { getGeneName } from 'src/shared/helpers/formatters/geneFormatter';
......@@ -35,6 +34,9 @@ import {
getSplicedRNALength
} from 'src/content/app/entity-viewer/shared/helpers/entity-helpers';
import { useGetTrackPanelGeneQuery } from 'src/content/app/browser/state/genomeBrowserApiSlice';
import { getBrowserActiveEnsObject } from 'src/content/app/browser/browserSelectors';
import { InstantDownloadTranscript } from 'src/shared/components/instant-download';
import ViewInApp from 'src/shared/components/view-in-app/ViewInApp';
import ExternalReference from 'src/shared/components/external-reference/ExternalReference';
......@@ -156,10 +158,16 @@ const GENE_AND_TRANSCRIPT_QUERY = gql`
const TranscriptSummary = () => {
const ensObjectGene = useSelector(getBrowserActiveEnsObject) as EnsObjectGene;
const [shouldShowDownload, showDownload] = useState(false);
const transcriptTrack = ensObjectGene?.track?.child_tracks?.[0];
const { data: geneData } = useGetTrackPanelGeneQuery({
genomeId: ensObjectGene.genome_id,
geneId: ensObjectGene.stable_id
});
const [shouldShowDownload, showDownload] = useState(false);
const canonicalTranscript = geneData?.gene.transcripts.find(
(transcript) => transcript.metadata.canonical
);
const { data, loading } = useQuery<{
gene: Gene;
......@@ -167,10 +175,10 @@ const TranscriptSummary = () => {
}>(GENE_AND_TRANSCRIPT_QUERY, {
variables: {
geneId: ensObjectGene.stable_id,
transcriptId: transcriptTrack?.stable_id,
transcriptId: canonicalTranscript?.stable_id,
genomeId: ensObjectGene.genome_id
},
skip: !transcriptTrack?.stable_id
skip: !canonicalTranscript?.stable_id
});
if (loading) {
......@@ -224,22 +232,31 @@ const TranscriptSummary = () => {
<div className={styles.label}>Transcript</div>
<div className={styles.value}>
<div className={styles.featureDetails}>
<span className={styles.featureSymbol}>{stableId}</span>
<span className={styles.label}>
<TranscriptQualityLabel metadata={metadata} />
</span>
<div className={styles.featureDetail}>
<span className={styles.featureSymbol}>{stableId}</span>
<div className={styles.transcriptQuality}>
<TranscriptQualityLabel metadata={metadata} />
</div>
</div>
{metadata.biotype && (
<>
<div className={styles.featureDetail}>
<span>{metadata.biotype.label}</span>
<div className={styles.questionButton}>
<QuestionButton helpText={metadata.biotype.definition} />
</div>
</>
</div>
)}
{transcript.slice.strand.code && (
<span>{getStrandDisplayName(transcript.slice.strand.code)}</span>
<div className={styles.featureDetail}>
<span>
{getStrandDisplayName(transcript.slice.strand.code)}
</span>
</div>
)}
<span>{getFormattedLocation(ensObjectGene.location)}</span>
<div className={styles.featureDetail}>
<span>{getFormattedLocation(ensObjectGene.location)}</span>
</div>
</div>
</div>
</div>
......@@ -322,6 +339,7 @@ const TranscriptSummary = () => {
}}
gene={{ id: gene.unversioned_stable_id }}
theme="light"
layout="vertical"
/>
<CloseButton
className={styles.closeButton}
......
/**
* 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 { createApi } from '@reduxjs/toolkit/query/react';
import { request, ClientError } from 'graphql-request';
import trackPanelGeneQuery from './queries/trackPanelGeneQuery';
import type { TrackPanelGene } from './types/track-panel-gene';
// FIXME: move some place more generic
const graphqlBaseQuery =
({ baseUrl }: { baseUrl: string }) =>
async ({ body }: { body: string }) => {
try {
const result = await request(baseUrl, body);
return { data: result };
} catch (error) {
if (error instanceof ClientError) {
return { error: { status: error.response.status, data: error } };
}
return { error: { status: 500, data: error } };
}
};
type GeneQueryParams = { genomeId: string; geneId: string };
export const genomeBrowserApiSlice = createApi({
reducerPath: 'genomeBrowserApi',
baseQuery: graphqlBaseQuery({
baseUrl: '/api/thoas'
}),
endpoints: (builder) => ({
getTrackPanelGene: builder.query<{ gene: TrackPanelGene }, GeneQueryParams>(
{
query: (params) => ({
body: trackPanelGeneQuery(params)
})
}
)
})
});
export const { getTrackPanelGene } = genomeBrowserApiSlice.endpoints;
export const { useGetTrackPanelGeneQuery } = genomeBrowserApiSlice;
/**
* 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 'graphql-request';
type Params = {
genomeId: string;
geneId: string;
};
const trackPanelGeneQuery = (params: Params) => gql`
query TrackPanelGene {
gene(byId: { genome_id: "${params.genomeId}", stable_id: "${params.geneId}" }) {
stable_id
unversioned_stable_id
symbol
slice {
region {
name
}
location {
start
end
}
strand {
code
}
}
metadata {
biotype {
label
}
}
transcripts {
stable_id
slice {
location {
length
}
}
product_generating_contexts {
product_type
}
metadata {
biotype {
label
}
canonical {
value
label
}
mane {
value
label
ncbi_transcript {
id
url
}
}
}
}
}
}
`;
export default trackPanelGeneQuery;
/**
* 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 { Pick2, Pick3 } from 'ts-multipick';
import type { FullGene } from 'src/shared/types/thoas/gene';
import type { FullTranscript } from 'src/shared/types/thoas/transcript';
type GeneFields = Pick<
FullGene,
'stable_id' | 'unversioned_stable_id' | 'symbol'
>;
type GeneMetadata = Pick3<FullGene, 'metadata', 'biotype', 'label'>;
type GeneSlice = Pick3<FullGene, 'slice', 'region', 'name'> &
Pick3<FullGene, 'slice', 'location', 'start' | 'end'> &
Pick3<FullGene, 'slice', 'strand', 'code'>;
type TranscriptFields = Pick<FullTranscript, 'stable_id'>;
type TranscriptSlice = Pick3<FullTranscript, 'slice', 'location', 'length'>;
type TranscriptPGCs = Pick3<
FullTranscript,
'product_generating_contexts',
number,
'product_type'
>;
type TranscriptMetadata = Pick3<
FullTranscript,
'metadata',
'biotype',
'label'
> &
Pick2<FullTranscript, 'metadata', 'canonical'> & // FIXME: this is not quite right
Pick2<FullTranscript, 'metadata', 'mane'>; // FIXME: this is not quite right
export type TrackPanelTranscript = TranscriptFields &
TranscriptSlice &
TranscriptPGCs &
TranscriptMetadata;
export type TrackPanelGene = GeneFields &
GeneMetadata &
GeneSlice & {
transcripts: TrackPanelTranscript[];
};
/**
* 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 { useGetTrackPanelGeneQuery } from 'src/content/app/browser/state/genomeBrowserApiSlice';
import { getBrowserTrackStates } from 'src/content/app/browser/browserSelectors';
import { getTranscriptMetadata as getTranscriptSupportLevel } from 'src/content/app/entity-viewer/shared/components/default-transcript-label/TranscriptQualityLabel';
import TrackPanelListItem from './TrackPanelListItem';
import { Status } from 'src/shared/types/status';
import type { TrackActivityStatus } from 'src/content/app/browser/track-panel/trackPanelConfig';
import type { EnsObjectTrack } from 'src/shared/state/ens-object/ensObjectTypes';
import type {
TrackPanelGene as TrackPanelGeneType,
TrackPanelTranscript as TrackPanelTranscriptType
} from 'src/content/app/browser/state/types/track-panel-gene';
type TrackPanelGeneProps = {
genomeId: string;
geneId: string;
ensObjectId: string;
};
// TODO: change track ids
const GENE_TRACK_ID = 'track:gene-feat';
const TRANSCRIPT_TRACK_ID = 'track:gene-feat-1';
const TrackPanelGene = (props: TrackPanelGeneProps) => {
const { genomeId, geneId, ensObjectId } = props;
const { data } = useGetTrackPanelGeneQuery({
genomeId,
geneId
});
const trackStates = useSelector(getBrowserTrackStates);
const trackStatusGetter = prepareTrackStatusGetter({
trackStates,
genomeId,
objectId: ensObjectId,
categoryName: 'main'
});
if (!data) {
return null;
}
const { gene } = data;
const geneTrackStatus = trackStatusGetter(GENE_TRACK_ID);
const transcriptTrackStatus = trackStatusGetter(TRANSCRIPT_TRACK_ID);
return (
<div>
<TrackPanelListItem
categoryName="main"
trackStatus={geneTrackStatus}
defaultTrackStatus={Status.SELECTED}
track={prepareGeneTrackData(gene)}
>
{prepareTranscriptsTrackData(gene).map((transcriptTrackData) => (
<TrackPanelListItem
key={transcriptTrackData.label}
categoryName="main"
trackStatus={transcriptTrackStatus}
defaultTrackStatus={Status.SELECTED}
track={transcriptTrackData}
/>
))}
</TrackPanelListItem>
</div>
);
};
// TODO: examine EnsObjectTrack
// as we change our thinking about the EnsObject type, the EnsObjectTrack type,
// and the TrackPanelListItem components, the functions below will need changing