From 9ad569a54bfcd0ffd40a9be3aef3175398cb0e96 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 7 Sep 2023 16:29:03 -0400 Subject: [PATCH 01/79] feat(explorer): deduplicate individual metadata table entries --- src/components/explorer/IndividualMetadata.js | 73 +++++++++++-------- 1 file changed, 42 insertions(+), 31 deletions(-) diff --git a/src/components/explorer/IndividualMetadata.js b/src/components/explorer/IndividualMetadata.js index 2362fc50d..9a30be667 100644 --- a/src/components/explorer/IndividualMetadata.js +++ b/src/components/explorer/IndividualMetadata.js @@ -1,8 +1,7 @@ -import React from "react"; +import React, {useMemo} from "react"; import {Table} from "antd"; - import {individualPropTypesShape} from "../../propTypes"; @@ -12,54 +11,66 @@ import {individualPropTypesShape} from "../../propTypes"; const METADATA_COLUMNS = [ { title: "Resource ID", - key: "r_id", - render: (_, individual) => individual.id, + dataIndex: "id", sorter: (a, b) => a.id.toString().localeCompare(b.id), defaultSortOrder: "ascend", - },{ + }, + { title: "Name", - key: "name", - render: (_, individual) => individual.name, + dataIndex: "name", sorter: (a, b) => a.name.toString().localeCompare(b.name), defaultSortOrder: "ascend", - },{ + }, + { title: "Namespace Prefix", - key: "namespace_prefix", - render: (_, individual) => individual.namespace_prefix, + dataIndex: "namespace_prefix", sorter: (a, b) => a.namespace_prefix.toString().localeCompare(b.namespace_prefix), defaultSortOrder: "ascend", - },{ + }, + { title: "Url", - key: "url", - render: (_, individual) => {individual.url}, + dataIndex: "url", + render: (url) => {url}, defaultSortOrder: "ascend", - },{ + }, + { title: "Version", - key: "version", - render: (_, individual) => individual.version, + dataIndex: "version", sorter: (a, b) => a.version.toString().localeCompare(b.version), defaultSortOrder: "ascend", - },{ + }, + { title: "IRI Prefix", - key: "iri_prefix", - render: (_, individual) => {individual.iri_prefix}, + dataIndex: "iri_prefix", + render: (iriPrefix) => {iriPrefix}, defaultSortOrder: "ascend", }, ]; -const IndividualMetadata = ({individual}) => - (p.meta_data || {}).resources || [])} />; - +const IndividualMetadata = ({individual}) => { + const resources = useMemo( + () => + Object.values( + Object.fromEntries( + (individual || {}).phenopackets + .flatMap(p => (p.meta_data || {}).resources || []) + .map(r => [r.id, r]), + ), + ), + [individual], + ); + return ( +
+ ); +}; IndividualMetadata.propTypes = { individual: individualPropTypesShape, From d47ef536d3e55b39aa170e6b2fc469b1559f25e8 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Thu, 7 Sep 2023 16:42:05 -0400 Subject: [PATCH 02/79] feat(explorer): deduplicate phenotypic features in table --- .../explorer/IndividualPhenotypicFeatures.js | 58 +++++++++++++------ 1 file changed, 40 insertions(+), 18 deletions(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 6d7639bb1..12a80d9b0 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useMemo} from "react"; import {Table} from "antd"; @@ -8,35 +8,57 @@ import {individualPropTypesShape} from "../../propTypes"; const P_FEATURES_COLUMNS = [ { title: "Type", - key: "type", - render: (_, individual) => - {individual?.type?.label ?? EM_DASH} - {individual?.type?.id ?? EM_DASH} + dataIndex: "type", + render: (type) => + {type?.label ?? EM_DASH} + {type?.id ?? EM_DASH} , }, { title: "Negated", - key: "negated", - render: (_, individual) => (individual?.negated ?? "false").toString(), + dataIndex: "negated", + render: (negated) => (negated ?? "false").toString(), }, { title: "Extra Properties", - key: "extra_properties", - render: (_, individual) => - (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) - ?
{JSON.stringify(individual.extra_properties, null, 2)}
+ dataIndex: "extra_properties", + render: (extraProperties) => + (Object.keys(extraProperties ?? {}).length) + ?
{JSON.stringify(extraProperties ?? {}, null, 2)}
: EM_DASH, }, ]; -const IndividualPhenotypicFeatures = ({individual}) => -
(p.phenotypic_features ?? []))} />; +const IndividualPhenotypicFeatures = ({individual}) => { + // TODO: this logic might be technically incorrect with different versions of the same resource + // across multiple phenopackets + const phenotypicFeatures = useMemo( + () => + Object.values( + Object.fromEntries( + (individual?.phenopackets ?? []) + .flatMap(p => (p.phenotypic_features ?? [])) + .map(pf => { + const pfID = `${pf.type.id}:${pf.negated}`; + return [pfID, {...pf, id: pfID}]; + }) + ) + ), + [individual], + ); + + return ( +
+ ); +} IndividualPhenotypicFeatures.propTypes = { individual: individualPropTypesShape, From 3f3faaba59178170103b87c1ef42429f97c1f300 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 09:23:02 -0400 Subject: [PATCH 03/79] style: improve comment for individual phenotypic features --- src/components/explorer/IndividualPhenotypicFeatures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 12a80d9b0..779a2daff 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -31,7 +31,7 @@ const P_FEATURES_COLUMNS = [ ]; const IndividualPhenotypicFeatures = ({individual}) => { - // TODO: this logic might be technically incorrect with different versions of the same resource + // TODO: this logic might be technically incorrect with different versions of the same resource (i.e. ontology) // across multiple phenopackets const phenotypicFeatures = useMemo( () => From b897a7ca036b9a6ef0bb5f602c3b7b9f20c2d7d5 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 09:23:41 -0400 Subject: [PATCH 04/79] feat: deduplicate biosamples in IndividualVariants + refact --- src/components/explorer/IndividualVariants.js | 85 +++++++++---------- 1 file changed, 39 insertions(+), 46 deletions(-) diff --git a/src/components/explorer/IndividualVariants.js b/src/components/explorer/IndividualVariants.js index 7edb3afba..5ed544ceb 100644 --- a/src/components/explorer/IndividualVariants.js +++ b/src/components/explorer/IndividualVariants.js @@ -1,10 +1,12 @@ import React from "react"; import {Link} from "react-router-dom"; -import { useDispatch } from "react-redux"; -import {Button, Descriptions, Empty} from "antd"; +import {useDispatch} from "react-redux"; import PropTypes from "prop-types"; + +import {Button, Descriptions, Empty} from "antd"; + import {individualPropTypesShape} from "../../propTypes"; -import { setIgvPosition } from "../../modules/explorer/actions"; +import {setIgvPosition} from "../../modules/explorer/actions"; import "./explorer.css"; // TODO: Only show variants from the relevant dataset, if specified; @@ -14,66 +16,57 @@ const sampleStyle = {display: "flex", flexDirection: "column", flexWrap: "nowrap const variantStyle = {margin: "5px"}; const IndividualVariants = ({individual, tracksUrl}) => { - const biosamples = (individual || {}).phenopackets.flatMap(p => p.biosamples); - const variantsMapped = {}; const dispatch = useDispatch(); - biosamples.forEach((bs) => { - const allvariants = (bs || {}).variants; + const biosamples = Object.values( + Object.fromEntries( + (individual || {}).phenopackets + .flatMap(p => p.biosamples) + .map(b => [b.id, b]) + ) + ); - const variantsObject = (allvariants || []).map((v) => ({ + const variantsMapped = Object.fromEntries(biosamples.map((biosample) => [ + biosample.id, + (biosample.variants ?? []).map((v) => ({ id: v.hgvsAllele?.id, hgvs: v.hgvsAllele?.hgvs, gene_context: v.extra_properties?.gene_context ?? "", - })); - variantsMapped[bs.id] = variantsObject; - }); + })), + ])); - const ids = (biosamples || []).map(b => - ({ - title: `Biosample ${b.id}`, - key: b.id, - render: (_, map) =>
-
{JSON.stringify(map[b.id], null, 2)}
, - //sorter: (a, b) => a.id.localeCompare(b.id), - //defaultSortOrder: "ascend" - }), + const VariantDetails = ({variant}) => ( +
+ + {`id: ${variant.id} hgvs: ${variant.hgvs}`} + + {variant.gene_context && ( + <> + gene context: + dispatch(setIgvPosition(variant.gene_context))} + to={{ pathname: tracksUrl }}> + + + + )} +
); - const VariantDetails = ({variant}) => { - return
- {`id: ${variant.id} hgvs: ${variant.hgvs}`} - {variant.gene_context && ( - <>gene context: - dispatch(setIgvPosition(variant.gene_context))} - to={{ - pathname: tracksUrl, - }} - > - - - - )} -
; - }; - - const SampleVariants = ({id}) => { - - return variantsMapped[id].length ? ( + const SampleVariants = ({biosampleID}) => + variantsMapped[biosampleID].length ? (
- {variantsMapped[id].map((v) => ( + {variantsMapped[biosampleID].map((v) => ( ))}
) : ; - }; return (
- {ids.length ? - {ids.map((i) => ( - - + {biosamples.length ? + {biosamples.map(({id}) => ( + + ))} : } From 4a088528dc9689e0294aa2b3176b96999663c411 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 09:42:09 -0400 Subject: [PATCH 05/79] refact: move out sub-components in IndividualVariants --- src/components/explorer/IndividualVariants.js | 83 ++++++++++++------- 1 file changed, 53 insertions(+), 30 deletions(-) diff --git a/src/components/explorer/IndividualVariants.js b/src/components/explorer/IndividualVariants.js index 5ed544ceb..bf50a9dd7 100644 --- a/src/components/explorer/IndividualVariants.js +++ b/src/components/explorer/IndividualVariants.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useMemo} from "react"; import {Link} from "react-router-dom"; import {useDispatch} from "react-redux"; import PropTypes from "prop-types"; @@ -15,58 +15,81 @@ import "./explorer.css"; const sampleStyle = {display: "flex", flexDirection: "column", flexWrap: "nowrap"}; const variantStyle = {margin: "5px"}; -const IndividualVariants = ({individual, tracksUrl}) => { - const dispatch = useDispatch(); - - const biosamples = Object.values( - Object.fromEntries( - (individual || {}).phenopackets - .flatMap(p => p.biosamples) - .map(b => [b.id, b]) - ) - ); +const mappedVariantPropType = PropTypes.shape({ + id: PropTypes.string, + hgvs: PropTypes.string, + geneContext: PropTypes.string, +}); - const variantsMapped = Object.fromEntries(biosamples.map((biosample) => [ - biosample.id, - (biosample.variants ?? []).map((v) => ({ - id: v.hgvsAllele?.id, - hgvs: v.hgvsAllele?.hgvs, - gene_context: v.extra_properties?.gene_context ?? "", - })), - ])); +const VariantDetails = ({variant, tracksUrl}) => { + const dispatch = useDispatch(); - const VariantDetails = ({variant}) => ( + return (
{`id: ${variant.id} hgvs: ${variant.hgvs}`} - {variant.gene_context && ( + {variant.geneContext && ( <> gene context: - dispatch(setIgvPosition(variant.gene_context))} + dispatch(setIgvPosition(variant.geneContext))} to={{ pathname: tracksUrl }}> - + )}
); +} +VariantDetails.propTypes = { + variant: mappedVariantPropType, + tracksUrl: PropTypes.string, +}; - const SampleVariants = ({biosampleID}) => - variantsMapped[biosampleID].length ? ( -
+const SampleVariants = ({variantsMapped, biosampleID, tracksUrl}) => + variantsMapped[biosampleID].length ? ( +
{variantsMapped[biosampleID].map((v) => ( - + ))} -
- ) : ; +
+ ) : ; +SampleVariants.propTypes = { + variantsMapped: PropTypes.objectOf(mappedVariantPropType), + biosampleID: PropTypes.string, + tracksUrl: PropTypes.string, +}; + +const IndividualVariants = ({individual, tracksUrl}) => { + const biosamples = useMemo( + () => Object.values( + Object.fromEntries( + (individual || {}).phenopackets + .flatMap(p => p.biosamples) + .map(b => [b.id, b]), + ), + ), + [individual] + ); + + const variantsMapped = useMemo( + () => Object.fromEntries(biosamples.map((biosample) => [ + biosample.id, + (biosample.variants ?? []).map((v) => ({ + id: v.hgvsAllele?.id, + hgvs: v.hgvsAllele?.hgvs, + geneContext: v.extra_properties?.gene_context ?? "", + })), + ])), + [biosamples], + ); return (
{biosamples.length ? {biosamples.map(({id}) => ( - + ))} : } From 15ac8cdbe9d97e5f61cc74e9014aa495e44f12b5 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 09:44:10 -0400 Subject: [PATCH 06/79] lint: IndividualVariants --- src/components/explorer/IndividualVariants.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/components/explorer/IndividualVariants.js b/src/components/explorer/IndividualVariants.js index bf50a9dd7..085c29bf3 100644 --- a/src/components/explorer/IndividualVariants.js +++ b/src/components/explorer/IndividualVariants.js @@ -40,7 +40,7 @@ const VariantDetails = ({variant, tracksUrl}) => { )}
); -} +}; VariantDetails.propTypes = { variant: mappedVariantPropType, tracksUrl: PropTypes.string, @@ -55,7 +55,7 @@ const SampleVariants = ({variantsMapped, biosampleID, tracksUrl}) =>
) : ; SampleVariants.propTypes = { - variantsMapped: PropTypes.objectOf(mappedVariantPropType), + variantsMapped: PropTypes.objectOf(PropTypes.arrayOf(mappedVariantPropType)), biosampleID: PropTypes.string, tracksUrl: PropTypes.string, }; @@ -69,12 +69,12 @@ const IndividualVariants = ({individual, tracksUrl}) => { .map(b => [b.id, b]), ), ), - [individual] + [individual], ); const variantsMapped = useMemo( () => Object.fromEntries(biosamples.map((biosample) => [ - biosample.id, + biosample.id, (biosample.variants ?? []).map((v) => ({ id: v.hgvsAllele?.id, hgvs: v.hgvsAllele?.hgvs, From d73734e9655520bf192584e65c8ad3b88d8d5010 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 09:44:19 -0400 Subject: [PATCH 07/79] lint: IndividualPhenotypicFeatures --- src/components/explorer/IndividualPhenotypicFeatures.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 779a2daff..df02bf1a1 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -42,8 +42,8 @@ const IndividualPhenotypicFeatures = ({individual}) => { .map(pf => { const pfID = `${pf.type.id}:${pf.negated}`; return [pfID, {...pf, id: pfID}]; - }) - ) + }), + ), ), [individual], ); @@ -58,7 +58,7 @@ const IndividualPhenotypicFeatures = ({individual}) => { dataSource={phenotypicFeatures} /> ); -} +}; IndividualPhenotypicFeatures.propTypes = { individual: individualPropTypesShape, From 127030b23b7bed246c88153f94d99e85320f65f9 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 09:55:01 -0400 Subject: [PATCH 08/79] refact: factor out biosample deduplication as hook --- src/components/explorer/IndividualVariants.js | 12 ++---------- src/components/explorer/utils.js | 10 ++++++++++ 2 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 src/components/explorer/utils.js diff --git a/src/components/explorer/IndividualVariants.js b/src/components/explorer/IndividualVariants.js index 085c29bf3..07db55df9 100644 --- a/src/components/explorer/IndividualVariants.js +++ b/src/components/explorer/IndividualVariants.js @@ -7,6 +7,7 @@ import {Button, Descriptions, Empty} from "antd"; import {individualPropTypesShape} from "../../propTypes"; import {setIgvPosition} from "../../modules/explorer/actions"; +import {deduplicatedIndividualBiosamples} from "./utils"; import "./explorer.css"; // TODO: Only show variants from the relevant dataset, if specified; @@ -61,16 +62,7 @@ SampleVariants.propTypes = { }; const IndividualVariants = ({individual, tracksUrl}) => { - const biosamples = useMemo( - () => Object.values( - Object.fromEntries( - (individual || {}).phenopackets - .flatMap(p => p.biosamples) - .map(b => [b.id, b]), - ), - ), - [individual], - ); + const biosamples = useMemo(() => deduplicatedIndividualBiosamples(individual), [individual]); const variantsMapped = useMemo( () => Object.fromEntries(biosamples.map((biosample) => [ diff --git a/src/components/explorer/utils.js b/src/components/explorer/utils.js new file mode 100644 index 000000000..fa797aaa5 --- /dev/null +++ b/src/components/explorer/utils.js @@ -0,0 +1,10 @@ +import {useMemo} from "react"; + +export const useDeduplicatedIndividualBiosamples = (individual) => + useMemo(() => Object.values( + Object.fromEntries( + (individual || {}).phenopackets + .flatMap(p => p.biosamples) + .map(b => [b.id, b]), + ), + ), [individual]); From 6f15c1d5f5b0701adeace4ba5aaf10cb0a47081d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 10:02:54 -0400 Subject: [PATCH 09/79] feat: deduplicate gene display + clean up code --- src/components/explorer/IndividualGenes.js | 83 +++++++++++++--------- 1 file changed, 48 insertions(+), 35 deletions(-) diff --git a/src/components/explorer/IndividualGenes.js b/src/components/explorer/IndividualGenes.js index 7041b0dd0..584a1f152 100644 --- a/src/components/explorer/IndividualGenes.js +++ b/src/components/explorer/IndividualGenes.js @@ -1,49 +1,62 @@ -import React from "react"; -import {Link} from "react-router-dom"; +import React, { useMemo } from "react"; +import { Link } from "react-router-dom"; import { useDispatch } from "react-redux"; -import {Button, Table} from "antd"; -import { setIgvPosition } from "../../modules/explorer/actions"; -import {individualPropTypesShape} from "../../propTypes"; import PropTypes from "prop-types"; +import { Button, Table } from "antd"; + +import { setIgvPosition } from "../../modules/explorer/actions"; +import { individualPropTypesShape } from "../../propTypes"; + // TODO: Only show genes from the relevant dataset, if specified; // highlight those found in search results, if specified -const IndividualGenes = ({individual, tracksUrl}) => { - const genes = (individual || {}).phenopackets.flatMap(p => p.genes); - const genesFlat = genes.flatMap(g => ({symbol: g.symbol})); +const GeneIGVLink = React.memo(({symbol, tracksUrl}) => { const dispatch = useDispatch(); + return ( + dispatch(setIgvPosition(symbol))} to={{ pathname: tracksUrl }}> + + + ); +}); +GeneIGVLink.propTypes = { + symbol: PropTypes.string, + tracksUrl: PropTypes.string, +}; - console.log({genesFlat: genesFlat}); +const IndividualGenes = ({individual, tracksUrl}) => { + const genes = useMemo( + () => Object.values( + Object.fromEntries( + (individual || {}).phenopackets + .flatMap(p => p.genes) + .map(g => [g.symbol, g]), + ), + ), + [individual], + ); - const igvLink = (symbol) => ( - dispatch(setIgvPosition(symbol))} - to={{ - pathname: tracksUrl, - }} - > - - + const columns = useMemo( + () => [ + { + title: "Symbol", + dataIndex: "symbol", + render: (symbol) => , + }, + ], + [tracksUrl], ); - const ids = [{ - //(biosamples || []).map(_b => - title: "Symbol", - // key: "id", - render: (_, gene) => igvLink(gene.symbol), - //sorter: (a, b) => a.id.localeCompare(b.id), - //defaultSortOrder: "ascend" - }, - ]; - //); - - return
gene.symbol} - dataSource={genesFlat} - />; + return ( +
+ ); }; IndividualGenes.propTypes = { individual: individualPropTypesShape, From de2911e0c4ce8593f4c86cdf70270567c193be7c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 10:09:28 -0400 Subject: [PATCH 10/79] feat: rewrite / refact experiments; deduplicate biosamples; show empty --- .../explorer/IndividualExperiments.js | 492 +++++++++--------- 1 file changed, 242 insertions(+), 250 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index b80ece11f..03dc233f9 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -1,305 +1,297 @@ -import React, { useEffect } from "react"; +import React, { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; -import { Button, Collapse, Descriptions, Icon, Popover, Table } from "antd"; +import {Button, Collapse, Descriptions, Empty, Icon, Popover, Table} from "antd"; import JsonView from "./JsonView"; import FileSaver from "file-saver"; import { EM_DASH } from "../../constants"; import { individualPropTypesShape } from "../../propTypes"; import { getFileDownloadUrlsFromDrs } from "../../modules/drs/actions"; import { guessFileType } from "../../utils/guessFileType"; +import PropTypes from "prop-types"; +import {useDeduplicatedIndividualBiosamples} from "./utils"; const { Panel } = Collapse; -const IndividualExperiments = ({ individual }) => { - const blankExperimentOntology = [{ id: EM_DASH, label: EM_DASH }]; +const DownloadButton = ({ resultFile }) => { + const downloadUrls = useSelector((state) => state.drs.downloadUrlsByFilename); + + const url = downloadUrls[resultFile.filename]?.url; + if (!url) { + return <>{EM_DASH}; + } + + const saveAs = () => + FileSaver.saveAs( + downloadUrls[resultFile.filename].url, + resultFile.filename, + ); - const downloadUrls = useSelector( - (state) => state.drs.downloadUrlsByFilename, + return ( +
+ + + +
); +}; +DownloadButton.propTypes = { + resultFile: PropTypes.shape({ + filename: PropTypes.string, + }), +}; + +const EXPERIMENT_RESULTS_COLUMNS = [ + { + title: "File Format", + dataIndex: "file_format", + }, + { + title: "Creation Date", + dataIndex: "creation_date", + }, + { + title: "Description", + dataIndex: "description", + }, + { + title: "Filename", + dataIndex: "filename", + }, + { + title: "Download", + key: "download", + align: "center", + render: (_, result) => , + }, + { + title: "Other Details", + key: "other_details", + align: "center", + render: (_, result) => ( + + + + {result.identifier} + + + {result.description} + + + {result.filename} + + + {result.file_format} + + + {result.data_output_type} + + + {result.usage} + + + {result.creation_date} + + + {result.created_by} + + + + } + trigger="click" + > + + + ), + }, +]; + +const BLANK_EXPERIMENT_ONTOLOGY = [{ id: EM_DASH, label: EM_DASH }]; + +const IndividualExperiments = ({ individual }) => { const dispatch = useDispatch(); const history = useHistory(); - const biosamplesData = (individual?.phenopackets ?? []).flatMap( - (p) => p.biosamples, + const biosamplesData = useDeduplicatedIndividualBiosamples(individual); + const experimentsData = useMemo( + () => biosamplesData.flatMap((b) => b?.experiments ?? []), + [biosamplesData], ); - const experimentsData = biosamplesData.flatMap((b) => b?.experiments ?? []); - let results = experimentsData.flatMap((e) => e?.experiment_results ?? []); - - // enforce file_format property - results = results.map((r) => { - return { - ...r, - file_format: r.file_format ?? guessFileType(r.filename), - }; - }); - const downloadableFiles = results.filter(isDownloadable); useEffect(() => { - // retrieve any download urls on mount + // retrieve any download urls if experiments data changes + + const downloadableFiles = experimentsData + .flatMap((e) => e?.experiment_results ?? []) + .map((r) => ({ // enforce file_format property + ...r, + file_format: r.file_format ?? guessFileType(r.filename), + })) + .filter(isDownloadable); + dispatch(getFileDownloadUrlsFromDrs(downloadableFiles)); - }, []); + }, [experimentsData]); const selected = history.location.hash.slice(1); - const opened = []; - if (selected && selected.length > 1) opened.push(selected); + const opened = (selected && selected.length > 1) ? [selected] : []; - const renderDownloadButton = (resultFile) => { - return downloadUrls[resultFile.filename]?.url ? ( -
- - FileSaver.saveAs( - downloadUrls[resultFile.filename].url, - resultFile.filename, - ) - } - > - - -
- ) : ( - EM_DASH - ); - }; + if (!experimentsData.length) { + return ; + } - const EXPERIMENT_RESULTS_COLUMNS = [ - { - title: "Result File", - key: "result_file", - render: (_, result) => result.file_format, - }, - { - title: "Creation Date", - key: "creation_date", - render: (_, result) => result.creation_date, - }, - { - title: "Description", - key: "description", - render: (_, result) => result.description, - }, - { - title: "Filename", - key: "filename", - render: (_, result) => result.filename, - }, - { - title: "Download", - key: "download", - align: "center", - render: (_, result) => renderDownloadButton(result), - }, - { - title: "Other Details", - key: "other_details", - align: "center", - render: (_, result) => ( - + return ( + + {experimentsData.map((e) => ( + +
+
- - {result.identifier} - - - {result.description} - - - {result.filename} - - - {result.file_format} - - - {result.data_output_type} - - - {result.usage} - - - {result.creation_date} - - - {result.created_by} - - -
- } - trigger="click" - > - - - ), - }, - ]; - - return ( - <> - - {experimentsData.map((e) => ( - -
-
- - - {(e.molecule_ontology ?? []).map( - (mo) => ( - - - {mo.id} - - - {mo.label} - - - ), - )} - - - {( - e.experiment_ontology || - blankExperimentOntology - ).map((eo) => ( - - - {eo.id} - - - {eo.label} - - - ))} - - + + {(e.molecule_ontology ?? []).map((mo) => ( - - {e.instrument.platform} + + {mo.id} - - {e.instrument.identifier} + + {mo.label} - - - - + ))} + + + {(e.experiment_ontology || BLANK_EXPERIMENT_ONTOLOGY).map((eo) => ( - - {e.experiment_type} - - - {e.study_type} + + {eo.id} - - {e.extraction_protocol} - - - {e.library_layout} - - - {e.library_selection} - - - {e.library_source} - - - {e.library_strategy} - - - - - - - + + {eo.label} - - -
+ ))} + + + + + {e.instrument.platform} + + + {e.instrument.identifier} + + + + -
- r1.file_format > r2.file_format ? 1 : -1, - )} - /> + > + + + + {e.experiment_type} + + + {e.study_type} + + + {e.extraction_protocol} + + + {e.library_layout} + + + {e.library_selection} + + + {e.library_source} + + + {e.library_strategy} + + + + + + + + + + + - - ))} - - + +
+ r1.file_format > r2.file_format ? 1 : -1, + )} + /> + + + ))} + ); }; From 1f71519bb5a1ae1f5495324f653b8c9eaeebf553 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 12:05:06 -0400 Subject: [PATCH 11/79] refact(explorer): use deduplicated biosamples hook in biosamples view --- src/components/explorer/IndividualBiosamples.js | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/components/explorer/IndividualBiosamples.js b/src/components/explorer/IndividualBiosamples.js index 834844019..3d58133ab 100644 --- a/src/components/explorer/IndividualBiosamples.js +++ b/src/components/explorer/IndividualBiosamples.js @@ -1,19 +1,20 @@ import React, {Fragment, useCallback, useEffect, useMemo} from "react"; import PropTypes from "prop-types"; -import { Route, Switch, useHistory } from "react-router-dom"; +import { Route, Switch, useHistory, useRouteMatch, useParams } from "react-router-dom"; import { Button, Descriptions, Table } from "antd"; import { EM_DASH } from "../../constants"; import { renderOntologyTerm } from "./ontologies"; +import { useDeduplicatedIndividualBiosamples } from "./utils"; import { biosamplePropTypesShape, experimentPropTypesShape, individualPropTypesShape, ontologyShape, } from "../../propTypes"; + import JsonView from "./JsonView"; -import { useRouteMatch, useParams } from "react-router-dom/cjs/react-router-dom"; import "./explorer.css"; @@ -123,15 +124,7 @@ const Biosamples = ({ individual, handleBiosampleClick, handleExperimentClick }) }, 100); }, []); - const biosamples = useMemo( - () => Object.values( - Object.fromEntries( - (individual?.phenopackets ?? []) - .flatMap((p) => p.biosamples) - .map(b => [b.id, b]), - ), - ), - [individual]); + const biosamples = useDeduplicatedIndividualBiosamples(individual); const columns = useMemo( () => [ From 5ea224d80ba9425d1e9d4b51115bdaceec6b116b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 12:07:01 -0400 Subject: [PATCH 12/79] fix(explorer): variants page crash --- src/components/explorer/IndividualVariants.js | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/components/explorer/IndividualVariants.js b/src/components/explorer/IndividualVariants.js index 07db55df9..5e7b26764 100644 --- a/src/components/explorer/IndividualVariants.js +++ b/src/components/explorer/IndividualVariants.js @@ -1,13 +1,13 @@ -import React, {useMemo} from "react"; -import {Link} from "react-router-dom"; -import {useDispatch} from "react-redux"; +import React, { useMemo } from "react"; +import { Link } from "react-router-dom"; +import { useDispatch } from "react-redux"; import PropTypes from "prop-types"; -import {Button, Descriptions, Empty} from "antd"; +import { Button, Descriptions, Empty } from "antd"; -import {individualPropTypesShape} from "../../propTypes"; -import {setIgvPosition} from "../../modules/explorer/actions"; -import {deduplicatedIndividualBiosamples} from "./utils"; +import { individualPropTypesShape } from "../../propTypes"; +import { setIgvPosition } from "../../modules/explorer/actions"; +import { useDeduplicatedIndividualBiosamples } from "./utils"; import "./explorer.css"; // TODO: Only show variants from the relevant dataset, if specified; @@ -62,7 +62,7 @@ SampleVariants.propTypes = { }; const IndividualVariants = ({individual, tracksUrl}) => { - const biosamples = useMemo(() => deduplicatedIndividualBiosamples(individual), [individual]); + const biosamples = useDeduplicatedIndividualBiosamples(individual); const variantsMapped = useMemo( () => Object.fromEntries(biosamples.map((biosample) => [ From df2d610198502e150e3fa5a99fa22aa8c12ff7d4 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 12:12:05 -0400 Subject: [PATCH 13/79] chore: improve some logging --- src/modules/drs/actions.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/modules/drs/actions.js b/src/modules/drs/actions.js index 91b27eab4..3e1f7766c 100644 --- a/src/modules/drs/actions.js +++ b/src/modules/drs/actions.js @@ -146,12 +146,12 @@ export const getFileDownloadUrlsFromDrs = (fileObjects) => async (dispatch, getS // reduce array to object that's addressable by filename const urlsObj = urls.reduce((obj, item) => Object.assign(obj, item), {}); - console.log(`received download urls from drs: ${urlsObj}`); + console.debug(`received download urls from drs:`, urlsObj); dispatch(setDownloadUrls(urlsObj)); }) .catch((err) => { - console.log(err); + console.error(err); dispatch(errorDownloadUrls()); }); }; From b00a73f7e18c31038fcc7c5b86337af672250fd4 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 12:13:49 -0400 Subject: [PATCH 14/79] lint --- src/modules/drs/actions.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/modules/drs/actions.js b/src/modules/drs/actions.js index 3e1f7766c..7a5cffc64 100644 --- a/src/modules/drs/actions.js +++ b/src/modules/drs/actions.js @@ -146,7 +146,7 @@ export const getFileDownloadUrlsFromDrs = (fileObjects) => async (dispatch, getS // reduce array to object that's addressable by filename const urlsObj = urls.reduce((obj, item) => Object.assign(obj, item), {}); - console.debug(`received download urls from drs:`, urlsObj); + console.debug("received download urls from drs:", urlsObj); dispatch(setDownloadUrls(urlsObj)); }) From c557a3788d31d516aa9296618b24e693a776b6ba Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 12:16:58 -0400 Subject: [PATCH 15/79] feat(explorer): deduplicate biosamples in individualtracks --- src/components/explorer/IndividualTracks.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualTracks.js b/src/components/explorer/IndividualTracks.js index 757bec048..7f169447c 100644 --- a/src/components/explorer/IndividualTracks.js +++ b/src/components/explorer/IndividualTracks.js @@ -10,6 +10,7 @@ import { individualPropTypesShape } from "../../propTypes"; import { getIgvUrlsFromDrs } from "../../modules/drs/actions"; import { setIgvPosition } from "../../modules/explorer/actions"; import { guessFileType } from "../../utils/guessFileType"; +import {useDeduplicatedIndividualBiosamples} from "./utils"; const SQUISHED_CALL_HEIGHT = 10; const EXPANDED_CALL_HEIGHT = 50; @@ -96,7 +97,7 @@ const IndividualTracks = ({ individual }) => { ); const dispatch = useDispatch(); - const biosamplesData = (individual?.phenopackets ?? []).flatMap((p) => p.biosamples); + const biosamplesData = useDeduplicatedIndividualBiosamples(individual); const experimentsData = biosamplesData.flatMap((b) => b?.experiments ?? []); const viewableResults = useMemo( From 5cf18b9dc19954a74d3bea1a5af805e1a652a1d8 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 12:18:20 -0400 Subject: [PATCH 16/79] style(explorer): add gap to experiment summary --- src/components/explorer/explorer.css | 1 + 1 file changed, 1 insertion(+) diff --git a/src/components/explorer/explorer.css b/src/components/explorer/explorer.css index 314839bb7..f3eb14a4b 100644 --- a/src/components/explorer/explorer.css +++ b/src/components/explorer/explorer.css @@ -69,6 +69,7 @@ .experiment_summary { display: flex; margin-bottom: 25px; + gap: 12px; } /* box shadow instead of borders for automatic border collapse on adjacent items */ From 64fa3c7e8533037d626943395a1601997092d9ca Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 12:52:50 -0400 Subject: [PATCH 17/79] debug logging --- src/components/explorer/IndividualTracks.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/components/explorer/IndividualTracks.js b/src/components/explorer/IndividualTracks.js index 7f169447c..225fbfce8 100644 --- a/src/components/explorer/IndividualTracks.js +++ b/src/components/explorer/IndividualTracks.js @@ -116,6 +116,8 @@ const IndividualTracks = ({ individual }) => { [experimentsData], ); + console.debug("Viewable experiment results:", viewableResults); + const [allTracks, setAllTracks] = useState( viewableResults.sort((r1, r2) => (r1.file_format > r2.file_format ? 1 : -1)), ); From e78d342dcae3e10fe188e582465713bac0ae3466 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 13:43:12 -0400 Subject: [PATCH 18/79] chore: update node to v20 (will be LTS in Oct 2023) --- Dockerfile | 5 +++-- dev.Dockerfile | 4 ++-- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/Dockerfile b/Dockerfile index c9eff72c0..b1ef2b055 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM node:18-bullseye-slim AS build +FROM --platform=$BUILDPLATFORM node:20-bookworm-slim AS build # Build bento_web with NodeJS + Webpack # - Use BUILDPLATFORM for running webpack, since it should perform a lot better. @@ -25,7 +25,8 @@ RUN npm run build FROM nginx:1.23 -RUN curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \ +# Install node so that we can run the create_config_prod.js & create_service_info.js scripts +RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get update -y && \ apt-get install nodejs diff --git a/dev.Dockerfile b/dev.Dockerfile index 38cee9c02..0580da3d4 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -1,4 +1,4 @@ -FROM --platform=$BUILDPLATFORM node:18-bullseye-slim AS install +FROM --platform=$BUILDPLATFORM node:20-bookworm-slim AS install WORKDIR /web @@ -7,7 +7,7 @@ COPY package-lock.json . RUN npm ci -FROM ghcr.io/bento-platform/bento_base_image:node-debian-2023.03.22 +FROM ghcr.io/bento-platform/bento_base_image:node-debian-2023.09.08 LABEL org.opencontainers.image.description="Local development image for Bento Web." From 70473869c0017533648be251093f48d646b4debf Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 13:49:37 -0400 Subject: [PATCH 19/79] chore: rm apt lists from production image --- Dockerfile | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index b1ef2b055..f437c8bab 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,8 @@ FROM nginx:1.23 # Install node so that we can run the create_config_prod.js & create_service_info.js scripts RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get update -y && \ - apt-get install nodejs + apt-get install nodejs && \ + rm -rf /var/lib/apt/lists/* # Serve bento_web with NGINX; copy in configuration COPY nginx.conf /etc/nginx/nginx.conf From 8dd6d33f1f439f1d07b337e1fa1234795b532c99 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 13:49:58 -0400 Subject: [PATCH 20/79] chore: rearrange dockerfile file order copying for layer caching --- Dockerfile | 23 ++++++++++++++--------- dev.Dockerfile | 2 +- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/Dockerfile b/Dockerfile index f437c8bab..1e233c78f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -35,17 +35,22 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ COPY nginx.conf /etc/nginx/nginx.conf WORKDIR /web -# Copy webpack-built source code from the build stage to the final image -COPY --from=build /web/dist ./dist -# Copy in package.json to provide version -COPY package.json . -# Copy in the production config generation script + +# In general, we want to copy files in order of least -> most changed for layer caching reasons. + +# - Copy in LICENSE so that people can see it if they explore the image contents +COPY LICENSE . +# - Copy in the production config generation script COPY create_config_prod.js . -# Copy in the service info generator +# - Copy in the service info generator COPY create_service_info.js . -# Copy in the entrypoint, which writes the config file and starts NGINX +# - Copy in the entrypoint, which writes the config file and starts NGINX COPY run.bash . -# Copy in LICENSE so that people can see it if they explore the image contents -COPY LICENSE . +# - Copy in package.json to provide version to scripts +COPY package.json . +# - Copy webpack-built source code from the build stage to the final image +# - copy this last, since it changes more often than everything above it +# - this way we can cache layers +COPY --from=build /web/dist ./dist CMD ["bash", "./run.bash"] diff --git a/dev.Dockerfile b/dev.Dockerfile index 0580da3d4..abfb2c71a 100644 --- a/dev.Dockerfile +++ b/dev.Dockerfile @@ -13,9 +13,9 @@ LABEL org.opencontainers.image.description="Local development image for Bento We WORKDIR /web +COPY run.dev.bash . COPY package.json . COPY package-lock.json . -COPY run.dev.bash . COPY --from=install /web/node_modules ./node_modules From d270e10121c3173320499761927e468b3ffb9e59 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 13:51:48 -0400 Subject: [PATCH 21/79] chore: update nginx to 1.25 --- Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 1e233c78f..98fcb7d2f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -23,7 +23,7 @@ COPY static static RUN npm run build -FROM nginx:1.23 +FROM nginx:1.25 # Install node so that we can run the create_config_prod.js & create_service_info.js scripts RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ From b74e37089577a9bd071826449df2e6efdda6afce Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 15:15:14 -0400 Subject: [PATCH 22/79] chore: update node install process from nodesource --- Dockerfile | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Dockerfile b/Dockerfile index 98fcb7d2f..2ee28fe1b 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,13 @@ FROM nginx:1.25 # Install node so that we can run the create_config_prod.js & create_service_info.js scripts RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ apt-get update -y && \ - apt-get install nodejs && \ + apt-get install -y ca-certificates curl gnupg && \ + mkdir -p /etc/apt/keyrings && \ + curl -fsSL https://deb.nodesource.com/gpgkey/nodesource-repo.gpg.key | \ + gpg --dearmor -o /etc/apt/keyrings/nodesource.gpg && \ + echo "deb [signed-by=/etc/apt/keyrings/nodesource.gpg] https://deb.nodesource.com/node_20.x nodistro main" | \ + tee /etc/apt/sources.list.d/nodesource.list && \ + apt-get install -y nodejs && \ rm -rf /var/lib/apt/lists/* # Serve bento_web with NGINX; copy in configuration From 32e591b158ca6ffac503fb5868f3f90ff4e0d3a6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 15:48:38 -0400 Subject: [PATCH 23/79] feat: deduplicate tracks based on filename (for now) --- src/components/explorer/IndividualTracks.js | 33 ++++++++++++++------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/src/components/explorer/IndividualTracks.js b/src/components/explorer/IndividualTracks.js index 225fbfce8..d6c419c00 100644 --- a/src/components/explorer/IndividualTracks.js +++ b/src/components/explorer/IndividualTracks.js @@ -83,6 +83,10 @@ TrackControlTable.propTypes = { allFoundFiles: PropTypes.arrayOf(PropTypes.object), }; +// Right now, a lot of this code uses filenames. This should not be the case going forward, +// as multiple files may have the same name. Everything *should* be done through DRS IDs. +// For now, we treat the filenames as unique identifiers (unfortunately). + const IndividualTracks = ({ individual }) => { const { accessToken } = useSelector((state) => state.auth); @@ -102,17 +106,24 @@ const IndividualTracks = ({ individual }) => { const viewableResults = useMemo( () => - experimentsData.flatMap((e) => e?.experiment_results ?? []) - .filter(isViewable) - .map((v) => { // add properties for visibility and file type - const fileFormat = v.file_format?.toLowerCase() ?? guessFileType(v.filename); - return { - ...v, - // by default, don't view crams (user can turn them on in track controls): - viewInIgv: fileFormat !== "cram", - file_format: fileFormat, // re-formatted: to lowercase + guess if missing - }; - }), + Object.values( + Object.fromEntries( + experimentsData.flatMap((e) => e?.experiment_results ?? []) + .filter(isViewable) + .map((v) => { // add properties for visibility and file type + const fileFormat = v.file_format?.toLowerCase() ?? guessFileType(v.filename); + return [ + v.filename, + { + ...v, + // by default, don't view crams (user can turn them on in track controls): + viewInIgv: fileFormat !== "cram", + file_format: fileFormat, // re-formatted: to lowercase + guess if missing + }, + ]; + }) + ) + ), [experimentsData], ); From dc7348e662b229aadbc20f944dd72fb623a874e5 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 15:50:08 -0400 Subject: [PATCH 24/79] lint --- src/components/explorer/IndividualTracks.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/explorer/IndividualTracks.js b/src/components/explorer/IndividualTracks.js index d6c419c00..af5ee81eb 100644 --- a/src/components/explorer/IndividualTracks.js +++ b/src/components/explorer/IndividualTracks.js @@ -121,8 +121,8 @@ const IndividualTracks = ({ individual }) => { file_format: fileFormat, // re-formatted: to lowercase + guess if missing }, ]; - }) - ) + }), + ), ), [experimentsData], ); From b1c02b0d328b151b6ac589ff753742a9aa88ef43 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 16:17:52 -0400 Subject: [PATCH 25/79] feat: show experiment ID in collapse header --- src/components/explorer/IndividualExperiments.js | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 03dc233f9..f53aff19e 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -155,7 +155,14 @@ const IndividualExperiments = ({ individual }) => { {experimentsData.map((e) => ( + + ID: {e.id} + {" "} + {e.experiment_type} (Biosample {e.biosample}) + + )} >
From 4ae6ba428ac766f178cba03974a7bcc2ba0fcee1 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 16:20:46 -0400 Subject: [PATCH 26/79] style: consistent monospace colouring for exp id --- src/components/explorer/IndividualExperiments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index f53aff19e..b653c979b 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -157,7 +157,7 @@ const IndividualExperiments = ({ individual }) => { key={e.id} header={( - + ID: {e.id} {" "} {e.experiment_type} (Biosample {e.biosample}) From 495bcb07aab9b6859f66cb76f7fb531cb421238c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 8 Sep 2023 16:25:33 -0400 Subject: [PATCH 27/79] chore: move experiment ID to descriptions item --- src/components/explorer/IndividualExperiments.js | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index b653c979b..d3878b73e 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -155,14 +155,7 @@ const IndividualExperiments = ({ individual }) => { {experimentsData.map((e) => ( - - ID: {e.id} - {" "} - {e.experiment_type} (Biosample {e.biosample}) - - )} + header={`${e.experiment_type} (Biosample ${e.biosample})`} >
@@ -242,6 +235,9 @@ const IndividualExperiments = ({ individual }) => { column={1} size="small" > + + {e.id} + {e.experiment_type} From a8be83f4f67ad4516734b46b62c45e897007945f Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 13:40:57 -0400 Subject: [PATCH 28/79] feat(explorer): component for rendering linked ontology terms refact: dedup and expand on some prop types --- .../explorer/IndividualBiosamples.js | 32 ++--- src/components/explorer/IndividualDiseases.js | 118 +++++++++--------- .../explorer/IndividualExperiments.js | 41 +----- src/components/explorer/IndividualMetadata.js | 15 +-- src/components/explorer/IndividualOverview.js | 17 +-- .../explorer/IndividualPhenotypicFeatures.js | 62 ++++----- src/components/explorer/OntologyTerm.js | 43 +++++++ src/components/explorer/ontologies.js | 4 - .../searchResultsTables/BiosamplesTable.js | 29 ++--- src/components/explorer/utils.js | 45 ++++++- src/modules/explorer/reducers.js | 3 +- src/propTypes.js | 20 ++- 12 files changed, 237 insertions(+), 192 deletions(-) create mode 100644 src/components/explorer/OntologyTerm.js delete mode 100644 src/components/explorer/ontologies.js diff --git a/src/components/explorer/IndividualBiosamples.js b/src/components/explorer/IndividualBiosamples.js index 3d58133ab..f49a0bab6 100644 --- a/src/components/explorer/IndividualBiosamples.js +++ b/src/components/explorer/IndividualBiosamples.js @@ -5,7 +5,6 @@ import { Route, Switch, useHistory, useRouteMatch, useParams } from "react-route import { Button, Descriptions, Table } from "antd"; import { EM_DASH } from "../../constants"; -import { renderOntologyTerm } from "./ontologies"; import { useDeduplicatedIndividualBiosamples } from "./utils"; import { biosamplePropTypesShape, @@ -15,29 +14,29 @@ import { } from "../../propTypes"; import JsonView from "./JsonView"; +import OntologyTerm from "./OntologyTerm"; import "./explorer.css"; // TODO: Only show biosamples from the relevant dataset, if specified; // highlight those found in search results, if specified -const BiosampleProcedure = ({procedure}) => ( +const BiosampleProcedure = ({ individual, procedure }) => (
- Code:{" "} - {renderOntologyTerm(procedure.code)} + Code:{" "} {procedure.body_site ? (
- Body Site:{" "} - {renderOntologyTerm(procedure.body_site)} + Body Site:{" "}
) : null}
); BiosampleProcedure.propTypes = { + individual: individualPropTypesShape.isRequired, procedure: PropTypes.shape({ code: ontologyShape.isRequired, body_site: ontologyShape, - }), + }).isRequired, }; const ExperimentsClickList = ({ experiments, handleExperimentClick }) => { @@ -60,20 +59,20 @@ ExperimentsClickList.propTypes = { handleExperimentClick: PropTypes.func, }; -const BiosampleDetail = ({ biosample, handleExperimentClick }) => { +const BiosampleDetail = ({ individual, biosample, handleExperimentClick }) => { return ( {biosample.id} - {renderOntologyTerm(biosample.sampled_tissue)} + - + - {renderOntologyTerm(biosample.histological_diagnosis)} + {biosample.individual_age_at_collection @@ -100,6 +99,7 @@ const BiosampleDetail = ({ biosample, handleExperimentClick }) => { ); }; BiosampleDetail.propTypes = { + individual: individualPropTypesShape, biosample: biosamplePropTypesShape, handleExperimentClick: PropTypes.func, }; @@ -136,7 +136,7 @@ const Biosamples = ({ individual, handleBiosampleClick, handleExperimentClick }) { title: "Sampled Tissue", dataIndex: "sampled_tissue", - render: tissue => renderOntologyTerm(tissue), + render: tissue => , }, { title: "Experiments", @@ -146,7 +146,7 @@ const Biosamples = ({ individual, handleBiosampleClick, handleExperimentClick }) ), }, ], - [handleExperimentClick], + [individual, handleExperimentClick], ); const onExpand = useCallback( @@ -158,7 +158,11 @@ const Biosamples = ({ individual, handleBiosampleClick, handleExperimentClick }) const expandedRowRender = useCallback( (biosample) => ( - + ), [handleExperimentClick], ); diff --git a/src/components/explorer/IndividualDiseases.js b/src/components/explorer/IndividualDiseases.js index 26d8226b7..379b31409 100644 --- a/src/components/explorer/IndividualDiseases.js +++ b/src/components/explorer/IndividualDiseases.js @@ -1,72 +1,76 @@ -import React from "react"; - -import {Table} from "antd"; +import React, { useMemo } from "react"; +import { Table } from "antd"; import { individualPropTypesShape } from "../../propTypes"; import { EM_DASH } from "../../constants"; +import { ontologyTermSorter } from "./utils"; + +import OntologyTerm from "./OntologyTerm"; // TODO: Only show diseases from the relevant dataset, if specified; // highlight those found in search results, if specified -const DISEASE_COLUMNS = [ - { - title: "Disease ID", - key: "d_id", - render: (_, individual) => individual.id, - sorter: (a, b) => a.id.toString().localeCompare(b.id), - defaultSortOrder: "ascend", - }, - { - title: "Term ID", - key: "t_id", - render: (_, individual) => individual.term.id, - }, - { - title: "Label", - key: "t_label", - render: (_, individual) => individual.term.label, - }, - { - title: "Onset Age(s)", - key: "t_onset_ages", - render: (_, individual) => - // Print onset age - (individual.hasOwnProperty("onset") && Object.keys(individual.onset).length) - // Single onset age - ? (individual.onset.hasOwnProperty("age") && Object.keys(individual.onset.age).length) - ?
{individual.onset.age}
- // Onset age start and end - : (individual.onset.hasOwnProperty("start") && Object.keys(individual.onset.start).length) - ?
{individual.onset.start.age} - {individual.onset.end.age}
- // Onset age label only - : (individual.onset.hasOwnProperty("label") && Object.keys(individual.onset.label).length) - ?
{individual.onset.label} ({individual.onset.id})
- : EM_DASH - : EM_DASH, - }, - { - title: "Extra Properties", - key: "extra_properties", - render: (_, individual) => - (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) - ?
{JSON.stringify(individual.extra_properties, null, 2)}
- : EM_DASH, - }, -]; - -const IndividualDiseases = ({individual}) => -
p.diseases)} />; +const IndividualDiseases = ({ individual }) => { + const diseases = individual.phenopackets.flatMap(p => p.diseases); + const columns = useMemo(() => [ + { + title: "Disease ID", + key: "id", + sorter: (a, b) => a.id.toString().localeCompare(b.id), + defaultSortOrder: "ascend", + }, + { + title: "term", + dataIndex: "term", + render: (term) => , + sorter: ontologyTermSorter("term"), + }, + { + title: "Onset Age(s)", + key: "t_onset_ages", + render: (_, disease) => + // Print onset age + (disease.hasOwnProperty("onset") && Object.keys(disease.onset).length) + // Single onset age + ? (disease.onset.hasOwnProperty("age") && Object.keys(disease.onset.age).length) + ?
{disease.onset.age}
+ // Onset age start and end + : (disease.onset.hasOwnProperty("start") && Object.keys(disease.onset.start).length) + ?
{disease.onset.start.age} - {disease.onset.end.age}
+ // Onset age label only + : (disease.onset.hasOwnProperty("label") && Object.keys(disease.onset.label).length) + ?
{disease.onset.label} ({disease.onset.id})
+ : EM_DASH + : EM_DASH, + }, + { + title: "Extra Properties", + key: "extra_properties", + render: (_, individual) => + (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) + ?
+
{JSON.stringify(individual.extra_properties, null, 2)}
+
+ : EM_DASH, + }, + ], [individual]); + return ( +
+ ); +}; IndividualDiseases.propTypes = { - individual: individualPropTypesShape, + individual: individualPropTypesShape.isRequired, }; export default IndividualDiseases; diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index d3878b73e..3409f9be4 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -1,7 +1,7 @@ import React, { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; -import {Button, Collapse, Descriptions, Empty, Icon, Popover, Table} from "antd"; +import { Button, Collapse, Descriptions, Empty, Icon, Popover, Table } from "antd"; import JsonView from "./JsonView"; import FileSaver from "file-saver"; import { EM_DASH } from "../../constants"; @@ -10,6 +10,7 @@ import { getFileDownloadUrlsFromDrs } from "../../modules/drs/actions"; import { guessFileType } from "../../utils/guessFileType"; import PropTypes from "prop-types"; import {useDeduplicatedIndividualBiosamples} from "./utils"; +import OntologyTerm from "./OntologyTerm"; const { Panel } = Collapse; @@ -116,8 +117,6 @@ const EXPERIMENT_RESULTS_COLUMNS = [ }, ]; -const BLANK_EXPERIMENT_ONTOLOGY = [{ id: EM_DASH, label: EM_DASH }]; - const IndividualExperiments = ({ individual }) => { const dispatch = useDispatch(); const history = useHistory(); @@ -169,41 +168,11 @@ const IndividualExperiments = ({ individual }) => { > {(e.molecule_ontology ?? []).map((mo) => ( - - - {mo.id} - - - {mo.label} - - + ))} - - {(e.experiment_ontology || BLANK_EXPERIMENT_ONTOLOGY).map((eo) => ( - - - {eo.id} - - - {eo.label} - - - ))} + + { - const resources = useMemo( - () => - Object.values( - Object.fromEntries( - (individual || {}).phenopackets - .flatMap(p => (p.meta_data || {}).resources || []) - .map(r => [r.id, r]), - ), - ), - [individual], - ); + const resources = useResources(individual); return (
individual ? @@ -14,11 +14,12 @@ const IndividualOverview = ({individual}) => individual ? {getAge(individual)} {individual.ethnicity || "UNKNOWN_ETHNICITY"} {individual.karyotypic_sex || "UNKNOWN_KARYOTYPE"} - {/* TODO: Link to ontology term */} - {renderOntologyTerm(individual.taxonomy - ? {...individual.taxonomy, label: {individual.taxonomy.label}} - : null)} + ({label})} + /> { (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index df02bf1a1..b4fdb6f2b 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -1,36 +1,12 @@ -import React, {useMemo} from "react"; +import React, { useMemo } from "react"; -import {Table} from "antd"; +import { Table } from "antd"; -import {EM_DASH} from "../../constants"; -import {individualPropTypesShape} from "../../propTypes"; +import { EM_DASH } from "../../constants"; +import { individualPropTypesShape } from "../../propTypes"; +import OntologyTerm from "./OntologyTerm"; -const P_FEATURES_COLUMNS = [ - { - title: "Type", - dataIndex: "type", - render: (type) => - {type?.label ?? EM_DASH} - {type?.id ?? EM_DASH} - , - }, - { - title: "Negated", - dataIndex: "negated", - render: (negated) => (negated ?? "false").toString(), - }, - { - title: "Extra Properties", - dataIndex: "extra_properties", - render: (extraProperties) => - (Object.keys(extraProperties ?? {}).length) - ?
{JSON.stringify(extraProperties ?? {}, null, 2)}
- : EM_DASH, - }, - -]; - -const IndividualPhenotypicFeatures = ({individual}) => { +const IndividualPhenotypicFeatures = ({ individual }) => { // TODO: this logic might be technically incorrect with different versions of the same resource (i.e. ontology) // across multiple phenopackets const phenotypicFeatures = useMemo( @@ -48,12 +24,36 @@ const IndividualPhenotypicFeatures = ({individual}) => { [individual], ); + const columns = useMemo(() => [ + { + title: "Type", + dataIndex: "type", + render: (type) => ( + ({label})} /> + ), + }, + { + title: "Negated", + dataIndex: "negated", + render: (negated) => (negated ?? "false").toString(), + }, + { + title: "Extra Properties", + dataIndex: "extra_properties", + render: (extraProperties) => + (Object.keys(extraProperties ?? {}).length) + ?
{JSON.stringify(extraProperties ?? {}, null, 2)}
+ : EM_DASH, + }, + + ], [individual]) + return (
diff --git a/src/components/explorer/OntologyTerm.js b/src/components/explorer/OntologyTerm.js new file mode 100644 index 000000000..b1d39e22d --- /dev/null +++ b/src/components/explorer/OntologyTerm.js @@ -0,0 +1,43 @@ +import React, { memo } from "react"; +import PropTypes from "prop-types"; + +import { EM_DASH } from "../../constants"; +import { individualPropTypesShape, ontologyShape } from "../../propTypes"; +import { id } from "../../utils/misc"; + +import { useResourcesByNamespacePrefix } from "./utils"; + +const OntologyTerm = memo(({ individual, term, renderLabel }) => { + // TODO: perf: might be slow to generate this over and over + const resourcesByNamespacePrefix = useResourcesByNamespacePrefix(individual); + + if (!term) return <>{EM_DASH}; + + const [namespacePrefix, namespaceID] = term.id.split(":"); + + const termResource = resourcesByNamespacePrefix[namespacePrefix]; + + // If resource doesn't exist / isn't linkable, render the term as an un-clickable plain + if (!termResource || !termResource.iri_prefix || termResource.iri_prefix.includes("example.org")) { + return ( + {renderLabel(term.label)} (ID: {term.id}) + ); + } + + return ( + + {renderLabel(term.label)} (ID: {term.id}) + + ); +}); + +OntologyTerm.propTypes = { + individual: individualPropTypesShape, + term: ontologyShape.isRequired, + renderLabel: PropTypes.func, +}; +OntologyTerm.defaultProps = { + renderLabel: id, +}; + +export default OntologyTerm; diff --git a/src/components/explorer/ontologies.js b/src/components/explorer/ontologies.js deleted file mode 100644 index 3d06e94c6..000000000 --- a/src/components/explorer/ontologies.js +++ /dev/null @@ -1,4 +0,0 @@ -import React from "react"; -import {EM_DASH} from "../../constants"; - -export const renderOntologyTerm = term => term ? {term.label} ({term.id}) : EM_DASH; diff --git a/src/components/explorer/searchResultsTables/BiosamplesTable.js b/src/components/explorer/searchResultsTables/BiosamplesTable.js index 132246c4a..04ce5c2f8 100644 --- a/src/components/explorer/searchResultsTables/BiosamplesTable.js +++ b/src/components/explorer/searchResultsTables/BiosamplesTable.js @@ -8,6 +8,9 @@ import ExplorerSearchResultsTable from "../ExplorerSearchResultsTable"; import BiosampleIDCell from "./BiosampleIDCell"; import IndividualIDCell from "./IndividualIDCell"; +import {ontologyShape} from "../../../propTypes"; +import OntologyTerm from "../OntologyTerm"; +import {ontologyTermSorter} from "../utils"; const NO_EXPERIMENTS_VALUE = -Infinity; @@ -57,15 +60,6 @@ const experimentsSorter = (a, b) => { return countNonNullElements(a.studyTypes) - countNonNullElements(b.studyTypes); }; -const sampledTissuesRender = (sampledTissues) => sampledTissues.map((m) => m.label)[0]; - -const sampledTissuesSorter = (a, b) => { - if (a.sampledTissues[0].label && b.sampledTissues[0].label) { - return a.sampledTissues[0].label.toString().localeCompare(b.sampledTissues[0].label.toString()); - } - return 0; -}; - const availableExperimentsRender = (experimentsType) => { if (experimentsType.every((s) => s !== null)) { const experimentCount = experimentsType.reduce((acc, experiment) => { @@ -107,7 +101,7 @@ const SEARCH_RESULT_COLUMNS_BIOSAMPLE = [ { title: "Biosample", dataIndex: "biosample", - render: (biosample, {individual}) => , + render: (biosample, { individual }) => , sorter: (a, b) => a.biosample.localeCompare(b.biosample), defaultSortOrder: "ascend", }, @@ -126,10 +120,11 @@ const SEARCH_RESULT_COLUMNS_BIOSAMPLE = [ sortDirections: ["descend", "ascend", "descend"], }, { - title: "Sampled Tissues", - dataIndex: "sampledTissues", - render: sampledTissuesRender, - sorter: sampledTissuesSorter, + title: "Sampled Tissue", + dataIndex: "sampledTissue", + // Can't pass individual here to OntologyTerm since it doesn't have a list of phenopackets + render: (sampledTissue) => , + sorter: ontologyTermSorter("sampledTissue"), sortDirections: ["descend", "ascend", "descend"], }, { @@ -173,11 +168,7 @@ BiosamplesTable.propTypes = { id: PropTypes.string.isRequired, }).isRequired, studyTypes: PropTypes.arrayOf(PropTypes.string).isRequired, - sampledTissues: PropTypes.arrayOf( - PropTypes.shape({ - label: PropTypes.string.isRequired, - }), - ).isRequired, + sampledTissue: ontologyShape, experimentTypes: PropTypes.arrayOf(PropTypes.string).isRequired, }), ).isRequired, diff --git a/src/components/explorer/utils.js b/src/components/explorer/utils.js index fa797aaa5..cdd11e11c 100644 --- a/src/components/explorer/utils.js +++ b/src/components/explorer/utils.js @@ -1,10 +1,43 @@ import {useMemo} from "react"; export const useDeduplicatedIndividualBiosamples = (individual) => - useMemo(() => Object.values( - Object.fromEntries( - (individual || {}).phenopackets - .flatMap(p => p.biosamples) - .map(b => [b.id, b]), + useMemo( + () => Object.values( + Object.fromEntries( + (individual || {}).phenopackets + .flatMap(p => p.biosamples) + .map(b => [b.id, b]), + ), ), - ), [individual]); + [individual] + ); + + +export const useResources = (individual) => + useMemo( + () => + Object.values( + Object.fromEntries( + (individual?.phenopackets ?? []) + .flatMap(p => p.meta_data.resources ?? []) + .map(r => [r.id, r]), + ), + ), + [individual], + ); + + +export const useResourcesByNamespacePrefix = (individual) => { + const resources = useResources(individual); + return useMemo( + () => Object.fromEntries(resources.map(r => [r.namespace_prefix, r])), + [individual] + ); +}; + +export const ontologyTermSorter = (k) => (a, b) => { + if (a[k]?.label && b[k]?.label) { + return a[k].label.toString().localeCompare(b[k].label.toString()); + } + return 0; +}; diff --git a/src/modules/explorer/reducers.js b/src/modules/explorer/reducers.js index 4dfeee4e7..8bb560a45 100644 --- a/src/modules/explorer/reducers.js +++ b/src/modules/explorer/reducers.js @@ -377,12 +377,11 @@ function generateBiosampleObjects(searchResults) { experimentIds: [], experimentTypes: [], studyTypes: [], - sampledTissues: [], + sampledTissue: biosample["sampled_tissue"], }; objects[index].experimentIds.push(biosample.experiment["experiment_id"]); objects[index].experimentTypes.push(biosample.experiment["experiment_type"]); objects[index].studyTypes.push(biosample.experiment["study_type"]); - objects[index].sampledTissues.push(biosample["sampled_tissue"]); } return objects; }, []); diff --git a/src/propTypes.js b/src/propTypes.js index abc384fa7..4f073fe21 100644 --- a/src/propTypes.js +++ b/src/propTypes.js @@ -287,7 +287,21 @@ export const phenopacketPropTypesShape = PropTypes.shape({ biosamples: biosamplePropTypesShape.isRequired, diseases: diseasePropTypesShape.isRequired, phenotypic_features: phenotypicFeaturePropTypesShape, - meta_data: PropTypes.object.isRequired, // TODO: Shape + meta_data: PropTypes.shape({ + created: PropTypes.string, + created_by: PropTypes.string, + submitted_by: PropTypes.string, + resources: PropTypes.arrayOf(PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + namespace_prefix: PropTypes.string, + url: PropTypes.string, + version: PropTypes.string, + iri_prefix: PropTypes.string, + extra_properties: PropTypes.object, + })), + phenopacket_schema_version: PropTypes.string, + }).isRequired, created: PropTypes.string, // ISO datetime string updated: PropTypes.string, // ISO datetime string }); @@ -297,10 +311,10 @@ export const experimentPropTypesShape = PropTypes.shape({ study_type: PropTypes.string, experiment_type: PropTypes.string.isRequired, - experiment_ontology: PropTypes.arrayOf(PropTypes.object), // TODO: Array of ontology terms + experiment_ontology: PropTypes.arrayOf(ontologyShape), // TODO: Array of ontology terms molecule: PropTypes.string, - molecule_ontology: PropTypes.arrayOf(PropTypes.object), // TODO: Array of ontology terms + molecule_ontology: PropTypes.arrayOf(ontologyShape), // TODO: Array of ontology terms library_strategy: PropTypes.string, library_source: PropTypes.string, From b26eb3ac7676a593d21c25953295229a2cb622a0 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 13:55:10 -0400 Subject: [PATCH 29/79] chore: use OntologyTerm for disease onset if ontology class --- src/components/explorer/IndividualDiseases.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/explorer/IndividualDiseases.js b/src/components/explorer/IndividualDiseases.js index 379b31409..950b2ac12 100644 --- a/src/components/explorer/IndividualDiseases.js +++ b/src/components/explorer/IndividualDiseases.js @@ -40,8 +40,8 @@ const IndividualDiseases = ({ individual }) => { : (disease.onset.hasOwnProperty("start") && Object.keys(disease.onset.start).length) ?
{disease.onset.start.age} - {disease.onset.end.age}
// Onset age label only - : (disease.onset.hasOwnProperty("label") && Object.keys(disease.onset.label).length) - ?
{disease.onset.label} ({disease.onset.id})
+ : disease.onset.label + ? : EM_DASH : EM_DASH, }, @@ -49,7 +49,7 @@ const IndividualDiseases = ({ individual }) => { title: "Extra Properties", key: "extra_properties", render: (_, individual) => - (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) + (Object.keys(individual.extra_properties ?? {}).length) ?
{JSON.stringify(individual.extra_properties, null, 2)}
From c663639467ba11fe70db7fe0d1979fe084e197e1 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:03:24 -0400 Subject: [PATCH 30/79] lint --- src/components/explorer/IndividualExperiments.js | 2 +- .../explorer/IndividualPhenotypicFeatures.js | 12 ++++++------ src/components/explorer/utils.js | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 3409f9be4..45b604787 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -168,7 +168,7 @@ const IndividualExperiments = ({ individual }) => { > {(e.molecule_ontology ?? []).map((mo) => ( - + ))} diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index b4fdb6f2b..339d71068 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -34,19 +34,19 @@ const IndividualPhenotypicFeatures = ({ individual }) => { }, { title: "Negated", - dataIndex: "negated", + dataIndex: "negated", render: (negated) => (negated ?? "false").toString(), }, { title: "Extra Properties", - dataIndex: "extra_properties", + dataIndex: "extra_properties", render: (extraProperties) => - (Object.keys(extraProperties ?? {}).length) - ?
{JSON.stringify(extraProperties ?? {}, null, 2)}
- : EM_DASH, + (Object.keys(extraProperties ?? {}).length) + ?
{JSON.stringify(extraProperties ?? {}, null, 2)}
+ : EM_DASH, }, - ], [individual]) + ], [individual]); return (
.map(b => [b.id, b]), ), ), - [individual] + [individual], ); @@ -31,7 +31,7 @@ export const useResourcesByNamespacePrefix = (individual) => { const resources = useResources(individual); return useMemo( () => Object.fromEntries(resources.map(r => [r.namespace_prefix, r])), - [individual] + [individual], ); }; From bfc96981459c81f78d164de0212b81e4ce848412 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:05:29 -0400 Subject: [PATCH 31/79] style: render list of molecule ontology terms for experiments nicely --- src/components/explorer/IndividualExperiments.js | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 45b604787..669c087c4 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -167,8 +167,11 @@ const IndividualExperiments = ({ individual }) => { key={e.id} > - {(e.molecule_ontology ?? []).map((mo) => ( - + {(e.molecule_ontology ?? []).map((mo, i) => ( + <> + + {i < ((e.molecule_ontology ?? []).length - 1) ? "; " : ""} + ))} From 70752f0ad8862c3902374d8e7f6c928185d17f0c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:05:49 -0400 Subject: [PATCH 32/79] fix: molecule ontology keys --- src/components/explorer/IndividualExperiments.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 669c087c4..5dda484f8 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -168,10 +168,10 @@ const IndividualExperiments = ({ individual }) => { > {(e.molecule_ontology ?? []).map((mo, i) => ( - <> - + + {i < ((e.molecule_ontology ?? []).length - 1) ? "; " : ""} - + ))} From ea7bba4a31ca9ee5c8ccccb1b6adcbb4be72f24c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:28:05 -0400 Subject: [PATCH 33/79] fix(explorer): handle malformed term IDs in OntologyTerm by rendering as plain --- src/components/explorer/OntologyTerm.js | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/src/components/explorer/OntologyTerm.js b/src/components/explorer/OntologyTerm.js index b1d39e22d..0925f51b3 100644 --- a/src/components/explorer/OntologyTerm.js +++ b/src/components/explorer/OntologyTerm.js @@ -7,21 +7,32 @@ import { id } from "../../utils/misc"; import { useResourcesByNamespacePrefix } from "./utils"; +const OntologyTermPlain = memo(({ term, renderLabel }) => ( + {renderLabel(term.label)} (ID: {term.id}) +)); +OntologyTermPlain.propTypes = { + term: ontologyShape.isRequired, + renderLabel: PropTypes.func.isRequired, +}; + const OntologyTerm = memo(({ individual, term, renderLabel }) => { // TODO: perf: might be slow to generate this over and over const resourcesByNamespacePrefix = useResourcesByNamespacePrefix(individual); if (!term) return <>{EM_DASH}; + if (!term.id.includes(":")) { + // Malformed ID, render as plain text + return ; + } + const [namespacePrefix, namespaceID] = term.id.split(":"); const termResource = resourcesByNamespacePrefix[namespacePrefix]; // If resource doesn't exist / isn't linkable, render the term as an un-clickable plain if (!termResource || !termResource.iri_prefix || termResource.iri_prefix.includes("example.org")) { - return ( - {renderLabel(term.label)} (ID: {term.id}) - ); + return ; } return ( From 21968a1a9ab4ad041c951e004e3b5b8056a4f98c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:29:04 -0400 Subject: [PATCH 34/79] lint --- src/components/explorer/OntologyTerm.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/components/explorer/OntologyTerm.js b/src/components/explorer/OntologyTerm.js index 0925f51b3..04fae105a 100644 --- a/src/components/explorer/OntologyTerm.js +++ b/src/components/explorer/OntologyTerm.js @@ -23,7 +23,9 @@ const OntologyTerm = memo(({ individual, term, renderLabel }) => { if (!term.id.includes(":")) { // Malformed ID, render as plain text - return ; + return ( + + ); } const [namespacePrefix, namespaceID] = term.id.split(":"); @@ -32,7 +34,9 @@ const OntologyTerm = memo(({ individual, term, renderLabel }) => { // If resource doesn't exist / isn't linkable, render the term as an un-clickable plain if (!termResource || !termResource.iri_prefix || termResource.iri_prefix.includes("example.org")) { - return ; + return ( + + ); } return ( From 6cef098386c7ed2b85f4fa706f9f7f89befc4973 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:37:43 -0400 Subject: [PATCH 35/79] fix(explorer): bad handling of accidental array ontology terms --- .../explorer/IndividualExperiments.js | 23 +++++++++++++------ src/components/explorer/OntologyTerm.js | 20 ++++++++++++++-- 2 files changed, 34 insertions(+), 9 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 5dda484f8..d04a87e4b 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -167,15 +167,24 @@ const IndividualExperiments = ({ individual }) => { key={e.id} > - {(e.molecule_ontology ?? []).map((mo, i) => ( - - - {i < ((e.molecule_ontology ?? []).length - 1) ? "; " : ""} - - ))} + {/* + molecule_ontology is accidentally an array in Katsu, so this takes the first item + and falls back to just the field (if we fix this in the future) + */} + - + {/* + experiment_ontology is accidentally an array in Katsu, so this takes the first item + and falls back to just the field (if we fix this in the future) + */} + { // TODO: perf: might be slow to generate this over and over const resourcesByNamespacePrefix = useResourcesByNamespacePrefix(individual); - if (!term) return <>{EM_DASH}; + if (!term) { + return ( + <>{EM_DASH} + ); + } + + useEffect(() => { + if (!term.id || !term.label) { + console.error("Invalid term provided to OntologyTerm component:", term); + } + }, [term]); + + if (!term.id || !term.label) { + return ( + <>{EM_DASH} + ); + } if (!term.id.includes(":")) { // Malformed ID, render as plain text From b1a7fd205b9c9710eaa64b2c0acbfe85d38783b7 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:38:50 -0400 Subject: [PATCH 36/79] lint --- src/components/explorer/OntologyTerm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/OntologyTerm.js b/src/components/explorer/OntologyTerm.js index 0dc268091..f0a1bffda 100644 --- a/src/components/explorer/OntologyTerm.js +++ b/src/components/explorer/OntologyTerm.js @@ -1,4 +1,4 @@ -import React, {memo, useEffect} from "react"; +import React, { memo, useEffect } from "react"; import PropTypes from "prop-types"; import { EM_DASH } from "../../constants"; From b3214311f7cd53e86d6894eb85603a9bc9bb77f2 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 14:41:33 -0400 Subject: [PATCH 37/79] fix(explorer): label molecule ontology --- src/components/explorer/IndividualExperiments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index d04a87e4b..71c0f1be7 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -166,7 +166,7 @@ const IndividualExperiments = ({ individual }) => { size="small" key={e.id} > - + {/* molecule_ontology is accidentally an array in Katsu, so this takes the first item and falls back to just the field (if we fix this in the future) From 7dd92c42afa7853a7f75341df3c29c7ad41260e7 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 15:04:11 -0400 Subject: [PATCH 38/79] lint --- .../explorer/searchResultsTables/BiosamplesTable.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/components/explorer/searchResultsTables/BiosamplesTable.js b/src/components/explorer/searchResultsTables/BiosamplesTable.js index 04ce5c2f8..5d4deefd8 100644 --- a/src/components/explorer/searchResultsTables/BiosamplesTable.js +++ b/src/components/explorer/searchResultsTables/BiosamplesTable.js @@ -2,15 +2,15 @@ import React from "react"; import PropTypes from "prop-types"; import { useSortedColumns } from "../hooks/explorerHooks"; import { useSelector } from "react-redux"; -import { countNonNullElements } from "../../../utils/misc"; - -import ExplorerSearchResultsTable from "../ExplorerSearchResultsTable"; import BiosampleIDCell from "./BiosampleIDCell"; +import ExplorerSearchResultsTable from "../ExplorerSearchResultsTable"; import IndividualIDCell from "./IndividualIDCell"; -import {ontologyShape} from "../../../propTypes"; import OntologyTerm from "../OntologyTerm"; -import {ontologyTermSorter} from "../utils"; + +import { ontologyShape } from "../../../propTypes"; +import { countNonNullElements } from "../../../utils/misc"; +import { ontologyTermSorter } from "../utils"; const NO_EXPERIMENTS_VALUE = -Infinity; From bd20aac715a811ca0edd217a9dbaa56875c05bc3 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 15:46:55 -0400 Subject: [PATCH 39/79] style: unbold phenotypic feature ontology term --- src/components/explorer/IndividualPhenotypicFeatures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 339d71068..28264f282 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -29,7 +29,7 @@ const IndividualPhenotypicFeatures = ({ individual }) => { title: "Type", dataIndex: "type", render: (type) => ( - ({label})} /> + ), }, { From ad3b92d7eb0923ae94b2922868286780b196921b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 15:59:23 -0400 Subject: [PATCH 40/79] style(explorer): display ontology def link as a little icon beside the label/id --- src/components/explorer/OntologyTerm.js | 47 +++++++++++-------------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/components/explorer/OntologyTerm.js b/src/components/explorer/OntologyTerm.js index f0a1bffda..41a192869 100644 --- a/src/components/explorer/OntologyTerm.js +++ b/src/components/explorer/OntologyTerm.js @@ -1,20 +1,14 @@ import React, { memo, useEffect } from "react"; import PropTypes from "prop-types"; +import { Icon } from "antd"; + import { EM_DASH } from "../../constants"; import { individualPropTypesShape, ontologyShape } from "../../propTypes"; import { id } from "../../utils/misc"; import { useResourcesByNamespacePrefix } from "./utils"; -const OntologyTermPlain = memo(({ term, renderLabel }) => ( - {renderLabel(term.label)} (ID: {term.id}) -)); -OntologyTermPlain.propTypes = { - term: ontologyShape.isRequired, - renderLabel: PropTypes.func.isRequired, -}; - const OntologyTerm = memo(({ individual, term, renderLabel }) => { // TODO: perf: might be slow to generate this over and over const resourcesByNamespacePrefix = useResourcesByNamespacePrefix(individual); @@ -37,28 +31,29 @@ const OntologyTerm = memo(({ individual, term, renderLabel }) => { ); } - if (!term.id.includes(":")) { - // Malformed ID, render as plain text - return ( - - ); - } - - const [namespacePrefix, namespaceID] = term.id.split(":"); + /** + * @type {string|null} + */ + let defLink = null; - const termResource = resourcesByNamespacePrefix[namespacePrefix]; + if (term.id.includes(":")) { + const [namespacePrefix, namespaceID] = term.id.split(":"); + const termResource = resourcesByNamespacePrefix[namespacePrefix]; - // If resource doesn't exist / isn't linkable, render the term as an un-clickable plain - if (!termResource || !termResource.iri_prefix || termResource.iri_prefix.includes("example.org")) { - return ( - - ); - } + if (termResource?.iri_prefix && !termResource.iri_prefix.includes("example.org")) { + defLink = `${termResource.iri_prefix}${namespaceID}`; + } // If resource doesn't exist / isn't linkable, don't include a link + } // Otherwise, malformed ID - render without a link return ( - - {renderLabel(term.label)} (ID: {term.id}) - + + {renderLabel(term.label)} (ID: {term.id}){" "} + {defLink && ( + + + + )} + ); }); From 7bb1eeaad981f493d4ae23ea11572a146387534a Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 16:23:15 -0400 Subject: [PATCH 41/79] feat(explorer): individual phenopackets JSON view --- .../explorer/ExplorerIndividualContent.js | 46 +++++++++++-------- .../explorer/IndividualPhenopackets.js | 21 +++++++++ 2 files changed, 49 insertions(+), 18 deletions(-) create mode 100644 src/components/explorer/IndividualPhenopackets.js diff --git a/src/components/explorer/ExplorerIndividualContent.js b/src/components/explorer/ExplorerIndividualContent.js index 664102a4d..25931ed8c 100644 --- a/src/components/explorer/ExplorerIndividualContent.js +++ b/src/components/explorer/ExplorerIndividualContent.js @@ -7,11 +7,11 @@ import ReactRouterPropTypes from "react-router-prop-types"; import {Layout, Menu, Skeleton} from "antd"; -import {fetchIndividualIfNecessary} from "../../modules/metadata/actions"; -import {individualPropTypesShape} from "../../propTypes"; -import {LAYOUT_CONTENT_STYLE} from "../../styles/layoutContent"; -import {matchingMenuKeys, renderMenuItem} from "../../utils/menu"; -import {urlPath} from "../../utils/url"; +import { fetchIndividualIfNecessary } from "../../modules/metadata/actions"; +import { individualPropTypesShape } from "../../propTypes"; +import { LAYOUT_CONTENT_STYLE } from "../../styles/layoutContent"; +import { matchingMenuKeys, renderMenuItem } from "../../utils/menu"; +import { urlPath } from "../../utils/url"; import SitePageHeader from "../SitePageHeader"; import IndividualOverview from "./IndividualOverview"; @@ -23,9 +23,9 @@ import IndividualMetadata from "./IndividualMetadata"; import IndividualVariants from "./IndividualVariants"; import IndividualGenes from "./IndividualGenes"; import IndividualTracks from "./IndividualTracks"; -import {BENTO_URL} from "../../config"; +import IndividualPhenopackets from "./IndividualPhenopackets"; -const withURLPrefix = (individual, page) => `/data/explorer/individuals/${individual}/${page}`; +import { BENTO_URL } from "../../config"; const MENU_STYLE = { marginLeft: "-24px", @@ -69,22 +69,28 @@ class ExplorerIndividualContent extends Component { render() { // TODO: Disease content - highlight what was found in search results? + console.log(this.props.match); + const individualID = this.props.match.params.individual || null; const individualInfo = this.props.individuals[individualID] || {}; const individual = individualInfo.data; - const overviewUrl = withURLPrefix(individualID, "overview"); - const pfeaturesUrl = withURLPrefix(individualID, "phenotypicfeatures"); - const biosamplesUrl = withURLPrefix(individualID, "biosamples"); - const experimentsUrl = withURLPrefix(individualID, "experiments"); - const variantsUrl = withURLPrefix(individualID, "variants"); - const genesUrl = withURLPrefix(individualID, "genes"); - const diseasesUrl = withURLPrefix(individualID, "diseases"); - const metadataUrl = withURLPrefix(individualID, "metadata"); - const tracksUrl = withURLPrefix(individualID, "tracks"); + const individualUrl = this.props.match.url; + + const overviewUrl = `${individualUrl}/overview`; + const phenotypicFeaturesUrl = `${individualUrl}/phenotypic-features`; + const biosamplesUrl = `${individualUrl}/biosamples`; + const experimentsUrl = `${individualUrl}/experiments`; + const variantsUrl = `${individualUrl}/variants`; + const genesUrl = `${individualUrl}/genes`; + const diseasesUrl = `${individualUrl}/diseases`; + const metadataUrl = `${individualUrl}/metadata`; + const tracksUrl = `${individualUrl}/tracks`; + const phenopacketsUrl = `${individualUrl}/phenopackets`; + const individualMenu = [ {url: overviewUrl, style: {marginLeft: "4px"}, text: "Overview"}, - {url: pfeaturesUrl, text: "Phenotypic Features"}, + {url: phenotypicFeaturesUrl, text: "Phenotypic Features"}, {url: biosamplesUrl, text: "Biosamples"}, {url: experimentsUrl, text: "Experiments"}, {url: tracksUrl, text: "Tracks"}, @@ -92,6 +98,7 @@ class ExplorerIndividualContent extends Component { {url: genesUrl, text: "Genes"}, {url: diseasesUrl, text: "Diseases"}, {url: metadataUrl, text: "Metadata"}, + {url: phenopacketsUrl, text: "Phenopackets JSON"}, ]; const selectedKeys = matchingMenuKeys(individualMenu, urlPath(BENTO_URL)); @@ -121,7 +128,7 @@ class ExplorerIndividualContent extends Component { - + @@ -145,6 +152,9 @@ class ExplorerIndividualContent extends Component { + + + : } diff --git a/src/components/explorer/IndividualPhenopackets.js b/src/components/explorer/IndividualPhenopackets.js new file mode 100644 index 000000000..2a38dd8d4 --- /dev/null +++ b/src/components/explorer/IndividualPhenopackets.js @@ -0,0 +1,21 @@ +import React from "react"; + +import { individualPropTypesShape } from "../../propTypes"; +import ReactJson from "react-json-view"; + +const IndividualPhenopackets = ({ individual }) => { + return ( + + ); +}; + +IndividualPhenopackets.propTypes = { + individual: individualPropTypesShape.isRequired, +}; + +export default IndividualPhenopackets; From d6b8f7bb9cbc795a489a63303b75fefc4bff4d34 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 11 Sep 2023 16:47:28 -0400 Subject: [PATCH 42/79] lint --- src/components/explorer/IndividualPhenopackets.js | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/components/explorer/IndividualPhenopackets.js b/src/components/explorer/IndividualPhenopackets.js index 2a38dd8d4..05e9b496d 100644 --- a/src/components/explorer/IndividualPhenopackets.js +++ b/src/components/explorer/IndividualPhenopackets.js @@ -1,7 +1,6 @@ import React from "react"; - -import { individualPropTypesShape } from "../../propTypes"; import ReactJson from "react-json-view"; +import { individualPropTypesShape } from "../../propTypes"; const IndividualPhenopackets = ({ individual }) => { return ( From d57091f299559b83a02e47b778e1b99fa946a959 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 12 Sep 2023 12:04:34 -0400 Subject: [PATCH 43/79] feat(explorer): add download button to phenopackets json (req. katsu + gateway updates) --- .../{manager => }/DownloadButton.js | 0 .../explorer/IndividualPhenopackets.js | 27 ++++++++++++++----- .../manager/ManagerDropBoxContent.js | 2 +- .../manager/drs/ManagerDRSContent.js | 2 +- 4 files changed, 23 insertions(+), 8 deletions(-) rename src/components/{manager => }/DownloadButton.js (100%) diff --git a/src/components/manager/DownloadButton.js b/src/components/DownloadButton.js similarity index 100% rename from src/components/manager/DownloadButton.js rename to src/components/DownloadButton.js diff --git a/src/components/explorer/IndividualPhenopackets.js b/src/components/explorer/IndividualPhenopackets.js index 05e9b496d..9ae28f639 100644 --- a/src/components/explorer/IndividualPhenopackets.js +++ b/src/components/explorer/IndividualPhenopackets.js @@ -1,15 +1,30 @@ import React from "react"; +import { useSelector } from "react-redux"; + +import { Divider } from "antd"; import ReactJson from "react-json-view"; + import { individualPropTypesShape } from "../../propTypes"; +import DownloadButton from "../DownloadButton"; + const IndividualPhenopackets = ({ individual }) => { + const katsuUrl = useSelector((state) => state.services.metadataService?.url ?? ""); + const downloadUrl = `${katsuUrl}/api/individuals/${individual.id}/phenopackets?attachment=1&format=json`; + + console.log(downloadUrl); + return ( - + <> + Download JSON + + + ); }; diff --git a/src/components/manager/ManagerDropBoxContent.js b/src/components/manager/ManagerDropBoxContent.js index 9807640db..6416def39 100644 --- a/src/components/manager/ManagerDropBoxContent.js +++ b/src/components/manager/ManagerDropBoxContent.js @@ -36,7 +36,7 @@ import { } from "antd"; import {LAYOUT_CONTENT_STYLE} from "../../styles/layoutContent"; -import DownloadButton from "./DownloadButton"; +import DownloadButton from "../DownloadButton"; import DropBoxTreeSelect from "./DropBoxTreeSelect"; import JsonDisplay from "../JsonDisplay"; import DatasetSelectionModal from "./DatasetSelectionModal"; diff --git a/src/components/manager/drs/ManagerDRSContent.js b/src/components/manager/drs/ManagerDRSContent.js index b40a254bb..23ce377e7 100644 --- a/src/components/manager/drs/ManagerDRSContent.js +++ b/src/components/manager/drs/ManagerDRSContent.js @@ -8,7 +8,7 @@ import { Layout, Input, Table, Descriptions, message } from "antd"; import { LAYOUT_CONTENT_STYLE } from "../../../styles/layoutContent"; import { useAuthorizationHeader } from "../../../lib/auth/utils"; -import DownloadButton from "../DownloadButton"; +import DownloadButton from "../../DownloadButton"; const SEARCH_CONTAINER_STYLE = { maxWidth: 800, From 4eb761e990fb22838fb034e5661e9f6f5e9a5f14 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 12 Sep 2023 12:04:49 -0400 Subject: [PATCH 44/79] chore: don't cache responses in service list request modal --- src/components/services/ServiceRequestModal.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/components/services/ServiceRequestModal.js b/src/components/services/ServiceRequestModal.js index 34fdf74a4..6f005bdd4 100644 --- a/src/components/services/ServiceRequestModal.js +++ b/src/components/services/ServiceRequestModal.js @@ -31,7 +31,10 @@ const ServiceRequestModal = ({service, onCancel}) => { const p = requestPath.replace(/^\//, ""); try { const res = await fetch(`${serviceUrl}/${p}`, { - headers: authHeader, + headers: { + ...authHeader, + "Cache-Control": "no-cache", + }, }); if ((res.headers.get("content-type") ?? "").includes("application/json")) { From 87b2a7448e2ce76fc842fac3a6467b339339cf7a Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 12 Sep 2023 12:06:07 -0400 Subject: [PATCH 45/79] lint: rm debug log --- src/components/explorer/ExplorerIndividualContent.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/components/explorer/ExplorerIndividualContent.js b/src/components/explorer/ExplorerIndividualContent.js index 25931ed8c..7566aaa02 100644 --- a/src/components/explorer/ExplorerIndividualContent.js +++ b/src/components/explorer/ExplorerIndividualContent.js @@ -69,8 +69,6 @@ class ExplorerIndividualContent extends Component { render() { // TODO: Disease content - highlight what was found in search results? - console.log(this.props.match); - const individualID = this.props.match.params.individual || null; const individualInfo = this.props.individuals[individualID] || {}; const individual = individualInfo.data; From b5bc4ae7708be2c78bb6de5d3e9c6248f50b1ac5 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 13:15:18 -0400 Subject: [PATCH 46/79] fix(explorer): load actual phenopackets for individual --- .../explorer/IndividualPhenopackets.js | 33 +++++++++---- src/modules/metadata/actions.js | 15 ++++++ src/modules/metadata/reducers.js | 46 +++++++++++++++++++ 3 files changed, 84 insertions(+), 10 deletions(-) diff --git a/src/components/explorer/IndividualPhenopackets.js b/src/components/explorer/IndividualPhenopackets.js index 9ae28f639..d7f3fb268 100644 --- a/src/components/explorer/IndividualPhenopackets.js +++ b/src/components/explorer/IndividualPhenopackets.js @@ -1,29 +1,42 @@ -import React from "react"; -import { useSelector } from "react-redux"; +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; -import { Divider } from "antd"; +import { Divider, Skeleton } from "antd"; import ReactJson from "react-json-view"; +import { fetchIndividualPhenopacketsIfNecessary } from "../../modules/metadata/actions"; import { individualPropTypesShape } from "../../propTypes"; import DownloadButton from "../DownloadButton"; const IndividualPhenopackets = ({ individual }) => { + const dispatch = useDispatch(); + const katsuUrl = useSelector((state) => state.services.metadataService?.url ?? ""); const downloadUrl = `${katsuUrl}/api/individuals/${individual.id}/phenopackets?attachment=1&format=json`; - console.log(downloadUrl); + const { isFetching, data } = useSelector( + (state) => state.individuals.phenopacketsByIndividualID[individual.id] ?? {}, + ); + + useEffect(() => { + dispatch(fetchIndividualPhenopacketsIfNecessary(individual.id)); + }, [individual]); return ( <> Download JSON - + {(data === undefined || isFetching) ? ( + + ) : ( + + )} ); }; diff --git a/src/modules/metadata/actions.js b/src/modules/metadata/actions.js index 9e692b9a3..deb1c3317 100644 --- a/src/modules/metadata/actions.js +++ b/src/modules/metadata/actions.js @@ -24,6 +24,7 @@ export const SAVE_DATASET_LINKED_FIELD_SET = createNetworkActionTypes("SAVE_DATA export const DELETE_DATASET_LINKED_FIELD_SET = createNetworkActionTypes("DELETE_DATASET_LINKED_FIELD_SET"); export const FETCH_INDIVIDUAL = createNetworkActionTypes("FETCH_INDIVIDUAL"); +export const FETCH_INDIVIDUAL_PHENOPACKETS = createNetworkActionTypes("FETCH_INDIVIDUAL_PHENOPACKETS"); export const FETCH_OVERVIEW_SUMMARY = createNetworkActionTypes("FETCH_OVERVIEW_SUMMARY"); export const DELETE_DATASET_DATA_TYPE = createNetworkActionTypes("DELETE_DATASET_DATA_TYPE"); @@ -287,6 +288,20 @@ export const fetchIndividualIfNecessary = individualID => (dispatch, getState) = }; +const fetchIndividualPhenopackets = networkAction((individualID) => (dispatch, getState) => ({ + types: FETCH_INDIVIDUAL_PHENOPACKETS, + params: {individualID}, + url: `${getState().services.metadataService.url}/api/individuals/${individualID}/phenopackets`, + err: `Error fetching phenopackets for individual ${individualID}`, +})); + +export const fetchIndividualPhenopacketsIfNecessary = individualID => (dispatch, getState) => { + const record = getState().individuals.phenopacketsByIndividualID[individualID] || {}; + if (record.isFetching || record.data) return; // Don't fetch if already fetching or loaded. + return dispatch(fetchIndividualPhenopackets(individualID)); +}; + + export const fetchOverviewSummary = networkAction(() => (dispatch, getState) => ({ types: FETCH_OVERVIEW_SUMMARY, url: `${getState().services.metadataService.url}/api/overview`, diff --git a/src/modules/metadata/reducers.js b/src/modules/metadata/reducers.js index 0614fcdf7..baa1cfd41 100644 --- a/src/modules/metadata/reducers.js +++ b/src/modules/metadata/reducers.js @@ -16,6 +16,7 @@ import { DELETE_DATASET_LINKED_FIELD_SET, FETCH_INDIVIDUAL, + FETCH_INDIVIDUAL_PHENOPACKETS, FETCH_OVERVIEW_SUMMARY, @@ -293,10 +294,13 @@ export const biosamples = ( export const individuals = ( state = { itemsByID: {}, + phenopacketsByIndividualID: {}, }, action, ) => { switch (action.type) { + // FETCH_INDIVIDUAL + case FETCH_INDIVIDUAL.REQUEST: return { ...state, @@ -331,6 +335,48 @@ export const individuals = ( }, }; + // FETCH_INDIVIDUAL_PHENOPACKETS + + case FETCH_INDIVIDUAL_PHENOPACKETS.REQUEST: { + const { individualID } = action; + return { + ...state, + phenopacketsByIndividualID: { + ...state.phenopacketsByIndividualID, + [individualID]: { + ...(state.phenopacketsByIndividualID[individualID] ?? {}), + isFetching: true, + }, + }, + }; + } + case FETCH_INDIVIDUAL_PHENOPACKETS.RECEIVE: { + const { individualID, data } = action; + return { + ...state, + phenopacketsByIndividualID: { + ...state.phenopacketsByIndividualID, + [individualID]: { + ...(state.phenopacketsByIndividualID[individualID] ?? {}), + data, + }, + }, + }; + } + case FETCH_INDIVIDUAL_PHENOPACKETS.FINISH: { + const { individualID } = action; + return { + ...state, + phenopacketsByIndividualID: { + ...state.phenopacketsByIndividualID, + [individualID]: { + ...(state.phenopacketsByIndividualID[individualID] ?? {}), + isFetching: false, + }, + }, + }; + } + default: return state; } From 532613c47ea65a95cfc4068de485a8569929b1a7 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 13:20:01 -0400 Subject: [PATCH 47/79] style(explorer): put individual loading skeleton in 'loading' mode --- src/components/explorer/ExplorerIndividualContent.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/ExplorerIndividualContent.js b/src/components/explorer/ExplorerIndividualContent.js index 7566aaa02..bb38ec37f 100644 --- a/src/components/explorer/ExplorerIndividualContent.js +++ b/src/components/explorer/ExplorerIndividualContent.js @@ -154,7 +154,7 @@ class ExplorerIndividualContent extends Component { - : } + : } ; From 1dda50ff392cbeb302aa06af251b7e510a86cdfb Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 13:50:37 -0400 Subject: [PATCH 48/79] fix: no target blank for download --- src/components/DownloadButton.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/DownloadButton.js b/src/components/DownloadButton.js index 7a42a0f80..693acf12e 100644 --- a/src/components/DownloadButton.js +++ b/src/components/DownloadButton.js @@ -11,7 +11,6 @@ const DownloadButton = ({ disabled, uri, children }) => { const form = document.createElement("form"); form.method = "post"; - form.target = "_blank"; form.action = uri; form.innerHTML = ``; document.body.appendChild(form); From 2d0c49839637e465e51edf40e6d12e3df162aa16 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 14:43:18 -0400 Subject: [PATCH 49/79] fix: revert cache control in service request modal --- src/components/services/ServiceRequestModal.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/components/services/ServiceRequestModal.js b/src/components/services/ServiceRequestModal.js index 6f005bdd4..34fdf74a4 100644 --- a/src/components/services/ServiceRequestModal.js +++ b/src/components/services/ServiceRequestModal.js @@ -31,10 +31,7 @@ const ServiceRequestModal = ({service, onCancel}) => { const p = requestPath.replace(/^\//, ""); try { const res = await fetch(`${serviceUrl}/${p}`, { - headers: { - ...authHeader, - "Cache-Control": "no-cache", - }, + headers: authHeader, }); if ((res.headers.get("content-type") ?? "").includes("application/json")) { From cc9eb26009689d91dde5b5dce65d4050c55e98ed Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 15:51:41 -0400 Subject: [PATCH 50/79] feat(explorer): improve phenotypic feature display * separate phenotypic features by phenopacket (encounter) * remove separate column for negated/excluded & link to definition if true * render extra properties more compactly if only one is present --- .../explorer/IndividualPhenotypicFeatures.js | 96 ++++++++++++------- src/components/explorer/explorer.css | 7 ++ 2 files changed, 68 insertions(+), 35 deletions(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 28264f282..a31ddb57b 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -1,61 +1,87 @@ -import React, { useMemo } from "react"; +import React, {useCallback, useMemo} from "react"; -import { Table } from "antd"; +import {Card, Icon, Table} from "antd"; import { EM_DASH } from "../../constants"; import { individualPropTypesShape } from "../../propTypes"; import OntologyTerm from "./OntologyTerm"; const IndividualPhenotypicFeatures = ({ individual }) => { - // TODO: this logic might be technically incorrect with different versions of the same resource (i.e. ontology) - // across multiple phenopackets - const phenotypicFeatures = useMemo( - () => - Object.values( - Object.fromEntries( - (individual?.phenopackets ?? []) - .flatMap(p => (p.phenotypic_features ?? [])) - .map(pf => { - const pfID = `${pf.type.id}:${pf.negated}`; - return [pfID, {...pf, id: pfID}]; - }), - ), - ), - [individual], - ); - const columns = useMemo(() => [ { - title: "Type", - dataIndex: "type", - render: (type) => ( - - ), - }, - { - title: "Negated", - dataIndex: "negated", - render: (negated) => (negated ?? "false").toString(), + title: "Feature", + key: "feature", + render: ({ header, type, negated }) => ({ + children: header ? ( +

+ Phenopacket:{" "} + + {header} + +

+ ) : <> + {" "} + {negated ? ( + + (Excluded:{" "} + Found to be absent{" "} + + + ) + + ) : null} + , + props: { + colSpan: header ? 2 : 1, + }, + }), }, { title: "Extra Properties", dataIndex: "extra_properties", - render: (extraProperties) => - (Object.keys(extraProperties ?? {}).length) - ?
{JSON.stringify(extraProperties ?? {}, null, 2)}
- : EM_DASH, + render: (extraProperties, feature) => { + console.log(feature); + const nExtraProperties = Object.keys(extraProperties ?? {}).length; + return { + children: nExtraProperties ? ( +
+
+                                {JSON.stringify(
+                                    extraProperties,
+                                    null,
+                                    nExtraProperties === 1 ? null : 2,
+                                )}
+                            
+
+ ) : EM_DASH, // If no extra properties, just show a dash + props: { + colSpan: feature.header ? 0 : 1, + }, + }; + }, }, ], [individual]); + const data = useMemo(() => (individual?.phenopackets ?? []).flatMap((p) => [ + { + header: p.id, + key: p.id, + }, + ...p.phenotypic_features.map((pf) => ({...pf, key: `${pf.type.id}:${pf.negated}`})), + ]), [individual]); + return (
); }; diff --git a/src/components/explorer/explorer.css b/src/components/explorer/explorer.css index f3eb14a4b..c6e0996ee 100644 --- a/src/components/explorer/explorer.css +++ b/src/components/explorer/explorer.css @@ -10,6 +10,13 @@ vertical-align: top !important; } +/* Phenotypic Features tab */ + +.phenotypic-features-table td[colspan="2"] { + /*background-color: red;*/ + border-top: 2px solid #e8e8e8; +} + /* Variants tab */ .variantDescriptions .ant-descriptions-item-label{ From 34aa019b9762c31c41e96fbc484efc61f7f3de53 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 15:53:32 -0400 Subject: [PATCH 51/79] lint --- src/components/explorer/IndividualPhenotypicFeatures.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index a31ddb57b..c35d3f89f 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -1,6 +1,6 @@ -import React, {useCallback, useMemo} from "react"; +import React, { useMemo } from "react"; -import {Card, Icon, Table} from "antd"; +import { Icon, Table } from "antd"; import { EM_DASH } from "../../constants"; import { individualPropTypesShape } from "../../propTypes"; From 5de0137973703bfd4bb7b5f454545d3d720a6cf9 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 16:02:49 -0400 Subject: [PATCH 52/79] style(explorer): only show phenotypic feature header rows if >1 phenopacket --- .../explorer/IndividualPhenotypicFeatures.js | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index c35d3f89f..a8a6e8d52 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -65,13 +65,19 @@ const IndividualPhenotypicFeatures = ({ individual }) => { ], [individual]); - const data = useMemo(() => (individual?.phenopackets ?? []).flatMap((p) => [ - { - header: p.id, - key: p.id, - }, - ...p.phenotypic_features.map((pf) => ({...pf, key: `${pf.type.id}:${pf.negated}`})), - ]), [individual]); + const data = useMemo(() => { + const phenopackets = (individual?.phenopackets ?? []); + return phenopackets.flatMap((p) => [ + ...(phenopackets.length > 1 ? [{ + header: p.id, + key: p.id, + }] : []), // If there is just 1 phenopacket, don't include a header row + ...p.phenotypic_features.map((pf) => ({ + ...pf, + key: `${pf.type.id}:${pf.negated}`, + })), + ]) + }, [individual]); return (
Date: Wed, 13 Sep 2023 16:03:54 -0400 Subject: [PATCH 53/79] lint --- src/components/explorer/IndividualPhenotypicFeatures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index a8a6e8d52..6ea703bc5 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -76,7 +76,7 @@ const IndividualPhenotypicFeatures = ({ individual }) => { ...pf, key: `${pf.type.id}:${pf.negated}`, })), - ]) + ]); }, [individual]); return ( From 24348bac2b9eff350515950d2036287e0f2a0fe1 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 16:49:42 -0400 Subject: [PATCH 54/79] fix(explorer): use authorized-download button in experiments view --- src/components/DownloadButton.js | 4 +- .../explorer/IndividualExperiments.js | 40 ++++++++----------- 2 files changed, 19 insertions(+), 25 deletions(-) diff --git a/src/components/DownloadButton.js b/src/components/DownloadButton.js index 693acf12e..6caccf4b2 100644 --- a/src/components/DownloadButton.js +++ b/src/components/DownloadButton.js @@ -23,7 +23,7 @@ const DownloadButton = ({ disabled, uri, children }) => { }, [uri, accessToken]); return ( - ); @@ -32,12 +32,14 @@ const DownloadButton = ({ disabled, uri, children }) => { DownloadButton.defaultProps = { disabled: false, children: "Download", + type: "default", }; DownloadButton.propTypes = { disabled: PropTypes.bool, uri: PropTypes.string, children: PropTypes.oneOfType([PropTypes.object, PropTypes.string]), + type: PropTypes.oneOf(["primary", "ghost", "dashed", "danger", "link", "default"]), }; export default DownloadButton; diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 71c0f1be7..785616d20 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -1,42 +1,34 @@ import React, { useEffect, useMemo } from "react"; import { useDispatch, useSelector } from "react-redux"; import { useHistory } from "react-router-dom"; -import { Button, Collapse, Descriptions, Empty, Icon, Popover, Table } from "antd"; -import JsonView from "./JsonView"; -import FileSaver from "file-saver"; +import PropTypes from "prop-types"; + +import { Button, Collapse, Descriptions, Empty, Popover, Table } from "antd"; + import { EM_DASH } from "../../constants"; import { individualPropTypesShape } from "../../propTypes"; import { getFileDownloadUrlsFromDrs } from "../../modules/drs/actions"; import { guessFileType } from "../../utils/guessFileType"; -import PropTypes from "prop-types"; -import {useDeduplicatedIndividualBiosamples} from "./utils"; + +import { useDeduplicatedIndividualBiosamples } from "./utils"; + +import JsonView from "./JsonView"; import OntologyTerm from "./OntologyTerm"; +import DownloadButton from "../DownloadButton"; const { Panel } = Collapse; -const DownloadButton = ({ resultFile }) => { +const ExperimentResultDownloadButton = ({ resultFile }) => { const downloadUrls = useSelector((state) => state.drs.downloadUrlsByFilename); const url = downloadUrls[resultFile.filename]?.url; - if (!url) { - return <>{EM_DASH}; - } - - const saveAs = () => - FileSaver.saveAs( - downloadUrls[resultFile.filename].url, - resultFile.filename, - ); - - return ( -
- - - -
+ return url ? ( + + ) : ( + {EM_DASH} ); }; -DownloadButton.propTypes = { +ExperimentResultDownloadButton.propTypes = { resultFile: PropTypes.shape({ filename: PropTypes.string, }), @@ -63,7 +55,7 @@ const EXPERIMENT_RESULTS_COLUMNS = [ title: "Download", key: "download", align: "center", - render: (_, result) => , + render: (_, result) => , }, { title: "Other Details", From 7250e3397272e7365996686f1c9ead41766c0c6d Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 16:57:45 -0400 Subject: [PATCH 55/79] fix: download button --- src/components/DownloadButton.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/DownloadButton.js b/src/components/DownloadButton.js index 6caccf4b2..676661d69 100644 --- a/src/components/DownloadButton.js +++ b/src/components/DownloadButton.js @@ -3,7 +3,7 @@ import React, { useCallback } from "react"; import { Button } from "antd"; import PropTypes from "prop-types"; -const DownloadButton = ({ disabled, uri, children }) => { +const DownloadButton = ({ disabled, uri, children, type }) => { const { accessToken } = useSelector((state) => state.auth); const onClick = useCallback(() => { From 240b30dc300b3fa857983eb556e5091efb6a9e3e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 17:04:16 -0400 Subject: [PATCH 56/79] style(explorer): icon only download btn --- src/components/explorer/IndividualExperiments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 785616d20..e04682c12 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -23,7 +23,7 @@ const ExperimentResultDownloadButton = ({ resultFile }) => { const url = downloadUrls[resultFile.filename]?.url; return url ? ( - + ) : ( {EM_DASH} ); From f91a0d5dd69a024b61e686ac283685d61620425b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 17:06:47 -0400 Subject: [PATCH 57/79] fix(explorer): download button icon-only again --- src/components/DownloadButton.js | 3 +-- src/components/explorer/IndividualExperiments.js | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/components/DownloadButton.js b/src/components/DownloadButton.js index 676661d69..9ffdf92c3 100644 --- a/src/components/DownloadButton.js +++ b/src/components/DownloadButton.js @@ -24,14 +24,13 @@ const DownloadButton = ({ disabled, uri, children, type }) => { return ( ); }; DownloadButton.defaultProps = { disabled: false, - children: "Download", type: "default", }; diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index e04682c12..1678fc704 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -23,7 +23,7 @@ const ExperimentResultDownloadButton = ({ resultFile }) => { const url = downloadUrls[resultFile.filename]?.url; return url ? ( - + ) : ( {EM_DASH} ); From 98fedad036264eb188ec17dcef2e300b04c83a5b Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 17:08:09 -0400 Subject: [PATCH 58/79] fix(explorer): crash in experiment download button --- src/components/explorer/IndividualExperiments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 1678fc704..aa90b74eb 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -25,7 +25,7 @@ const ExperimentResultDownloadButton = ({ resultFile }) => { return url ? ( ) : ( - {EM_DASH} + <>EM_DASH ); }; ExperimentResultDownloadButton.propTypes = { From 339eca68a446f33b988a630ce9018bf0333c2286 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 13 Sep 2023 17:12:04 -0400 Subject: [PATCH 59/79] lint, maybe --- src/components/explorer/IndividualExperiments.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index aa90b74eb..3f94ef026 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -23,9 +23,9 @@ const ExperimentResultDownloadButton = ({ resultFile }) => { const url = downloadUrls[resultFile.filename]?.url; return url ? ( - + {""} ) : ( - <>EM_DASH + <>{EM_DASH} ); }; ExperimentResultDownloadButton.propTypes = { From d7cc572c92fe29d436616cc29bd886c9424bd6ca Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 19 Sep 2023 15:51:13 -0400 Subject: [PATCH 60/79] fix: if katsu cannot be contacted, show an error in the projects/datasets page --- .../projects/ManagerProjectDatasetContent.js | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/components/manager/projects/ManagerProjectDatasetContent.js b/src/components/manager/projects/ManagerProjectDatasetContent.js index 63cb62a04..7912c6b12 100644 --- a/src/components/manager/projects/ManagerProjectDatasetContent.js +++ b/src/components/manager/projects/ManagerProjectDatasetContent.js @@ -2,7 +2,7 @@ import React, {useCallback, useMemo} from "react"; import {Redirect, Route, Switch} from "react-router-dom"; import {useDispatch, useSelector} from "react-redux"; -import {Button, Empty, Layout, Menu, Typography} from "antd"; +import {Button, Empty, Layout, Menu, Result, Typography} from "antd"; import ProjectCreationModal from "./ProjectCreationModal"; import ProjectSkeleton from "./ProjectSkeleton"; @@ -31,6 +31,7 @@ const ManagerProjectDatasetContent = () => { const {items} = useSelector(state => state.projects); const {isFetchingDependentData} = useSelector(state => state.auth); + const {metadataService, isFetchingAll: isFetchingAllServices} = useSelector(state => state.services); const projectMenuItems = useMemo(() => items.map(project => ({ url: `/admin/data/manager/projects/${project.identifier}`, @@ -41,6 +42,18 @@ const ManagerProjectDatasetContent = () => { () => dispatch(toggleProjectCreationModalAction()), [dispatch]); if (!isFetchingDependentData && projectMenuItems.length === 0) { + if (!isFetchingAllServices && metadataService === null) { + return + + + + ; + } + return <> From 3cddae77975fd771906411cddd3cbbea25d69bdd Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 20 Sep 2023 11:04:09 -0400 Subject: [PATCH 61/79] feat(explorer): new table/single-descriptions-based linkable experiments view --- .../explorer/IndividualBiosamples.js | 28 +- .../explorer/IndividualExperiments.js | 341 ++++++++++-------- src/components/explorer/explorer.css | 64 +--- .../searchResultsTables/ExperimentsTable.js | 3 +- src/propTypes.js | 2 + 5 files changed, 212 insertions(+), 226 deletions(-) diff --git a/src/components/explorer/IndividualBiosamples.js b/src/components/explorer/IndividualBiosamples.js index f49a0bab6..fb5728d3f 100644 --- a/src/components/explorer/IndividualBiosamples.js +++ b/src/components/explorer/IndividualBiosamples.js @@ -192,7 +192,6 @@ const IndividualBiosamples = ({individual, experimentsUrl}) => { const match = useRouteMatch(); const handleBiosampleClick = useCallback((bID) => { - console.log(match); if (!bID) { history.replace(match.url); return; @@ -201,26 +200,21 @@ const IndividualBiosamples = ({individual, experimentsUrl}) => { }, [history, match]); const handleExperimentClick = useCallback((eid) => { - const hashLink = experimentsUrl + "#" + eid; - history.push(hashLink); + history.push(`${experimentsUrl}/${eid}`); }, [experimentsUrl, history]); + const biosamplesNode = ( + + ); + return ( - - - - - - + {biosamplesNode} + {biosamplesNode} ); }; diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 3f94ef026..ac3508954 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -1,12 +1,12 @@ -import React, { useEffect, useMemo } from "react"; +import React, {useCallback, useEffect, useMemo} from "react"; import { useDispatch, useSelector } from "react-redux"; -import { useHistory } from "react-router-dom"; +import { Route, Switch, useHistory, useParams, useRouteMatch } from "react-router-dom"; import PropTypes from "prop-types"; -import { Button, Collapse, Descriptions, Empty, Popover, Table } from "antd"; +import { Button, Descriptions, Popover, Table, Typography } from "antd"; import { EM_DASH } from "../../constants"; -import { individualPropTypesShape } from "../../propTypes"; +import { experimentPropTypesShape, individualPropTypesShape } from "../../propTypes"; import { getFileDownloadUrlsFromDrs } from "../../modules/drs/actions"; import { guessFileType } from "../../utils/guessFileType"; @@ -16,8 +16,6 @@ import JsonView from "./JsonView"; import OntologyTerm from "./OntologyTerm"; import DownloadButton from "../DownloadButton"; -const { Panel } = Collapse; - const ExperimentResultDownloadButton = ({ resultFile }) => { const downloadUrls = useSelector((state) => state.drs.downloadUrlsByFilename); @@ -58,7 +56,6 @@ const EXPERIMENT_RESULTS_COLUMNS = [ render: (_, result) => , }, { - title: "Other Details", key: "other_details", align: "center", render: (_, result) => ( @@ -74,28 +71,28 @@ const EXPERIMENT_RESULTS_COLUMNS = [ column={1} size="small" > - + {result.identifier} - + {result.description} - + {result.filename} - + {result.file_format} - + {result.data_output_type} - + {result.usage} - + {result.creation_date} - + {result.created_by} @@ -103,15 +100,120 @@ const EXPERIMENT_RESULTS_COLUMNS = [ } trigger="click" > - + ), }, ]; -const IndividualExperiments = ({ individual }) => { +const ExperimentDetail = ({ individual, experiment }) => { + const { + id, + experiment_type: experimentType, + experiment_ontology: experimentOntology, + molecule, + molecule_ontology: moleculeOntology, + instrument, + study_type: studyType, + extraction_protocol: extractionProtocol, + library_layout: libraryLayout, + library_selection: librarySelection, + library_source: librarySource, + library_strategy: libraryStrategy, + experiment_results: experimentResults, + extra_properties: extraProperties, + } = experiment; + + const sortedExperimentResults = useMemo( + () => + [...(experimentResults || [])].sort((r1, r2) => r1.file_format > r2.file_format ? 1 : -1), + [experimentResults]); + + return ( +
+ {experimentType} - Details + + + {id} + + {experimentType} + + {/* + experiment_ontology is accidentally an array in Katsu, so this takes the first item + and falls back to just the field (if we fix this in the future) + */} + + + + {molecule} + + + {/* + molecule_ontology is accidentally an array in Katsu, so this takes the first item + and falls back to just the field (if we fix this in the future) + */} + + + {studyType} + {extractionProtocol} + {libraryLayout} + {librarySelection} + {librarySource} + {libraryStrategy} + +
+
+ Platform: {instrument.platform} +
+
+ ID:  + {instrument.identifier} +
+
+
+ + + +
+ {experimentType} - Results +
+ + ); +}; +ExperimentDetail.propTypes = { + individual: individualPropTypesShape, + experiment: experimentPropTypesShape, +}; + +const Experiments = ({ individual, handleExperimentClick }) => { const dispatch = useDispatch(); - const history = useHistory(); + + const { selectedExperiment } = useParams(); + const selectedRowKeys = useMemo( + () => selectedExperiment ? [selectedExperiment] : [], + [selectedExperiment], + ); + + useEffect(() => { + // If, on first load, there's a selected experiment: + // - find the experiment-${id} element (a span in the table row) + // - scroll it into view + setTimeout(() => { + if (selectedExperiment) { + const el = document.getElementById(`experiment-${selectedExperiment}`); + if (!el) return; + el.scrollIntoView(); + } + }, 100); + }, []); const biosamplesData = useDeduplicatedIndividualBiosamples(individual); const experimentsData = useMemo( @@ -119,7 +221,6 @@ const IndividualExperiments = ({ individual }) => { [biosamplesData], ); - useEffect(() => { // retrieve any download urls if experiments data changes @@ -134,140 +235,80 @@ const IndividualExperiments = ({ individual }) => { dispatch(getFileDownloadUrlsFromDrs(downloadableFiles)); }, [experimentsData]); - const selected = history.location.hash.slice(1); - const opened = (selected && selected.length > 1) ? [selected] : []; + const columns = useMemo( + () => [ + { + title: "Experiment Type", + dataIndex: "experiment_type", + render: (type, { id }) => {type}, // scroll anchor wrapper + }, + { + title: "Molecule", + dataIndex: "molecule_ontology", + render: (mo) => , + }, + { + title: "Experiment Results", + key: "experiment_results", + render: (exp) => {exp.experiment_results.length ?? 0} files, + }, + ], + [individual, handleExperimentClick], + ); + + const onExpand = useCallback( + (e, experiment) => { + handleExperimentClick(e ? experiment.id : undefined); + }, + [handleExperimentClick], + ); - if (!experimentsData.length) { - return ; - } + const expandedRowRender = useCallback( + (experiment) => ( + + ), + [handleExperimentClick], + ); return ( - - {experimentsData.map((e) => ( - -
-
- - - {/* - molecule_ontology is accidentally an array in Katsu, so this takes the first item - and falls back to just the field (if we fix this in the future) - */} - - - - {/* - experiment_ontology is accidentally an array in Katsu, so this takes the first item - and falls back to just the field (if we fix this in the future) - */} - - - - - - {e.instrument.platform} - - - {e.instrument.identifier} - - - - - - - - - {e.id} - - - {e.experiment_type} - - - {e.study_type} - - - {e.extraction_protocol} - - - {e.library_layout} - - - {e.library_selection} - - - {e.library_source} - - - {e.library_strategy} - - - - - - - - - - - -
- -
- r1.file_format > r2.file_format ? 1 : -1, - )} - /> - - - ))} - +
+ ); +} + +const IndividualExperiments = ({ individual }) => { + const history = useHistory(); + const match = useRouteMatch(); + + const handleExperimentClick = useCallback((eID) => { + if (!eID) { + history.replace(match.url); + return; + } + history.replace(`${match.url}/${eID}`); + }, [history, match]); + + const experimentsNode = ( + + ); + + return ( + + {experimentsNode} + {experimentsNode} + ); }; diff --git a/src/components/explorer/explorer.css b/src/components/explorer/explorer.css index c6e0996ee..668be59c1 100644 --- a/src/components/explorer/explorer.css +++ b/src/components/explorer/explorer.css @@ -46,66 +46,16 @@ padding: 8px 16px !important; } -.ant-descriptions-item-no-label { - display: none; +.ant-table-expanded-row .ant-descriptions-bordered table { + border-collapse: collapse; /* Fixes borders not showing in descriptions nested inside tables */ } - -.ant-descriptions-item-label.ant-descriptions-item-colon { - width: 200px; - padding: 8px 16px !important; -} - -/* selector for nested descriptions */ -.ant-descriptions-view .ant-descriptions-view { - border: 0; - box-shadow: 0 0 0 1px #e8e8e8; -} - .ant-table-expanded-row .ant-descriptions-item-content { - background-color: white; -} - -.ant-descriptions-view .ant-descriptions-view .ant-descriptions-item-content { - padding: 8px 16px; - border-spacing: 0; - border: 0; - border-top: 0; - border-bottom: 0; -} - -.experiment_summary { - display: flex; - margin-bottom: 25px; - gap: 12px; -} - -/* box shadow instead of borders for automatic border collapse on adjacent items */ -.experiment_summary > :last-child > * { - border: 0; - box-shadow: 0 0 0 1px #e8e8e8; + background-color: white; /* Fixes no background on item content for descriptions nested inside tables */ } -.experiment_summary > :first-child > * { - border: 0; - box-shadow: 0 0 0 1px #e8e8e8; -} - -.experiment-titles { - padding: 0 0 8px 17px; -} - -.other-details .ant-descriptions-item-label { - padding: 0 10px !important; -} - -.other-details .ant-descriptions-item-content { - padding: 0 10px !important; -} - -.other-details-click { - color: #1990ff; +.experiment_and_results { + margin: 8px 16px; } - -.experiment_and_results .ant-table-content { - display: inline-block; +.experiment_and_results h4:not(:first-of-type) { + margin-top: 16px; } diff --git a/src/components/explorer/searchResultsTables/ExperimentsTable.js b/src/components/explorer/searchResultsTables/ExperimentsTable.js index 28e4e4d05..cffa823b7 100644 --- a/src/components/explorer/searchResultsTables/ExperimentsTable.js +++ b/src/components/explorer/searchResultsTables/ExperimentsTable.js @@ -14,8 +14,7 @@ const ExperimentRender = React.memo(({ experimentId, individual }) => { <> diff --git a/src/propTypes.js b/src/propTypes.js index 4f073fe21..df3b3a49c 100644 --- a/src/propTypes.js +++ b/src/propTypes.js @@ -360,6 +360,8 @@ export const experimentPropTypesShape = PropTypes.shape({ ]), data_output_type: PropTypes.oneOf(["Raw data", "Derived data"]), usage: PropTypes.string, + creation_date: PropTypes.string, + created_by: PropTypes.string, })), qc_flags: PropTypes.arrayOf(PropTypes.string), From 54b96027e08ace2b13939c562bca4f2fb091c484 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Wed, 20 Sep 2023 11:18:53 -0400 Subject: [PATCH 62/79] lint: prop types --- .../explorer/IndividualExperiments.js | 18 ++-- src/propTypes.js | 84 ++++++++++--------- 2 files changed, 54 insertions(+), 48 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index ac3508954..0a799c0b7 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -6,7 +6,7 @@ import PropTypes from "prop-types"; import { Button, Descriptions, Popover, Table, Typography } from "antd"; import { EM_DASH } from "../../constants"; -import { experimentPropTypesShape, individualPropTypesShape } from "../../propTypes"; +import { experimentPropTypesShape, experimentResultPropTypesShape, individualPropTypesShape } from "../../propTypes"; import { getFileDownloadUrlsFromDrs } from "../../modules/drs/actions"; import { guessFileType } from "../../utils/guessFileType"; @@ -16,10 +16,10 @@ import JsonView from "./JsonView"; import OntologyTerm from "./OntologyTerm"; import DownloadButton from "../DownloadButton"; -const ExperimentResultDownloadButton = ({ resultFile }) => { +const ExperimentResultDownloadButton = ({ result }) => { const downloadUrls = useSelector((state) => state.drs.downloadUrlsByFilename); - const url = downloadUrls[resultFile.filename]?.url; + const url = downloadUrls[result.filename]?.url; return url ? ( {""} ) : ( @@ -27,9 +27,7 @@ const ExperimentResultDownloadButton = ({ resultFile }) => { ); }; ExperimentResultDownloadButton.propTypes = { - resultFile: PropTypes.shape({ - filename: PropTypes.string, - }), + result: experimentResultPropTypesShape, }; const EXPERIMENT_RESULTS_COLUMNS = [ @@ -53,7 +51,7 @@ const EXPERIMENT_RESULTS_COLUMNS = [ title: "Download", key: "download", align: "center", - render: (_, result) => , + render: (_, result) => , }, { key: "other_details", @@ -286,7 +284,11 @@ const Experiments = ({ individual, handleExperimentClick }) => { rowKey="id" /> ); -} +}; +Experiments.propTypes = { + individual: individualPropTypesShape, + handleExperimentClick: PropTypes.func, +}; const IndividualExperiments = ({ individual }) => { const history = useHistory(); diff --git a/src/propTypes.js b/src/propTypes.js index df3b3a49c..b55eebd5f 100644 --- a/src/propTypes.js +++ b/src/propTypes.js @@ -281,6 +281,16 @@ export const phenotypicFeaturePropTypesShape = PropTypes.shape({ updated: PropTypes.string, // ISO datetime string }); +export const resourcePropTypesShape = PropTypes.shape({ + id: PropTypes.string, + name: PropTypes.string, + namespace_prefix: PropTypes.string, + url: PropTypes.string, + version: PropTypes.string, + iri_prefix: PropTypes.string, + extra_properties: PropTypes.object, +}); + export const phenopacketPropTypesShape = PropTypes.shape({ id: PropTypes.string.isRequired, subject: PropTypes.oneOfType([individualPropTypesShape, PropTypes.string]).isRequired, @@ -291,21 +301,45 @@ export const phenopacketPropTypesShape = PropTypes.shape({ created: PropTypes.string, created_by: PropTypes.string, submitted_by: PropTypes.string, - resources: PropTypes.arrayOf(PropTypes.shape({ - id: PropTypes.string, - name: PropTypes.string, - namespace_prefix: PropTypes.string, - url: PropTypes.string, - version: PropTypes.string, - iri_prefix: PropTypes.string, - extra_properties: PropTypes.object, - })), + resources: PropTypes.arrayOf(resourcePropTypesShape), phenopacket_schema_version: PropTypes.string, }).isRequired, created: PropTypes.string, // ISO datetime string updated: PropTypes.string, // ISO datetime string }); +export const experimentResultPropTypesShape = PropTypes.shape({ + identifier: PropTypes.string, + description: PropTypes.string, + filename: PropTypes.string, + file_format: PropTypes.oneOf([ + "SAM", + "BAM", + "CRAM", + "BAI", + "CRAI", + "VCF", + "BCF", + "GVCF", + "BigWig", + "BigBed", + "FASTA", + "FASTQ", + "TAB", + "SRA", + "SRF", + "SFF", + "GFF", + "TABIX", + "UNKNOWN", + "OTHER", + ]), + data_output_type: PropTypes.oneOf(["Raw data", "Derived data"]), + usage: PropTypes.string, + creation_date: PropTypes.string, + created_by: PropTypes.string, +}); + export const experimentPropTypesShape = PropTypes.shape({ id: PropTypes.string.isRequired, study_type: PropTypes.string, @@ -332,37 +366,7 @@ export const experimentPropTypesShape = PropTypes.shape({ model: PropTypes.string, }), - experiment_results: PropTypes.arrayOf(PropTypes.shape({ - identifier: PropTypes.string, - description: PropTypes.string, - filename: PropTypes.string, - file_format: PropTypes.oneOf([ - "SAM", - "BAM", - "CRAM", - "BAI", - "CRAI", - "VCF", - "BCF", - "GVCF", - "BigWig", - "BigBed", - "FASTA", - "FASTQ", - "TAB", - "SRA", - "SRF", - "SFF", - "GFF", - "TABIX", - "UNKNOWN", - "OTHER", - ]), - data_output_type: PropTypes.oneOf(["Raw data", "Derived data"]), - usage: PropTypes.string, - creation_date: PropTypes.string, - created_by: PropTypes.string, - })), + experiment_results: PropTypes.arrayOf(experimentResultPropTypesShape), qc_flags: PropTypes.arrayOf(PropTypes.string), From 06507f0d4ac433807b77e03c684d7f7513671889 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 22 Sep 2023 10:50:16 -0400 Subject: [PATCH 63/79] chore: add actions for fetching dataset resources from katsu --- src/components/datasets/DatasetDataTypes.js | 10 +- src/components/datasets/DatasetOverview.js | 4 +- .../discovery/DiscoveryQueryBuilder.js | 2 +- .../explorer/ExplorerDatasetSearch.js | 61 ++++++----- src/modules/datasets/actions.js | 21 +++- src/modules/datasets/reducers.js | 101 ++++++++++-------- src/modules/metadata/actions.js | 3 +- src/reducers.js | 3 +- 8 files changed, 121 insertions(+), 84 deletions(-) diff --git a/src/components/datasets/DatasetDataTypes.js b/src/components/datasets/DatasetDataTypes.js index d2ffb3b03..319bd00ce 100644 --- a/src/components/datasets/DatasetDataTypes.js +++ b/src/components/datasets/DatasetDataTypes.js @@ -16,17 +16,15 @@ const DatasetDataTypes = React.memo( ({isPrivate, project, dataset, onDatasetIngest}) => { const dispatch = useDispatch(); const datasetDataTypes = useSelector((state) => Object.values( - state.datasetDataTypes.itemsById[dataset.identifier]?.itemsById ?? {})); - const datasetSummaries = useSelector((state) => state.datasetSummaries.itemsById[dataset.identifier]); + state.datasetDataTypes.itemsByID[dataset.identifier]?.itemsByID ?? {})); + const datasetSummaries = useSelector((state) => state.datasetSummaries.itemsByID[dataset.identifier]); const isFetchingDataset = useSelector( - (state) => state.datasetDataTypes.itemsById[dataset.identifier]?.isFetching); + (state) => state.datasetDataTypes.itemsByID[dataset.identifier]?.isFetching); const [datatypeSummaryVisible, setDatatypeSummaryVisible] = useState(false); const [selectedDataType, setSelectedDataType] = useState(null); - const selectedSummary = (selectedDataType !== null && datasetSummaries) - ? datasetSummaries[selectedDataType.id] - : {}; + const selectedSummary = datasetSummaries?.data?.[selectedDataType?.id] ?? {}; const handleClearDataType = useCallback((dataType) => { genericConfirm({ diff --git a/src/components/datasets/DatasetOverview.js b/src/components/datasets/DatasetOverview.js index 4d60a3f89..aeae16c06 100644 --- a/src/components/datasets/DatasetOverview.js +++ b/src/components/datasets/DatasetOverview.js @@ -9,8 +9,8 @@ import {EM_DASH} from "../../constants"; import { useSelector } from "react-redux"; const DatasetOverview = ({isPrivate, project, dataset}) => { - const datasetsDataTypes = useSelector((state) => state.datasetDataTypes.itemsById); - const dataTypesSummary = Object.values(datasetsDataTypes[dataset.identifier]?.itemsById || {}); + const datasetsDataTypes = useSelector((state) => state.datasetDataTypes.itemsByID); + const dataTypesSummary = Object.values(datasetsDataTypes[dataset.identifier]?.itemsByID || {}); const isFetchingDataset = datasetsDataTypes[dataset.identifier]?.isFetching; // Count data types which actually have data in them for showing in the overview diff --git a/src/components/discovery/DiscoveryQueryBuilder.js b/src/components/discovery/DiscoveryQueryBuilder.js index a0fb07c8b..4322567f2 100644 --- a/src/components/discovery/DiscoveryQueryBuilder.js +++ b/src/components/discovery/DiscoveryQueryBuilder.js @@ -133,7 +133,7 @@ class DiscoveryQueryBuilder extends Component { render() { const { activeDataset, dataTypesByDataset} = this.props; - const dataTypesForActiveDataset = Object.values(dataTypesByDataset.itemsById[activeDataset] || {}) + const dataTypesForActiveDataset = Object.values(dataTypesByDataset.itemsByID[activeDataset] || {}) .filter(dt => typeof dt === "object"); const filteredDataTypes = dataTypesForActiveDataset diff --git a/src/components/explorer/ExplorerDatasetSearch.js b/src/components/explorer/ExplorerDatasetSearch.js index c41072a50..2271c14e8 100644 --- a/src/components/explorer/ExplorerDatasetSearch.js +++ b/src/components/explorer/ExplorerDatasetSearch.js @@ -1,4 +1,4 @@ -import React, { useEffect } from "react"; +import React, {useCallback, useEffect} from "react"; import { useSelector, useDispatch } from "react-redux"; import { useParams } from "react-router-dom"; @@ -22,6 +22,7 @@ import { import IndividualsTable from "./searchResultsTables/IndividualsTable"; import BiosamplesTable from "./searchResultsTables/BiosamplesTable"; import ExperimentsTable from "./searchResultsTables/ExperimentsTable"; +import {fetchDatasetResourcesIfNecessary} from "../../modules/datasets/actions"; const { TabPane } = Tabs; @@ -36,42 +37,45 @@ const hasNonEmptyArrayProperty = (targetObject, propertyKey) => { }; const ExplorerDatasetSearch = () => { - const { dataset } = useParams(); + const { dataset: datasetID } = useParams(); const dispatch = useDispatch(); const datasetsByID = useSelector((state) => state.projects.datasetsByID); - const activeKey = useSelector((state) => state.explorer.activeTabByDatasetID[dataset]) || TAB_KEYS.INDIVIDUAL; - const dataTypeForms = useSelector((state) => state.explorer.dataTypeFormsByDatasetID[dataset] || []); - const fetchingSearch = useSelector((state) => state.explorer.fetchingSearchByDatasetID[dataset] || false); + const activeKey = useSelector((state) => state.explorer.activeTabByDatasetID[datasetID]) || TAB_KEYS.INDIVIDUAL; + const dataTypeForms = useSelector((state) => state.explorer.dataTypeFormsByDatasetID[datasetID] || []); + const fetchingSearch = useSelector((state) => state.explorer.fetchingSearchByDatasetID[datasetID] || false); const fetchingTextSearch = useSelector((state) => state.explorer.fetchingTextSearch || false); - const searchResults = useSelector((state) => state.explorer.searchResultsByDatasetID[dataset] || null); + const searchResults = useSelector((state) => state.explorer.searchResultsByDatasetID[datasetID] || null); console.debug("search results: ", searchResults); - const handleSetSelectedRows = (...args) => dispatch(setSelectedRows(dataset, ...args)); + const handleSetSelectedRows = useCallback( + (...args) => dispatch(setSelectedRows(datasetID, ...args)), + [dispatch, datasetID], + ); useEffect(() => { // Ensure user is at the top of the page after transition window.scrollTo(0, 0); }, []); - const onTabChange = (newActiveKey) => { - dispatch(setActiveTab(dataset, newActiveKey)); + const onTabChange = useCallback((newActiveKey) => { + dispatch(setActiveTab(datasetID, newActiveKey)); handleSetSelectedRows([]); - }; + }, [dispatch, datasetID, handleSetSelectedRows]); - const performSearch = () => { - dispatch(setActiveTab(dataset, TAB_KEYS.INDIVIDUAL)); - dispatch(resetTableSortOrder(dataset)); - dispatch(performSearchIfPossible(dataset)); - }; + const performSearch = useCallback(() => { + dispatch(setActiveTab(datasetID, TAB_KEYS.INDIVIDUAL)); + dispatch(resetTableSortOrder(datasetID)); + dispatch(performSearchIfPossible(datasetID)); + }, [dispatch, datasetID]); - if (!dataset) return null; + useEffect(() => { + dispatch(fetchDatasetResourcesIfNecessary(datasetID)); + }, [dispatch, datasetID]); - const selectedDataset = datasetsByID[dataset]; - - if (!selectedDataset) return null; + const selectedDataset = datasetsByID[datasetID]; const isFetchingSearchResults = fetchingSearch || fetchingTextSearch; @@ -80,19 +84,20 @@ const ExplorerDatasetSearch = () => { const hasBiosamples = hasNonEmptyArrayProperty(searchResults, "searchFormattedResultsBiosamples"); const showTabs = hasResults && (hasExperiments || hasBiosamples); + if (!selectedDataset) return null; return ( <> Explore Dataset {selectedDataset.title} - + dispatch(addDataTypeQueryForm(dataset, form))} - updateDataTypeQueryForm={(index, form) => dispatch(updateDataTypeQueryForm(dataset, index, form))} - removeDataTypeQueryForm={(index) => dispatch(removeDataTypeQueryForm(dataset, index))} + addDataTypeQueryForm={(form) => dispatch(addDataTypeQueryForm(datasetID, form))} + updateDataTypeQueryForm={(index, form) => dispatch(updateDataTypeQueryForm(datasetID, index, form))} + removeDataTypeQueryForm={(index) => dispatch(removeDataTypeQueryForm(datasetID, index))} /> {hasResults && !isFetchingSearchResults && @@ -101,14 +106,14 @@ const ExplorerDatasetSearch = () => { {hasBiosamples && ( )} @@ -116,13 +121,13 @@ const ExplorerDatasetSearch = () => { )} ) : ( - + ))} ); diff --git a/src/modules/datasets/actions.js b/src/modules/datasets/actions.js index 21b36ef11..b6ecc6d68 100644 --- a/src/modules/datasets/actions.js +++ b/src/modules/datasets/actions.js @@ -3,7 +3,11 @@ import {getDataServices} from "../services/utils"; export const FETCHING_DATASETS_DATA_TYPES = createFlowActionTypes("FETCHING_DATASETS_DATA_TYPES"); export const FETCH_DATASET_DATA_TYPES_SUMMARY = createNetworkActionTypes("FETCH_DATASET_DATA_TYPES_SUMMARY"); + export const FETCH_DATASET_SUMMARY = createNetworkActionTypes("FETCH_DATASET_SUMMARY"); +export const FETCHING_ALL_DATASET_SUMMARIES = createFlowActionTypes("FETCHING_ALL_DATASET_SUMMARIES"); + +export const FETCH_DATASET_RESOURCES = createNetworkActionTypes("FETCH_DATASET_RESOURCES"); const fetchDatasetDataTypesSummary = networkAction((serviceInfo, datasetID) => ({ types: FETCH_DATASET_DATA_TYPES_SUMMARY, @@ -12,7 +16,7 @@ const fetchDatasetDataTypesSummary = networkAction((serviceInfo, datasetID) => ( })); export const fetchDatasetDataTypesSummariesIfPossible = (datasetID) => async (dispatch, getState) => { - if (getState().datasetDataTypes.itemsById?.[datasetID]?.isFetching) return; + if (getState().datasetDataTypes.itemsByID?.[datasetID]?.isFetching) return; await Promise.all( getDataServices(getState()).map(serviceInfo => dispatch(fetchDatasetDataTypesSummary(serviceInfo, datasetID))), ); @@ -34,8 +38,21 @@ const fetchDatasetSummary = networkAction((serviceInfo, datasetID) => ({ })); export const fetchDatasetSummariesIfPossible = (datasetID) => async (dispatch, getState) => { - if (getState().datasetSummaries.isFetching) return; + if (getState().datasetSummaries.isFetchingAll) return; + dispatch(beginFlow(FETCHING_ALL_DATASET_SUMMARIES)); await Promise.all( getDataServices(getState()).map(serviceInfo => dispatch(fetchDatasetSummary(serviceInfo, datasetID))), ); + dispatch(endFlow(FETCHING_ALL_DATASET_SUMMARIES)); +}; + +const fetchDatasetResources = networkAction((datasetID) => (dispatch, getState) => ({ + types: FETCH_DATASET_RESOURCES, + params: {datasetID}, + url: `${getState().services.metadataService.url}/api/datasets/${datasetID}/resources`, + err: "Error fetching dataset resources", +})); +export const fetchDatasetResourcesIfNecessary = (datasetID) => (dispatch, getState) => { + if (getState().datasetResources.itemsByID[datasetID]?.isFetching) return; + return dispatch(fetchDatasetResources(datasetID)); }; diff --git a/src/modules/datasets/reducers.js b/src/modules/datasets/reducers.js index e2af914ed..f0e846a71 100644 --- a/src/modules/datasets/reducers.js +++ b/src/modules/datasets/reducers.js @@ -1,8 +1,13 @@ -import {FETCHING_DATASETS_DATA_TYPES, FETCH_DATASET_DATA_TYPES_SUMMARY, FETCH_DATASET_SUMMARY} from "./actions"; +import { + FETCHING_DATASETS_DATA_TYPES, + FETCH_DATASET_DATA_TYPES_SUMMARY, + FETCH_DATASET_SUMMARY, + FETCHING_ALL_DATASET_SUMMARIES, FETCH_DATASET_RESOURCES, +} from "./actions"; export const datasetDataTypes = ( state = { - itemsById: {}, + itemsByID: {}, isFetchingAll: false, }, action, @@ -16,10 +21,10 @@ export const datasetDataTypes = ( const {datasetID} = action; return { ...state, - itemsById: { - ...state.itemsById, + itemsByID: { + ...state.itemsByID, [datasetID]: { - itemsById: state.itemsById[datasetID]?.itemsById ?? {}, + itemsByID: state.itemsByID[datasetID]?.itemsByID ?? {}, isFetching: true, }, }, @@ -30,11 +35,11 @@ export const datasetDataTypes = ( const itemsByID = Object.fromEntries(action.data.map(d => [d.id, d])); return { ...state, - itemsById: { - ...state.itemsById, + itemsByID: { + ...state.itemsByID, [datasetID]: { - itemsById: { - ...state.itemsById[datasetID].itemsById, + itemsByID: { + ...state.itemsByID[datasetID].itemsByID, ...itemsByID, }, }, @@ -46,10 +51,10 @@ export const datasetDataTypes = ( const {datasetID} = action; return { ...state, - itemsById: { - ...state.itemsById, + itemsByID: { + ...state.itemsByID, [datasetID]: { - ...state.itemsById[datasetID], + ...state.itemsByID[datasetID], isFetching: false, }, }, @@ -61,43 +66,55 @@ export const datasetDataTypes = ( }; +const datasetItemSet = (oldState, datasetID, key, value) => ({ + ...oldState, + itemsByID: { + ...oldState.itemsByID, + [datasetID]: { + ...(oldState.itemsByID[datasetID] ?? {}), + [key]: value, + }, + }, +}); + + export const datasetSummaries = ( state = { - itemsById: {}, + isFetchingAll: false, + itemsByID: {}, }, action, ) => { switch (action.type) { - case FETCH_DATASET_SUMMARY.REQUEST:{ - const {datasetID} = action; - return { - ...state, - itemsById: { - ...state.itemsById, - [datasetID]: { - ...(state.itemsById[datasetID] ?? {}), - }, - }, - }; - } - case FETCH_DATASET_SUMMARY.RECEIVE:{ - const {datasetID} = action; - return { - ...state, - itemsById: { - ...state.itemsById, - [datasetID]: { - ...state.itemsById[datasetID], - ...action.data, - }, - }, - }; - } + case FETCH_DATASET_SUMMARY.REQUEST: + return datasetItemSet(state, action.datasetID, "isFetching", true); + case FETCH_DATASET_SUMMARY.RECEIVE: + return datasetItemSet(state, action.datasetID, "data", action.data); case FETCH_DATASET_SUMMARY.FINISH: - case FETCH_DATASET_SUMMARY.ERROR: - return { - ...state, - }; + return datasetItemSet(state, action.datasetID, "isFetching", false); + case FETCHING_ALL_DATASET_SUMMARIES.BEGIN: + return {...state, isFetchingAll: true}; + case FETCHING_ALL_DATASET_SUMMARIES.END: + case FETCHING_ALL_DATASET_SUMMARIES.TERMINATE: + return {...state, isFetchingAll: false}; + default: + return state; + } +}; + +export const datasetResources = ( + state = { + itemsByID: {}, + }, + action, +) => { + switch (action.type) { + case FETCH_DATASET_RESOURCES.REQUEST: + return datasetItemSet(state, action.datasetID, "isFetching", true); + case FETCH_DATASET_RESOURCES.RECEIVE: + return datasetItemSet(state, action.datasetID, "data", action.data); + case FETCH_DATASET_RESOURCES.FINISH: + return datasetItemSet(state, action.datasetID, "isFetching", false); default: return state; } diff --git a/src/modules/metadata/actions.js b/src/modules/metadata/actions.js index deb1c3317..2b1bd3ec0 100644 --- a/src/modules/metadata/actions.js +++ b/src/modules/metadata/actions.js @@ -141,7 +141,7 @@ export const deleteProjectIfPossible = project => async (dispatch, getState) => export const clearDatasetDataTypes = datasetId => async (dispatch, getState) => { // only clear data types which can yield counts - `queryable` is a proxy for this - const dataTypes = Object.values(getState().datasetDataTypes.itemsById[datasetId].itemsById) + const dataTypes = Object.values(getState().datasetDataTypes.itemsByID[datasetId].itemsByID) .filter(dt => dt.queryable); return await Promise.all(dataTypes.map(dt => dispatch(clearDatasetDataType(datasetId, dt.id)))); }; @@ -307,4 +307,3 @@ export const fetchOverviewSummary = networkAction(() => (dispatch, getState) => url: `${getState().services.metadataService.url}/api/overview`, err: "Error fetching overview summary metadata", })); - diff --git a/src/reducers.js b/src/reducers.js index c5a25689b..797dfa228 100644 --- a/src/reducers.js +++ b/src/reducers.js @@ -20,7 +20,7 @@ import { serviceDataTypes, serviceWorkflows, } from "./modules/services/reducers"; -import {datasetDataTypes, datasetSummaries} from "./modules/datasets/reducers"; +import {datasetDataTypes, datasetResources, datasetSummaries} from "./modules/datasets/reducers"; import {runs} from "./modules/wes/reducers"; const rootReducer = combineReducers({ @@ -60,6 +60,7 @@ const rootReducer = combineReducers({ // Dataset module datasetDataTypes, datasetSummaries, + datasetResources, // WES module runs, From 87f4e7ba5bb2363998c2baaa4a018f436c0292c7 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 22 Sep 2023 11:09:36 -0400 Subject: [PATCH 64/79] refact(explorer): rewrite explorer individual content comp as func --- .../explorer/ExplorerIndividualContent.js | 252 ++++++++---------- 1 file changed, 111 insertions(+), 141 deletions(-) diff --git a/src/components/explorer/ExplorerIndividualContent.js b/src/components/explorer/ExplorerIndividualContent.js index bb38ec37f..c39bcb55e 100644 --- a/src/components/explorer/ExplorerIndividualContent.js +++ b/src/components/explorer/ExplorerIndividualContent.js @@ -1,14 +1,11 @@ -import React, {Component} from "react"; -import {connect} from "react-redux"; -import {Redirect, Route, Switch} from "react-router-dom"; +import React, { useEffect } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { Redirect, Route, Switch, useHistory, useLocation, useParams, useRouteMatch } from "react-router-dom"; -import PropTypes from "prop-types"; -import ReactRouterPropTypes from "react-router-prop-types"; - -import {Layout, Menu, Skeleton} from "antd"; +import { Layout, Menu, Skeleton } from "antd"; +import { fetchDatasetResourcesIfNecessary } from "../../modules/datasets/actions"; import { fetchIndividualIfNecessary } from "../../modules/metadata/actions"; -import { individualPropTypesShape } from "../../propTypes"; import { LAYOUT_CONTENT_STYLE } from "../../styles/layoutContent"; import { matchingMenuKeys, renderMenuItem } from "../../utils/menu"; import { urlPath } from "../../utils/url"; @@ -33,147 +30,120 @@ const MENU_STYLE = { marginTop: "-12px", }; +const headerTitle = (individual) => { + if (!individual) { + return null; + } + const mainId = individual.id; + const alternateIds = individual.alternate_ids ?? []; + return alternateIds.length ? `${mainId} (${alternateIds.join(", ")})` : mainId; +}; -class ExplorerIndividualContent extends Component { - constructor(props) { - super(props); +const ExplorerIndividualContent = () => { + const dispatch = useDispatch(); - this.fetchIndividualData = this.fetchIndividualData.bind(this); + const location = useLocation(); + const history = useHistory(); + const { individual: individualID } = useParams(); + const { url: individualUrl } = useRouteMatch(); - this.state = { - backUrl: null, - }; - } + const backUrl = location.state?.backUrl; - fetchIndividualData() { - const individualID = this.props.match.params.individual || null; - if (!individualID || !this.props.metadataService) return; - this.props.fetchIndividualIfNecessary(individualID); - } + const metadataService = useSelector((state) => state.services.metadataService); + const individuals = useSelector((state) => state.individuals.itemsByID); - // noinspection JSCheckFunctionSignatures - componentDidUpdate(prevProps) { - if (!prevProps.metadataService && this.props.metadataService) { - // We loaded metadata service, so we can load individual data now - this.fetchIndividualData(); + useEffect(() => { + if (metadataService && individualID) { + // If we've loaded the metadata service, and we have an individual selected (or the individual ID changed), + // we should load individual data. + dispatch(fetchIndividualIfNecessary(individualID)); } - } + }, [dispatch, metadataService, individualID]); - componentDidMount() { - const { location } = this.props; - const { backUrl } = location.state || {}; - if (backUrl) this.setState({ backUrl }); - this.fetchIndividualData(); - } + const { isFetching: individualIsFetching, data: individual } = individuals[individualID] ?? {}; - render() { - // TODO: Disease content - highlight what was found in search results? - - const individualID = this.props.match.params.individual || null; - const individualInfo = this.props.individuals[individualID] || {}; - const individual = individualInfo.data; - - const individualUrl = this.props.match.url; - - const overviewUrl = `${individualUrl}/overview`; - const phenotypicFeaturesUrl = `${individualUrl}/phenotypic-features`; - const biosamplesUrl = `${individualUrl}/biosamples`; - const experimentsUrl = `${individualUrl}/experiments`; - const variantsUrl = `${individualUrl}/variants`; - const genesUrl = `${individualUrl}/genes`; - const diseasesUrl = `${individualUrl}/diseases`; - const metadataUrl = `${individualUrl}/metadata`; - const tracksUrl = `${individualUrl}/tracks`; - const phenopacketsUrl = `${individualUrl}/phenopackets`; - - const individualMenu = [ - {url: overviewUrl, style: {marginLeft: "4px"}, text: "Overview"}, - {url: phenotypicFeaturesUrl, text: "Phenotypic Features"}, - {url: biosamplesUrl, text: "Biosamples"}, - {url: experimentsUrl, text: "Experiments"}, - {url: tracksUrl, text: "Tracks"}, - {url: variantsUrl, text: "Variants"}, - {url: genesUrl, text: "Genes"}, - {url: diseasesUrl, text: "Diseases"}, - {url: metadataUrl, text: "Metadata"}, - {url: phenopacketsUrl, text: "Phenopackets JSON"}, - ]; - - const selectedKeys = matchingMenuKeys(individualMenu, urlPath(BENTO_URL)); - - const headerTitle = (individual) => { - if (!individual) { - return null; + useEffect(() => { + // TODO: when individual belongs to a single dataset, use that instead + if (individual) { + individual.phenopackets.map((p) => dispatch(fetchDatasetResourcesIfNecessary(p.dataset))); + } + }, [dispatch, individual]); + + const overviewUrl = `${individualUrl}/overview`; + const phenotypicFeaturesUrl = `${individualUrl}/phenotypic-features`; + const biosamplesUrl = `${individualUrl}/biosamples`; + const experimentsUrl = `${individualUrl}/experiments`; + const variantsUrl = `${individualUrl}/variants`; + const genesUrl = `${individualUrl}/genes`; + const diseasesUrl = `${individualUrl}/diseases`; + const metadataUrl = `${individualUrl}/metadata`; + const tracksUrl = `${individualUrl}/tracks`; + const phenopacketsUrl = `${individualUrl}/phenopackets`; + + const individualMenu = [ + {url: overviewUrl, style: {marginLeft: "4px"}, text: "Overview"}, + {url: phenotypicFeaturesUrl, text: "Phenotypic Features"}, + {url: biosamplesUrl, text: "Biosamples"}, + {url: experimentsUrl, text: "Experiments"}, + {url: tracksUrl, text: "Tracks"}, + {url: variantsUrl, text: "Variants"}, + {url: genesUrl, text: "Genes"}, + {url: diseasesUrl, text: "Diseases"}, + {url: metadataUrl, text: "Metadata"}, + {url: phenopacketsUrl, text: "Phenopackets JSON"}, + ]; + + const selectedKeys = matchingMenuKeys(individualMenu, urlPath(BENTO_URL)); + + return <> + history.push(backUrl)) : undefined} + footer={ + + {individualMenu.map(renderMenuItem)} + } - const mainId = individual.id; - const alternateIds = individual.alternate_ids ?? []; - return alternateIds.length ? `${mainId} (${alternateIds.join(", ")})` : mainId; - }; - - return <> - this.props.history.push(this.state.backUrl)) : undefined} - footer={ - - {individualMenu.map(renderMenuItem)} - - } /> - - - {(individual && !individualInfo.isFetching) ? - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - : } - - - ; - } -} - -ExplorerIndividualContent.propTypes = { - metadataService: PropTypes.object, // TODO - individuals: PropTypes.objectOf(individualPropTypesShape), - - fetchIndividualIfNecessary: PropTypes.func, - - location: ReactRouterPropTypes.location.isRequired, - match: ReactRouterPropTypes.match.isRequired, + /> + + + {(individual && !individualIsFetching) ? + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + : } + + + ; }; -const mapStateToProps = state => ({ - metadataService: state.services.metadataService, - individuals: state.individuals.itemsByID, -}); - -export default connect(mapStateToProps, {fetchIndividualIfNecessary})(ExplorerIndividualContent); +export default ExplorerIndividualContent; From 429bf04e23f9d1926105a7bb2d60a60b839ba9f8 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 22 Sep 2023 11:34:15 -0400 Subject: [PATCH 65/79] chore(explorer): rename metadata tab to ontologies --- src/components/explorer/ExplorerIndividualContent.js | 10 +++++----- .../{IndividualMetadata.js => IndividualOntologies.js} | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) rename src/components/explorer/{IndividualMetadata.js => IndividualOntologies.js} (93%) diff --git a/src/components/explorer/ExplorerIndividualContent.js b/src/components/explorer/ExplorerIndividualContent.js index c39bcb55e..8dc1077e5 100644 --- a/src/components/explorer/ExplorerIndividualContent.js +++ b/src/components/explorer/ExplorerIndividualContent.js @@ -16,7 +16,7 @@ import IndividualPhenotypicFeatures from "./IndividualPhenotypicFeatures"; import IndividualBiosamples from "./IndividualBiosamples"; import IndividualExperiments from "./IndividualExperiments"; import IndividualDiseases from "./IndividualDiseases"; -import IndividualMetadata from "./IndividualMetadata"; +import IndividualOntologies from "./IndividualOntologies"; import IndividualVariants from "./IndividualVariants"; import IndividualGenes from "./IndividualGenes"; import IndividualTracks from "./IndividualTracks"; @@ -76,7 +76,7 @@ const ExplorerIndividualContent = () => { const variantsUrl = `${individualUrl}/variants`; const genesUrl = `${individualUrl}/genes`; const diseasesUrl = `${individualUrl}/diseases`; - const metadataUrl = `${individualUrl}/metadata`; + const ontologiesUrl = `${individualUrl}/ontologies`; const tracksUrl = `${individualUrl}/tracks`; const phenopacketsUrl = `${individualUrl}/phenopackets`; @@ -89,7 +89,7 @@ const ExplorerIndividualContent = () => { {url: variantsUrl, text: "Variants"}, {url: genesUrl, text: "Genes"}, {url: diseasesUrl, text: "Diseases"}, - {url: metadataUrl, text: "Metadata"}, + {url: ontologiesUrl, text: "Ontologies"}, {url: phenopacketsUrl, text: "Phenopackets JSON"}, ]; @@ -133,8 +133,8 @@ const ExplorerIndividualContent = () => { - - + + diff --git a/src/components/explorer/IndividualMetadata.js b/src/components/explorer/IndividualOntologies.js similarity index 93% rename from src/components/explorer/IndividualMetadata.js rename to src/components/explorer/IndividualOntologies.js index 30bfbd966..ffc61ef18 100644 --- a/src/components/explorer/IndividualMetadata.js +++ b/src/components/explorer/IndividualOntologies.js @@ -48,7 +48,7 @@ const METADATA_COLUMNS = [ }, ]; -const IndividualMetadata = ({individual}) => { +const IndividualOntologies = ({individual}) => { const resources = useResources(individual); return ( @@ -63,8 +63,8 @@ const IndividualMetadata = ({individual}) => { ); }; -IndividualMetadata.propTypes = { +IndividualOntologies.propTypes = { individual: individualPropTypesShape, }; -export default IndividualMetadata; +export default IndividualOntologies; From bb75f67a1feefb8055fc8c5262c112fa26c102af Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 22 Sep 2023 11:34:27 -0400 Subject: [PATCH 66/79] style(explorer): fix individual variants tab styling --- src/components/explorer/explorer.css | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/explorer/explorer.css b/src/components/explorer/explorer.css index 668be59c1..012096070 100644 --- a/src/components/explorer/explorer.css +++ b/src/components/explorer/explorer.css @@ -19,8 +19,9 @@ /* Variants tab */ -.variantDescriptions .ant-descriptions-item-label{ +.variantDescriptions .ant-descriptions-item-label { vertical-align: top; + width: 200px; } /* Biosamples and Experiments tabs */ From f6a22eff4f76ab1e1f9a1b0f70677f7533115523 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 22 Sep 2023 12:19:15 -0400 Subject: [PATCH 67/79] chore(explorer): use all dataset resources in useResources hook --- .../explorer/ExplorerIndividualContent.js | 13 +++------ src/components/explorer/utils.js | 28 +++++++++++++++---- src/modules/datasets/actions.js | 3 +- 3 files changed, 28 insertions(+), 16 deletions(-) diff --git a/src/components/explorer/ExplorerIndividualContent.js b/src/components/explorer/ExplorerIndividualContent.js index 8dc1077e5..6b15440fa 100644 --- a/src/components/explorer/ExplorerIndividualContent.js +++ b/src/components/explorer/ExplorerIndividualContent.js @@ -4,11 +4,12 @@ import { Redirect, Route, Switch, useHistory, useLocation, useParams, useRouteMa import { Layout, Menu, Skeleton } from "antd"; -import { fetchDatasetResourcesIfNecessary } from "../../modules/datasets/actions"; +import { BENTO_URL } from "../../config"; import { fetchIndividualIfNecessary } from "../../modules/metadata/actions"; import { LAYOUT_CONTENT_STYLE } from "../../styles/layoutContent"; import { matchingMenuKeys, renderMenuItem } from "../../utils/menu"; import { urlPath } from "../../utils/url"; +import { useResources } from "./utils"; import SitePageHeader from "../SitePageHeader"; import IndividualOverview from "./IndividualOverview"; @@ -22,8 +23,6 @@ import IndividualGenes from "./IndividualGenes"; import IndividualTracks from "./IndividualTracks"; import IndividualPhenopackets from "./IndividualPhenopackets"; -import { BENTO_URL } from "../../config"; - const MENU_STYLE = { marginLeft: "-24px", marginRight: "-24px", @@ -62,12 +61,8 @@ const ExplorerIndividualContent = () => { const { isFetching: individualIsFetching, data: individual } = individuals[individualID] ?? {}; - useEffect(() => { - // TODO: when individual belongs to a single dataset, use that instead - if (individual) { - individual.phenopackets.map((p) => dispatch(fetchDatasetResourcesIfNecessary(p.dataset))); - } - }, [dispatch, individual]); + // Trigger resource loading + useResources(individual); const overviewUrl = `${individualUrl}/overview`; const phenotypicFeaturesUrl = `${individualUrl}/phenotypic-features`; diff --git a/src/components/explorer/utils.js b/src/components/explorer/utils.js index 4b3df37cf..d82b0831f 100644 --- a/src/components/explorer/utils.js +++ b/src/components/explorer/utils.js @@ -1,4 +1,6 @@ -import {useMemo} from "react"; +import { useEffect, useMemo } from "react"; +import { useDispatch, useSelector } from "react-redux"; +import { fetchDatasetResourcesIfNecessary } from "../../modules/datasets/actions"; export const useDeduplicatedIndividualBiosamples = (individual) => useMemo( @@ -13,18 +15,32 @@ export const useDeduplicatedIndividualBiosamples = (individual) => ); -export const useResources = (individual) => - useMemo( +export const useResources = (individual) => { + const dispatch = useDispatch(); + + // TODO: when individual belongs to a single dataset, use that instead + const individualDatasets = useMemo( + () => (individual?.phenopackets ?? []).map(p => p.dataset), + [individual]); + + const datasetResources = useSelector((state) => state.datasetResources.itemsByID); + + useEffect(() => { + individualDatasets.map((d) => dispatch(fetchDatasetResourcesIfNecessary(d))); + }, [dispatch, individualDatasets]); + + return useMemo( () => Object.values( Object.fromEntries( - (individual?.phenopackets ?? []) - .flatMap(p => p.meta_data.resources ?? []) + individualDatasets + .flatMap(d => datasetResources[d]?.data ?? []) .map(r => [r.id, r]), ), ), - [individual], + [datasetResources, individualDatasets], ); +}; export const useResourcesByNamespacePrefix = (individual) => { diff --git a/src/modules/datasets/actions.js b/src/modules/datasets/actions.js index b6ecc6d68..e5c7764d1 100644 --- a/src/modules/datasets/actions.js +++ b/src/modules/datasets/actions.js @@ -53,6 +53,7 @@ const fetchDatasetResources = networkAction((datasetID) => (dispatch, getState) err: "Error fetching dataset resources", })); export const fetchDatasetResourcesIfNecessary = (datasetID) => (dispatch, getState) => { - if (getState().datasetResources.itemsByID[datasetID]?.isFetching) return; + const datasetResources = getState().datasetResources.itemsByID[datasetID]; + if (datasetResources?.isFetching || datasetResources?.data) return; return dispatch(fetchDatasetResources(datasetID)); }; From ee30950c5985965794150584557ae2a542ff7c38 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 22 Sep 2023 13:59:51 -0400 Subject: [PATCH 68/79] feat(explorer): use dataset resources rather than phenopackets This will link more resources, since before any additional resources on the dataset were ignored. This now also means biosample ontology terms in the search results table are linked up. Failed links are now shown as a disabled link icon with an error-state cursor. --- .../explorer/ExplorerIndividualContent.js | 4 +- .../explorer/IndividualBiosamples.js | 23 ++--- src/components/explorer/IndividualDiseases.js | 15 ++-- .../explorer/IndividualExperiments.js | 25 +++--- .../explorer/IndividualOntologies.js | 9 +- src/components/explorer/IndividualOverview.js | 49 ++++++----- .../explorer/IndividualPhenotypicFeatures.js | 7 +- src/components/explorer/OntologyTerm.js | 28 +++--- src/components/explorer/explorer.css | 8 -- .../searchResultsTables/BiosamplesTable.js | 87 ++++++++++--------- src/components/explorer/utils.js | 47 ++++++---- 11 files changed, 166 insertions(+), 136 deletions(-) diff --git a/src/components/explorer/ExplorerIndividualContent.js b/src/components/explorer/ExplorerIndividualContent.js index 6b15440fa..2011f45b7 100644 --- a/src/components/explorer/ExplorerIndividualContent.js +++ b/src/components/explorer/ExplorerIndividualContent.js @@ -9,7 +9,7 @@ import { fetchIndividualIfNecessary } from "../../modules/metadata/actions"; import { LAYOUT_CONTENT_STYLE } from "../../styles/layoutContent"; import { matchingMenuKeys, renderMenuItem } from "../../utils/menu"; import { urlPath } from "../../utils/url"; -import { useResources } from "./utils"; +import { useIndividualResources } from "./utils"; import SitePageHeader from "../SitePageHeader"; import IndividualOverview from "./IndividualOverview"; @@ -62,7 +62,7 @@ const ExplorerIndividualContent = () => { const { isFetching: individualIsFetching, data: individual } = individuals[individualID] ?? {}; // Trigger resource loading - useResources(individual); + useIndividualResources(individual); const overviewUrl = `${individualUrl}/overview`; const phenotypicFeaturesUrl = `${individualUrl}/phenotypic-features`; diff --git a/src/components/explorer/IndividualBiosamples.js b/src/components/explorer/IndividualBiosamples.js index fb5728d3f..41c3e1d7d 100644 --- a/src/components/explorer/IndividualBiosamples.js +++ b/src/components/explorer/IndividualBiosamples.js @@ -5,7 +5,7 @@ import { Route, Switch, useHistory, useRouteMatch, useParams } from "react-route import { Button, Descriptions, Table } from "antd"; import { EM_DASH } from "../../constants"; -import { useDeduplicatedIndividualBiosamples } from "./utils"; +import { useDeduplicatedIndividualBiosamples, useIndividualResources } from "./utils"; import { biosamplePropTypesShape, experimentPropTypesShape, @@ -21,18 +21,19 @@ import "./explorer.css"; // TODO: Only show biosamples from the relevant dataset, if specified; // highlight those found in search results, if specified -const BiosampleProcedure = ({ individual, procedure }) => ( +const BiosampleProcedure = ({ resourcesTuple, procedure }) => (
- Code:{" "} + Code:{" "} {procedure.body_site ? (
- Body Site:{" "} + Body Site:{" "} +
) : null}
); BiosampleProcedure.propTypes = { - individual: individualPropTypesShape.isRequired, + resourcesTuple: PropTypes.array, procedure: PropTypes.shape({ code: ontologyShape.isRequired, body_site: ontologyShape, @@ -60,19 +61,20 @@ ExperimentsClickList.propTypes = { }; const BiosampleDetail = ({ individual, biosample, handleExperimentClick }) => { + const resourcesTuple = useIndividualResources(individual); return ( {biosample.id} - + - + - + {biosample.individual_age_at_collection @@ -125,6 +127,7 @@ const Biosamples = ({ individual, handleBiosampleClick, handleExperimentClick }) }, []); const biosamples = useDeduplicatedIndividualBiosamples(individual); + const resourcesTuple = useIndividualResources(individual); const columns = useMemo( () => [ @@ -136,7 +139,7 @@ const Biosamples = ({ individual, handleBiosampleClick, handleExperimentClick }) { title: "Sampled Tissue", dataIndex: "sampled_tissue", - render: tissue => , + render: tissue => , }, { title: "Experiments", @@ -146,7 +149,7 @@ const Biosamples = ({ individual, handleBiosampleClick, handleExperimentClick }) ), }, ], - [individual, handleExperimentClick], + [resourcesTuple, handleExperimentClick], ); const onExpand = useCallback( diff --git a/src/components/explorer/IndividualDiseases.js b/src/components/explorer/IndividualDiseases.js index 950b2ac12..fdec24f4c 100644 --- a/src/components/explorer/IndividualDiseases.js +++ b/src/components/explorer/IndividualDiseases.js @@ -4,7 +4,7 @@ import { Table } from "antd"; import { individualPropTypesShape } from "../../propTypes"; import { EM_DASH } from "../../constants"; -import { ontologyTermSorter } from "./utils"; +import { ontologyTermSorter, useIndividualResources } from "./utils"; import OntologyTerm from "./OntologyTerm"; @@ -13,6 +13,7 @@ import OntologyTerm from "./OntologyTerm"; const IndividualDiseases = ({ individual }) => { const diseases = individual.phenopackets.flatMap(p => p.diseases); + const resourcesTuple = useIndividualResources(individual); const columns = useMemo(() => [ { @@ -24,7 +25,7 @@ const IndividualDiseases = ({ individual }) => { { title: "term", dataIndex: "term", - render: (term) => , + render: (term) => , sorter: ontologyTermSorter("term"), }, { @@ -41,21 +42,21 @@ const IndividualDiseases = ({ individual }) => { ?
{disease.onset.start.age} - {disease.onset.end.age}
// Onset age label only : disease.onset.label - ? + ? : EM_DASH : EM_DASH, }, { title: "Extra Properties", key: "extra_properties", - render: (_, individual) => - (Object.keys(individual.extra_properties ?? {}).length) + render: (_, disease) => + (Object.keys(disease.extra_properties ?? {}).length) ?
-
{JSON.stringify(individual.extra_properties, null, 2)}
+
{JSON.stringify(disease.extra_properties, null, 2)}
: EM_DASH, }, - ], [individual]); + ], [resourcesTuple]); return (
{ +const ExperimentDetail = ({ experiment, resourcesTuple }) => { const { id, experiment_type: experimentType, @@ -140,7 +140,10 @@ const ExperimentDetail = ({ individual, experiment }) => { experiment_ontology is accidentally an array in Katsu, so this takes the first item and falls back to just the field (if we fix this in the future) */} - + {molecule} @@ -150,7 +153,7 @@ const ExperimentDetail = ({ individual, experiment }) => { molecule_ontology is accidentally an array in Katsu, so this takes the first item and falls back to just the field (if we fix this in the future) */} - + {studyType} {extractionProtocol} @@ -187,8 +190,8 @@ const ExperimentDetail = ({ individual, experiment }) => { ); }; ExperimentDetail.propTypes = { - individual: individualPropTypesShape, experiment: experimentPropTypesShape, + resourcesTuple: PropTypes.array, }; const Experiments = ({ individual, handleExperimentClick }) => { @@ -218,6 +221,7 @@ const Experiments = ({ individual, handleExperimentClick }) => { () => biosamplesData.flatMap((b) => b?.experiments ?? []), [biosamplesData], ); + const resourcesTuple = useIndividualResources(individual); useEffect(() => { // retrieve any download urls if experiments data changes @@ -243,7 +247,7 @@ const Experiments = ({ individual, handleExperimentClick }) => { { title: "Molecule", dataIndex: "molecule_ontology", - render: (mo) => , + render: (mo) => , }, { title: "Experiment Results", @@ -251,7 +255,7 @@ const Experiments = ({ individual, handleExperimentClick }) => { render: (exp) => {exp.experiment_results.length ?? 0} files, }, ], - [individual, handleExperimentClick], + [resourcesTuple, handleExperimentClick], ); const onExpand = useCallback( @@ -263,12 +267,9 @@ const Experiments = ({ individual, handleExperimentClick }) => { const expandedRowRender = useCallback( (experiment) => ( - + ), - [handleExperimentClick], + [resourcesTuple], ); return ( diff --git a/src/components/explorer/IndividualOntologies.js b/src/components/explorer/IndividualOntologies.js index ffc61ef18..0c24629b0 100644 --- a/src/components/explorer/IndividualOntologies.js +++ b/src/components/explorer/IndividualOntologies.js @@ -1,9 +1,9 @@ import React from "react"; -import {Table} from "antd"; +import { Table } from "antd"; -import {individualPropTypesShape} from "../../propTypes"; -import {useResources} from "./utils"; +import { individualPropTypesShape } from "../../propTypes"; +import { useIndividualResources } from "./utils"; // TODO: Only show diseases from the relevant dataset, if specified; @@ -49,7 +49,7 @@ const METADATA_COLUMNS = [ ]; const IndividualOntologies = ({individual}) => { - const resources = useResources(individual); + const [resources, isFetching] = useIndividualResources(individual); return (
{ columns={METADATA_COLUMNS} rowKey="id" dataSource={resources} + loading={isFetching} /> ); }; diff --git a/src/components/explorer/IndividualOverview.js b/src/components/explorer/IndividualOverview.js index 804cd0423..d65005510 100644 --- a/src/components/explorer/IndividualOverview.js +++ b/src/components/explorer/IndividualOverview.js @@ -5,25 +5,30 @@ import { Descriptions } from "antd"; import { EM_DASH } from "../../constants"; import { individualPropTypesShape } from "../../propTypes"; +import { useIndividualResources } from "./utils"; import OntologyTerm from "./OntologyTerm"; -const IndividualOverview = ({individual}) => individual ? - - {individual.date_of_birth || EM_DASH} - {individual.sex || "UNKNOWN_SEX"} - {getAge(individual)} - {individual.ethnicity || "UNKNOWN_ETHNICITY"} - {individual.karyotypic_sex || "UNKNOWN_KARYOTYPE"} - - ({label})} - /> - - { - (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) - ?
+const IndividualOverview = ({individual}) => { + const resourcesTuple = useIndividualResources(individual); + + if (!individual) return
; + return ( + + {individual.date_of_birth || EM_DASH} + {individual.sex || "UNKNOWN_SEX"} + {getAge(individual)} + {individual.ethnicity || "UNKNOWN_ETHNICITY"} + {individual.karyotypic_sex || "UNKNOWN_KARYOTYPE"} + + ({label})} + /> + + { + (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) + ?
                            individual ?
                                      enableClipboard={false}
                           />
                     
-
- : EM_DASH - }
-
:
; +
+ : EM_DASH + } + + ); +} IndividualOverview.propTypes = { individual: individualPropTypesShape, diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 6ea703bc5..0b1e6dc91 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -5,8 +5,11 @@ import { Icon, Table } from "antd"; import { EM_DASH } from "../../constants"; import { individualPropTypesShape } from "../../propTypes"; import OntologyTerm from "./OntologyTerm"; +import { useIndividualResources } from "./utils"; const IndividualPhenotypicFeatures = ({ individual }) => { + const resourcesTuple = useIndividualResources(individual); + const columns = useMemo(() => [ { title: "Feature", @@ -20,7 +23,7 @@ const IndividualPhenotypicFeatures = ({ individual }) => { ) : <> - {" "} + {" "} {negated ? ( (Excluded:{" "} @@ -63,7 +66,7 @@ const IndividualPhenotypicFeatures = ({ individual }) => { }, }, - ], [individual]); + ], [resourcesTuple]); const data = useMemo(() => { const phenopackets = (individual?.phenopackets ?? []); diff --git a/src/components/explorer/OntologyTerm.js b/src/components/explorer/OntologyTerm.js index 41a192869..4d9d44d0b 100644 --- a/src/components/explorer/OntologyTerm.js +++ b/src/components/explorer/OntologyTerm.js @@ -1,17 +1,17 @@ import React, { memo, useEffect } from "react"; import PropTypes from "prop-types"; -import { Icon } from "antd"; +import { Button, Icon } from "antd"; import { EM_DASH } from "../../constants"; -import { individualPropTypesShape, ontologyShape } from "../../propTypes"; +import { ontologyShape } from "../../propTypes"; import { id } from "../../utils/misc"; import { useResourcesByNamespacePrefix } from "./utils"; -const OntologyTerm = memo(({ individual, term, renderLabel }) => { +const OntologyTerm = memo(({ resourcesTuple, term, renderLabel }) => { // TODO: perf: might be slow to generate this over and over - const resourcesByNamespacePrefix = useResourcesByNamespacePrefix(individual); + const [resourcesByNamespacePrefix, isFetchingResources] = useResourcesByNamespacePrefix(resourcesTuple); if (!term) { return ( @@ -43,22 +43,30 @@ const OntologyTerm = memo(({ individual, term, renderLabel }) => { if (termResource?.iri_prefix && !termResource.iri_prefix.includes("example.org")) { defLink = `${termResource.iri_prefix}${namespaceID}`; } // If resource doesn't exist / isn't linkable, don't include a link - } // Otherwise, malformed ID - render without a link + } // Otherwise, malformed ID - render a disabled link return ( {renderLabel(term.label)} (ID: {term.id}){" "} - {defLink && ( - + + + ); }); OntologyTerm.propTypes = { - individual: individualPropTypesShape, + resourcesTuple: PropTypes.array, term: ontologyShape.isRequired, renderLabel: PropTypes.func, }; diff --git a/src/components/explorer/explorer.css b/src/components/explorer/explorer.css index 012096070..136d5099a 100644 --- a/src/components/explorer/explorer.css +++ b/src/components/explorer/explorer.css @@ -1,11 +1,3 @@ -/* css snippet to help align the antd spinner in the parent row */ -.ant-spin-nested-loading, -.ant-spin-nested-loading > div:first-child, -.ant-spin-spinning, -.ant-spin-container { - display: inline !important; -} - .ant-table-row.ant-table-row-level-0 { vertical-align: top !important; } diff --git a/src/components/explorer/searchResultsTables/BiosamplesTable.js b/src/components/explorer/searchResultsTables/BiosamplesTable.js index 5d4deefd8..2608e19f1 100644 --- a/src/components/explorer/searchResultsTables/BiosamplesTable.js +++ b/src/components/explorer/searchResultsTables/BiosamplesTable.js @@ -1,4 +1,4 @@ -import React from "react"; +import React, {useMemo} from "react"; import PropTypes from "prop-types"; import { useSortedColumns } from "../hooks/explorerHooks"; import { useSelector } from "react-redux"; @@ -10,7 +10,7 @@ import OntologyTerm from "../OntologyTerm"; import { ontologyShape } from "../../../propTypes"; import { countNonNullElements } from "../../../utils/misc"; -import { ontologyTermSorter } from "../utils"; +import { ontologyTermSorter, useDatasetResources } from "../utils"; const NO_EXPERIMENTS_VALUE = -Infinity; @@ -97,59 +97,60 @@ const availableExperimentsSorter = (a, b) => { return highB - highA; }; -const SEARCH_RESULT_COLUMNS_BIOSAMPLE = [ - { - title: "Biosample", - dataIndex: "biosample", - render: (biosample, { individual }) => , - sorter: (a, b) => a.biosample.localeCompare(b.biosample), - defaultSortOrder: "ascend", - }, - { - title: "Individual", - dataIndex: "individual", - render: (individual) => , - sorter: (a, b) => a.individual.id.localeCompare(b.individual.id), - sortDirections: ["descend", "ascend", "descend"], - }, - { - title: "Experiments", - dataIndex: "studyTypes", - render: (studyTypes) => , - sorter: experimentsSorter, - sortDirections: ["descend", "ascend", "descend"], - }, - { - title: "Sampled Tissue", - dataIndex: "sampledTissue", - // Can't pass individual here to OntologyTerm since it doesn't have a list of phenopackets - render: (sampledTissue) => , - sorter: ontologyTermSorter("sampledTissue"), - sortDirections: ["descend", "ascend", "descend"], - }, - { - title: "Available Experiments", - dataIndex: "experimentTypes", - render: availableExperimentsRender, - sorter: availableExperimentsSorter, - sortDirections: ["descend", "ascend", "descend"], - }, -]; - const BiosamplesTable = ({ data, datasetID }) => { + const resourcesTuple = useDatasetResources(datasetID); + const tableSortOrder = useSelector( (state) => state.explorer.tableSortOrderByDatasetID[datasetID]?.["biosamples"], ); + const columns = useMemo(() => [ + { + title: "Biosample", + dataIndex: "biosample", + render: (biosample, { individual }) => , + sorter: (a, b) => a.biosample.localeCompare(b.biosample), + defaultSortOrder: "ascend", + }, + { + title: "Individual", + dataIndex: "individual", + render: (individual) => , + sorter: (a, b) => a.individual.id.localeCompare(b.individual.id), + sortDirections: ["descend", "ascend", "descend"], + }, + { + title: "Experiments", + dataIndex: "studyTypes", + render: (studyTypes) => , + sorter: experimentsSorter, + sortDirections: ["descend", "ascend", "descend"], + }, + { + title: "Sampled Tissue", + dataIndex: "sampledTissue", + // Can't pass individual here to OntologyTerm since it doesn't have a list of phenopackets + render: (sampledTissue) => , + sorter: ontologyTermSorter("sampledTissue"), + sortDirections: ["descend", "ascend", "descend"], + }, + { + title: "Available Experiments", + dataIndex: "experimentTypes", + render: availableExperimentsRender, + sorter: availableExperimentsSorter, + sortDirections: ["descend", "ascend", "descend"], + }, + ], [resourcesTuple]); + const { sortedData, columnsWithSortOrder } = useSortedColumns( data, tableSortOrder, - SEARCH_RESULT_COLUMNS_BIOSAMPLE, + columns, ); return ( ); -export const useResources = (individual) => { +export const useDatasetResources = (datasetIDOrDatasetIDs) => { const dispatch = useDispatch(); - // TODO: when individual belongs to a single dataset, use that instead - const individualDatasets = useMemo( - () => (individual?.phenopackets ?? []).map(p => p.dataset), - [individual]); - const datasetResources = useSelector((state) => state.datasetResources.itemsByID); + const datasetIDs = useMemo( + () => Array.isArray(datasetIDOrDatasetIDs) ? datasetIDOrDatasetIDs : [datasetIDOrDatasetIDs], + [datasetIDOrDatasetIDs], + ); + useEffect(() => { - individualDatasets.map((d) => dispatch(fetchDatasetResourcesIfNecessary(d))); - }, [dispatch, individualDatasets]); + datasetIDs.map((d) => dispatch(fetchDatasetResourcesIfNecessary(d))); + }, [dispatch, datasetIDs]); return useMemo( - () => - Object.values( + () => { + const r = Object.values( Object.fromEntries( - individualDatasets + datasetIDs .flatMap(d => datasetResources[d]?.data ?? []) .map(r => [r.id, r]), ), - ), - [datasetResources, individualDatasets], + ); + const fetching = datasetIDs.reduce((flag, d) => flag || datasetResources[d]?.isFetching, false); + return [r, fetching]; + }, + [datasetResources, datasetIDs], ); }; +export const useIndividualResources = (individual) => { + // TODO: when individual belongs to a single dataset, use that instead + const individualDatasets = useMemo( + () => (individual?.phenopackets ?? []).map(p => p.dataset), + [individual]); -export const useResourcesByNamespacePrefix = (individual) => { - const resources = useResources(individual); + return useDatasetResources(individualDatasets); +}; + +export const useResourcesByNamespacePrefix = ([resources, isFetching]) => { return useMemo( - () => Object.fromEntries(resources.map(r => [r.namespace_prefix, r])), - [individual], + () => [ + Object.fromEntries(resources.map(r => [r.namespace_prefix, r])), + isFetching, + ], + [resources, isFetching], ); }; From c23a4fd26170186521ae8425c80e0877b05b981e Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Fri, 22 Sep 2023 14:04:53 -0400 Subject: [PATCH 69/79] lint --- src/components/explorer/IndividualOverview.js | 6 ++++-- .../explorer/searchResultsTables/BiosamplesTable.js | 4 +++- src/components/explorer/utils.js | 2 +- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/components/explorer/IndividualOverview.js b/src/components/explorer/IndividualOverview.js index d65005510..6acf8b175 100644 --- a/src/components/explorer/IndividualOverview.js +++ b/src/components/explorer/IndividualOverview.js @@ -18,7 +18,9 @@ const IndividualOverview = ({individual}) => { {individual.sex || "UNKNOWN_SEX"} {getAge(individual)} {individual.ethnicity || "UNKNOWN_ETHNICITY"} - {individual.karyotypic_sex || "UNKNOWN_KARYOTYPE"} + { + individual.karyotypic_sex || "UNKNOWN_KARYOTYPE"} + { } ); -} +}; IndividualOverview.propTypes = { individual: individualPropTypesShape, diff --git a/src/components/explorer/searchResultsTables/BiosamplesTable.js b/src/components/explorer/searchResultsTables/BiosamplesTable.js index 2608e19f1..f128c884d 100644 --- a/src/components/explorer/searchResultsTables/BiosamplesTable.js +++ b/src/components/explorer/searchResultsTables/BiosamplesTable.js @@ -108,7 +108,9 @@ const BiosamplesTable = ({ data, datasetID }) => { { title: "Biosample", dataIndex: "biosample", - render: (biosample, { individual }) => , + render: (biosample, { individual }) => ( + + ), sorter: (a, b) => a.biosample.localeCompare(b.biosample), defaultSortOrder: "ascend", }, diff --git a/src/components/explorer/utils.js b/src/components/explorer/utils.js index ae2c22d69..6142fe181 100644 --- a/src/components/explorer/utils.js +++ b/src/components/explorer/utils.js @@ -22,7 +22,7 @@ export const useDatasetResources = (datasetIDOrDatasetIDs) => { const datasetIDs = useMemo( () => Array.isArray(datasetIDOrDatasetIDs) ? datasetIDOrDatasetIDs : [datasetIDOrDatasetIDs], - [datasetIDOrDatasetIDs], + [datasetIDOrDatasetIDs], ); useEffect(() => { From 3a2204e2a791c8eb054f6ff0a926a2e130c7885f Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 Sep 2023 14:15:39 -0400 Subject: [PATCH 70/79] fix(explorer): experiment results missing crash --- src/components/explorer/IndividualExperiments.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 2160e22d0..039fbc6f1 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -252,7 +252,8 @@ const Experiments = ({ individual, handleExperimentClick }) => { { title: "Experiment Results", key: "experiment_results", - render: (exp) => {exp.experiment_results.length ?? 0} files, + // experiment_results can be undefined if no experiment results exist + render: (exp) => {exp.experiment_results?.length ?? 0} files, }, ], [resourcesTuple, handleExperimentClick], From 4850703e3e5bbc67fbfeeb73dc7659ec76f5a4d2 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 Sep 2023 15:36:54 -0400 Subject: [PATCH 71/79] style(explorer): experiment section titles + dont show empty results table --- src/components/explorer/IndividualExperiments.js | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index 039fbc6f1..e893f39f4 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -3,7 +3,7 @@ import { useDispatch, useSelector } from "react-redux"; import { Route, Switch, useHistory, useParams, useRouteMatch } from "react-router-dom"; import PropTypes from "prop-types"; -import { Button, Descriptions, Popover, Table, Typography } from "antd"; +import { Button, Descriptions, Icon, Popover, Table, Typography } from "antd"; import { EM_DASH } from "../../constants"; import { experimentPropTypesShape, experimentResultPropTypesShape, individualPropTypesShape } from "../../propTypes"; @@ -129,7 +129,7 @@ const ExperimentDetail = ({ experiment, resourcesTuple }) => { return (
- {experimentType} - Details + Details {id} @@ -176,8 +176,10 @@ const ExperimentDetail = ({ experiment, resourcesTuple }) => { - {experimentType} - Results -
+ {sortedExperimentResults.length ? 'Results' : 'No experiment results'} + + {sortedExperimentResults.length ?
{ rowKey="id" dataSource={sortedExperimentResults} style={{ maxWidth: 1200, backgroundColor: "white" }} - /> + /> : null} ); }; From 13522cc88afe637e08eaf7358971cee0e45a10a6 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Mon, 25 Sep 2023 15:42:51 -0400 Subject: [PATCH 72/79] lint --- src/components/explorer/IndividualExperiments.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualExperiments.js b/src/components/explorer/IndividualExperiments.js index e893f39f4..b794ca3d2 100644 --- a/src/components/explorer/IndividualExperiments.js +++ b/src/components/explorer/IndividualExperiments.js @@ -177,7 +177,7 @@ const ExperimentDetail = ({ experiment, resourcesTuple }) => { - {sortedExperimentResults.length ? 'Results' : 'No experiment results'} + {sortedExperimentResults.length ? "Results" : "No experiment results"} {sortedExperimentResults.length ?
Date: Mon, 25 Sep 2023 15:49:13 -0400 Subject: [PATCH 73/79] lint --- src/components/explorer/ExplorerDatasetSearch.js | 2 +- src/components/explorer/utils.js | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/explorer/ExplorerDatasetSearch.js b/src/components/explorer/ExplorerDatasetSearch.js index 2271c14e8..97fc6f909 100644 --- a/src/components/explorer/ExplorerDatasetSearch.js +++ b/src/components/explorer/ExplorerDatasetSearch.js @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect} from "react"; +import React, { useCallback, useEffect } from "react"; import { useSelector, useDispatch } from "react-redux"; import { useParams } from "react-router-dom"; diff --git a/src/components/explorer/utils.js b/src/components/explorer/utils.js index 6142fe181..e0b165a7a 100644 --- a/src/components/explorer/utils.js +++ b/src/components/explorer/utils.js @@ -6,7 +6,7 @@ export const useDeduplicatedIndividualBiosamples = (individual) => useMemo( () => Object.values( Object.fromEntries( - (individual || {}).phenopackets + (individual?.phenopackets ?? []) .flatMap(p => p.biosamples) .map(b => [b.id, b]), ), From f36217c1d26242c3e5be1ca4d0fec2578ba55ffe Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Sep 2023 10:05:10 -0400 Subject: [PATCH 74/79] fix(explorer): missing key when rendering biosample id list in search --- .../explorer/searchResultsTables/IndividualsTable.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/explorer/searchResultsTables/IndividualsTable.js b/src/components/explorer/searchResultsTables/IndividualsTable.js index fd0f5119b..7d60a9367 100644 --- a/src/components/explorer/searchResultsTables/IndividualsTable.js +++ b/src/components/explorer/searchResultsTables/IndividualsTable.js @@ -21,10 +21,10 @@ const SEARCH_RESULT_COLUMNS = [ <> {samples.length} Sample{samples.length === 1 ? "" : "s"} {samples.length ? ": " : ""} - {samples.map((s, si) => <> + {samples.map((s, si) => {si < samples.length - 1 ? ", " : ""} - )} + )} ), sorter: (a, b) => a.biosamples.length - b.biosamples.length, From 038f9f47ceabbc56d6ba82ac821504794eb56697 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Sep 2023 10:12:58 -0400 Subject: [PATCH 75/79] fix(explorer): wrong row key for phenotypic features --- src/components/explorer/IndividualPhenotypicFeatures.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 0b1e6dc91..543d67712 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -89,7 +89,7 @@ const IndividualPhenotypicFeatures = ({ individual }) => { size="middle" pagination={false} columns={columns} - rowKey="id" + rowKey="key" dataSource={data} /> ); From f018b47a51b1d17effd8211f48e65d921a4a363c Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Sep 2023 10:13:23 -0400 Subject: [PATCH 76/79] lint: rm debug log --- src/components/explorer/IndividualPhenotypicFeatures.js | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/explorer/IndividualPhenotypicFeatures.js b/src/components/explorer/IndividualPhenotypicFeatures.js index 543d67712..9e885d2c4 100644 --- a/src/components/explorer/IndividualPhenotypicFeatures.js +++ b/src/components/explorer/IndividualPhenotypicFeatures.js @@ -45,7 +45,6 @@ const IndividualPhenotypicFeatures = ({ individual }) => { title: "Extra Properties", dataIndex: "extra_properties", render: (extraProperties, feature) => { - console.log(feature); const nExtraProperties = Object.keys(extraProperties ?? {}).length; return { children: nExtraProperties ? ( From 0c77cf8ff2ff04e6911bcd79226bc3f30ca73672 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Sep 2023 10:15:22 -0400 Subject: [PATCH 77/79] lint: allow undefined term in OntologyTerm --- src/components/explorer/OntologyTerm.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/components/explorer/OntologyTerm.js b/src/components/explorer/OntologyTerm.js index 4d9d44d0b..8045b2e51 100644 --- a/src/components/explorer/OntologyTerm.js +++ b/src/components/explorer/OntologyTerm.js @@ -67,7 +67,7 @@ const OntologyTerm = memo(({ resourcesTuple, term, renderLabel }) => { OntologyTerm.propTypes = { resourcesTuple: PropTypes.array, - term: ontologyShape.isRequired, + term: ontologyShape, renderLabel: PropTypes.func, }; OntologyTerm.defaultProps = { From 34d28bf292539c7a4a6f47077696176e380a4b23 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Sep 2023 10:32:05 -0400 Subject: [PATCH 78/79] chore(explorer): don't render disease ID (visual noise) --- src/components/explorer/IndividualDiseases.js | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/components/explorer/IndividualDiseases.js b/src/components/explorer/IndividualDiseases.js index fdec24f4c..77612ac8f 100644 --- a/src/components/explorer/IndividualDiseases.js +++ b/src/components/explorer/IndividualDiseases.js @@ -17,15 +17,13 @@ const IndividualDiseases = ({ individual }) => { const columns = useMemo(() => [ { - title: "Disease ID", - key: "id", - sorter: (a, b) => a.id.toString().localeCompare(b.id), - defaultSortOrder: "ascend", - }, - { - title: "term", + title: "Disease", dataIndex: "term", - render: (term) => , + // Tag the ontology term with a data attribute holding the disease ID. This has no effect, but might + // help us debug diseases in production if we need it. + render: (term, disease) => ( + + ), sorter: ontologyTermSorter("term"), }, { From 9ad4cc922f8d4a0ae5d271d4ba354733daa09f03 Mon Sep 17 00:00:00 2001 From: David Lougheed Date: Tue, 26 Sep 2023 10:35:11 -0400 Subject: [PATCH 79/79] style(explorer): improve individual extra props rendering --- src/components/explorer/IndividualOverview.js | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/components/explorer/IndividualOverview.js b/src/components/explorer/IndividualOverview.js index 6acf8b175..ab8d330b1 100644 --- a/src/components/explorer/IndividualOverview.js +++ b/src/components/explorer/IndividualOverview.js @@ -30,17 +30,15 @@ const IndividualOverview = ({individual}) => { { (individual.hasOwnProperty("extra_properties") && Object.keys(individual.extra_properties).length) - ?
-
-                          
-                    
-
- : EM_DASH + ? ( + + ) : EM_DASH }
);