Skip to content

Commit

Permalink
Austenem/CAT-934 Update metadata sections (#3568)
Browse files Browse the repository at this point in the history
* update combineMetadata param

* add changelog

* append hubmap IDs to duplicate sample categories

* use metadata from provenance table

* fix tab widths

* update labels

* separate entity sorting

* add tests, move to utils file

* clean up

* update changelog

* remove extra utils functions

* lift helper function
  • Loading branch information
austenem authored Oct 15, 2024
1 parent 67e4f30 commit 1ec62d2
Show file tree
Hide file tree
Showing 13 changed files with 350 additions and 327 deletions.
3 changes: 3 additions & 0 deletions CHANGELOG-fix-metadata-table.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- Switch to using metadata table with tabs component in the Sample and Donor pages.
- In the metadata sections of Dataset, Sample, and Donor pages, add tabs for any entities in the Provenance section with metadata.
- Update the metadata table component to show unique labels for each tab and to be scrollable when many tabs are present.
Original file line number Diff line number Diff line change
Expand Up @@ -6,66 +6,14 @@ import { CollapsibleDetailPageSection } from 'js/components/detailPage/DetailPag
import { useTrackEntityPageEvent } from 'js/components/detailPage/useTrackEntityPageEvent';
import { tableToDelimitedString, createDownloadUrl } from 'js/helpers/functions';
import { useMetadataFieldDescriptions } from 'js/hooks/useUBKG';
import { getMetadata, hasMetadata } from 'js/helpers/metadata';
import { ESEntityType, isDataset } from 'js/components/types';
import { useProcessedDatasets } from 'js/pages/Dataset/hooks';
import { entityIconMap } from 'js/shared-styles/icons/entityIconMap';
import { Dataset, Donor, Sample, isDataset } from 'js/components/types';
import withShouldDisplay from 'js/helpers/withShouldDisplay';
import { sectionIconMap } from 'js/shared-styles/icons/sectionIconMap';
import { getTableEntities } from 'js/components/detailPage/MetadataSection/utils';
import { DownloadIcon, StyledWhiteBackgroundIconButton } from '../MetadataTable/style';
import MetadataTabs from '../multi-assay/MultiAssayMetadataTabs';
import { Columns, defaultTSVColumns } from './columns';
import { SectionDescription } from '../ProcessedData/ProcessedDataset/SectionDescription';
import MetadataTable from '../MetadataTable';
import { nodeIcons } from '../DatasetRelationships/nodeTypes';

export function getDescription(
field: string,
metadataFieldDescriptions: Record<string, string> | Record<string, never>,
) {
const [prefix, stem] = field.split('.');
if (!stem) {
return metadataFieldDescriptions?.[field];
}
const description = metadataFieldDescriptions?.[stem];
if (!description) {
return undefined;
}
if (prefix === 'donor') {
return `For the original donor: ${metadataFieldDescriptions?.[stem]}`;
}
if (prefix === 'sample') {
return `For the original sample: ${metadataFieldDescriptions?.[stem]}`;
}
throw new Error(`Unrecognized metadata field prefix: ${prefix}`);
}

export function buildTableData(
tableData: Record<string, string | object | unknown[]>,
metadataFieldDescriptions: Record<string, string> | Record<string, never>,
extraValues: Record<string, string> = {},
) {
return (
Object.entries(tableData)
// Filter out nested objects, like nested "metadata" for Samples...
// but allow arrays. Remember, in JS: typeof [] === 'object'
.filter((entry) => typeof entry[1] !== 'object' || Array.isArray(entry[1]))
// Filter out fields from TSV that aren't really metadata:
.filter((entry) => !['contributors_path', 'antibodies_path', 'version'].includes(entry[0]))
.map((entry) => ({
...extraValues,
key: entry[0],
// eslint-disable-next-line @typescript-eslint/no-base-to-string
value: Array.isArray(entry[1]) ? entry[1].join(', ') : entry[1].toString(),
description: getDescription(entry[0], metadataFieldDescriptions),
}))
);
}

function useTableData(tableData: Record<string, string>) {
const { data: fieldDescriptions } = useMetadataFieldDescriptions();
return buildTableData(tableData, fieldDescriptions);
}
import { Columns, defaultTSVColumns } from './columns';

interface TableRow {
key: string;
Expand Down Expand Up @@ -124,76 +72,25 @@ function MetadataWrapper({ allTableRows, tsvColumns = defaultTSVColumns, childre
);
}

function SingleMetadata({ metadata }: { metadata: Record<string, string> }) {
const tableRows = useTableData(metadata);

return (
<MetadataWrapper allTableRows={tableRows}>
<MetadataTable tableRows={tableRows} />
</MetadataWrapper>
);
}

function getEntityIcon(entity: { entity_type: ESEntityType; is_component?: boolean; processing?: string }) {
if (isDataset(entity)) {
if (entity.is_component) {
return nodeIcons.componentDataset;
}
if (entity.processing === 'processed') {
return nodeIcons.processedDataset;
}
return nodeIcons.primaryDataset;
}
return entityIconMap[entity.entity_type];
}

interface MetadataProps {
metadata?: Record<string, string>;
entities: (Donor | Dataset | Sample)[];
}

function Metadata({ metadata }: MetadataProps) {
const { searchHits: datasetsWithMetadata, isLoading } = useProcessedDatasets(true);
function Metadata({ entities }: MetadataProps) {
const { data: fieldDescriptions } = useMetadataFieldDescriptions();
const {
entity: { uuid },
} = useFlaskDataContext();

const { entity } = useFlaskDataContext();

if (!isDataset(entity)) {
return <SingleMetadata metadata={metadata!} />;
}

if (isLoading || !datasetsWithMetadata) {
return null;
}

const { donor, source_samples } = entity;

const entities = [entity, ...datasetsWithMetadata.map((d) => d._source), ...source_samples, donor]
.filter((e) => hasMetadata({ targetEntityType: e.entity_type, currentEntity: e }))
.map((e) => {
const label = isDataset(e) ? e.assay_display_name : e.entity_type;
return {
uuid: e.uuid,
label,
icon: getEntityIcon(e),
tableRows: buildTableData(
getMetadata({
targetEntityType: e.entity_type,
currentEntity: e,
}),
fieldDescriptions,
{ hubmap_id: e.hubmap_id, label },
),
};
});

const allTableRows = entities.map((d) => d.tableRows).flat();
const tableEntities = getTableEntities({ entities, uuid, fieldDescriptions });
const allTableRows = tableEntities.map((d) => d.tableRows).flat();

return (
<MetadataWrapper
allTableRows={allTableRows}
tsvColumns={[{ id: 'hubmap_id', label: 'HuBMAP ID' }, { id: 'label', label: 'Entity' }, ...defaultTSVColumns]}
>
<MetadataTabs entities={entities} />
<MetadataTabs entities={tableEntities} />
</MetadataWrapper>
);
}
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import { DonorIcon } from 'js/shared-styles/icons';
import { buildTableData, sortEntities, TableEntity } from './utils';

/**
* =============================================================================
* buildTableData
* =============================================================================
*/

test('should look up field descriptions', () => {
expect(
buildTableData(
{
assay_type: 'FAKE-seq',
},
{
assay_type: 'The specific type of assay being executed.',
},
),
).toEqual([
{
description: 'The specific type of assay being executed.',
key: 'assay_type',
value: 'FAKE-seq',
},
]);
});

test('should remove nested objects, but concat nested lists', () => {
expect(
buildTableData(
{
object: { foo: 'bar' },
list: ['foo', 'bar'],
},
{},
),
).toEqual([
{
description: undefined,
key: 'list',
value: 'foo, bar',
},
]);
});

test('should remove keys that are not metadata', () => {
expect(
buildTableData(
{
contributors_path: '/local/path',
antibodies_path: '/local/path',
version: '42',
},
{},
),
).toEqual([]);
});

/**
* =============================================================================
* sortEntities
* =============================================================================
*/

const baseEntity = {
icon: DonorIcon,
tableRows: [],
};

const current: TableEntity = {
...baseEntity,
uuid: 'current',
label: 'Current',
entity_type: 'Dataset',
hubmap_id: 'current',
};

const donor: TableEntity = {
...baseEntity,
uuid: 'donor',
label: 'Donor',
entity_type: 'Donor',
hubmap_id: 'donor',
};

const uniqueSample1: TableEntity = {
...baseEntity,
uuid: 'sample1',
label: 'Unique Category',
entity_type: 'Sample',
hubmap_id: 'sample1',
};

const duplicateSample2: TableEntity = {
...baseEntity,
uuid: 'sample2',
label: 'Duplicate Category (sample2)',
entity_type: 'Sample',
hubmap_id: 'sample2',
};

const duplicateSample3: TableEntity = {
...baseEntity,
uuid: 'sample3',
label: 'Duplicate Category (sample3)',
entity_type: 'Sample',
hubmap_id: 'sample3',
};

test('should sort by current -> donor -> samples without IDs in their label -> samples with IDs in their label', () => {
expect(
sortEntities({
tableEntities: [duplicateSample2, duplicateSample3, donor, current, uniqueSample1],
uuid: 'current',
}),
).toEqual([current, donor, uniqueSample1, duplicateSample2, duplicateSample3]);
});

test('should sort by current -> samples without IDs in their label -> samples with IDs in their label when donor is absent', () => {
expect(
sortEntities({
tableEntities: [duplicateSample2, current, uniqueSample1, duplicateSample3],
uuid: 'current',
}),
).toEqual([current, uniqueSample1, duplicateSample2, duplicateSample3]);
});

test('should sort by donor -> samples without IDs -> samples with IDs when current is absent', () => {
expect(
sortEntities({
tableEntities: [duplicateSample3, duplicateSample2, donor, uniqueSample1],
uuid: 'current',
}),
).toEqual([donor, uniqueSample1, duplicateSample2, duplicateSample3]);
});

test('should return only the current entity if no other entities are present', () => {
expect(
sortEntities({
tableEntities: [current],
uuid: 'current',
}),
).toEqual([current]);
});
Loading

0 comments on commit 1ec62d2

Please sign in to comment.