Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Austenem/CAT-934 Update metadata sections #3568

Merged
merged 14 commits into from
Oct 15, 2024
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
Loading