Skip to content

Commit

Permalink
NickAkhmetov/CAT-844 Migrate away from using descendants and `ances…
Browse files Browse the repository at this point in the history
…tors` in favor of `id` equivalents (#3515)
  • Loading branch information
NickAkhmetov authored Aug 28, 2024
1 parent b662d76 commit 1b0081a
Show file tree
Hide file tree
Showing 9 changed files with 91 additions and 101 deletions.
1 change: 1 addition & 0 deletions CHANGELOG-cat-844.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
- Deprecate `ancestors`, `descendants`, and their `immediate` variants to only look up by ID.
14 changes: 4 additions & 10 deletions context/app/routes_browse.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,18 +139,12 @@ def details_rui_json(type, uuid):
abort(404)
client = get_client()
entity = client.get_entity(uuid)
# For Samples...
# For samples and datasets, the nearest RUI location is indexed with the entity itself.
# https://github.com/hubmapconsortium/search-api/pull/860
if 'rui_location' in entity:
return json.loads(entity['rui_location'])
# For Datasets...
if 'ancestors' not in entity:
abort(404)
located_ancestors = [a for a in entity['ancestors'] if 'rui_location' in a]
if not located_ancestors:
abort(404)
# There may be multiple: The last should be the closest...
# but this should be confirmed, when there are examples.
return json.loads(located_ancestors[-1]['rui_location'])
# Otherwise throw 404
abort(404)


@blueprint.route('/sitemap.txt')
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import React, { useState } from 'react';
import Typography from '@mui/material/Typography';
import Button from '@mui/material/Button';
import Skeleton from '@mui/material/Skeleton';

import { useFlaskDataContext } from 'js/components/Contexts';
import { ESEntityType, Entity } from 'js/components/types';
import { entityIconMap } from 'js/shared-styles/icons/entityIconMap';
import { useTrackEntityPageEvent } from 'js/components/detailPage/useTrackEntityPageEvent';
import { Button } from '@mui/material';
import { useEntitiesData } from 'js/hooks/useEntityData';
import { ErrorTile } from 'js/components/entity-tile/EntityTile/EntityTile';
import { FlexContainer, FlexColumn, TableColumn, StyledSvgIcon, ProvTableEntityHeader } from './style';
import ProvTableTile from '../ProvTableTile';
import ProvTableDerivedLink from '../ProvTableDerivedLink';
Expand All @@ -20,14 +23,23 @@ interface ProvTableColumnProps {
entities: Entity[];
currentEntityUUID: string;
descendantEntityCounts?: Record<string, number>;
missingAncestors?: string[];
}

function ProvEntityColumn({ type, entities, currentEntityUUID, descendantEntityCounts }: ProvTableColumnProps) {
function ProvEntityColumn({
type,
entities,
currentEntityUUID,
descendantEntityCounts,
missingAncestors,
}: ProvTableColumnProps) {
const trackEntityPageEvent = useTrackEntityPageEvent();

// Track expanded state for each sample category
const [isExpanded, setIsExpanded] = useState<Record<string, boolean>>({});

const displayMissingAncestors =
missingAncestors && missingAncestors.length > 0 && entities.length === 0 && type !== 'Dataset';
return (
<TableColumn key={`provenance-list-${type.toLowerCase()}`}>
<ProvTableEntityHeader>
Expand Down Expand Up @@ -72,20 +84,42 @@ function ProvEntityColumn({ type, entities, currentEntityUUID, descendantEntityC
onClick={() =>
trackEntityPageEvent({ action: 'Provenance / Table / Select Card', label: item.hubmap_id })
}
entityData={item}
/>
);
})}
{descendantEntityCounts?.[type] && <ProvTableDerivedLink uuid={currentEntityUUID} type={type} />}
{displayMissingAncestors && missingAncestors.map((id) => <ErrorTile key={id} entity_type={type} id={id} />)}
</FlexColumn>
</TableColumn>
);
}

const provTableSource = [
'uuid',
'hubmap_id',
'entity_type',
'descendant_counts',
'mapped_data_types',
'last_modified_timestamp',
'origin_samples_unique_mapped_organs',
'mapped_metadata',
'sample_category',
'thumbnail_file',
];

function ProvTable() {
// Make a new list rather modifying old one in place: Caused duplication in UI.
const { entity: assayMetadata } = useFlaskDataContext();
const { ancestors, uuid } = assayMetadata;
const ancestorsAndSelf = [...ancestors, assayMetadata];
const { uuid, ancestor_ids } = assayMetadata;
const ancestorAndSelfIds = [...ancestor_ids, uuid];
const [ancestorsAndSelf, isLoadingAncestors] = useEntitiesData(ancestorAndSelfIds, provTableSource);

if (isLoadingAncestors) {
return <Skeleton variant="rectangular" height={300} />;
}

const missingAncestors = ancestorAndSelfIds.filter((id) => !ancestorsAndSelf.find((entity) => entity.uuid === id));

const ancestorsAndSelfByType = ancestorsAndSelf.reduce(
(acc, entity) => {
Expand All @@ -111,6 +145,7 @@ function ProvTable() {
entities={entities}
currentEntityUUID={uuid}
descendantEntityCounts={descendantEntityCounts}
missingAncestors={missingAncestors}
/>
))}
</FlexContainer>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,49 +1,30 @@
import React, { ComponentProps } from 'react';

import { useEntityData } from 'js/hooks/useEntityData';
import EntityTile from 'js/components/entity-tile/EntityTile';
import { getTileDescendantCounts } from 'js/components/entity-tile/EntityTile/utils';
import { ErrorTile } from 'js/components/entity-tile/EntityTile/EntityTile';
import ProvTableDerivedLink from '../ProvTableDerivedLink';
import { DownIcon } from './style';

interface ProvTableTileProps extends Omit<ComponentProps<typeof EntityTile>, 'entityData' | 'descendantCounts'> {
interface ProvTableTileProps extends Omit<ComponentProps<typeof EntityTile>, 'descendantCounts'> {
isCurrentEntity: boolean;
isSampleSibling: boolean;
isFirstTile: boolean;
isLastTile: boolean;
}

const provTilesSource = [
'descendant_counts',
'mapped_data_types',
'last_modified_timestamp',
'origin_samples_unique_mapped_organs',
'mapped_metadata',
'sample_category',
'origin_samples_unique_mapped_organs',
'thumbnail_file',
];

function ProvTableTile({
uuid,
entity_type,
entityData,
isCurrentEntity,
isSampleSibling,
isFirstTile,
isLastTile,
...rest
}: ProvTableTileProps) {
// mapped fields are not included in ancestor object, so we need to fetch them separately
const [entityData, isLoading] = useEntityData(uuid, provTilesSource);
const descendantCounts = entityData?.descendant_counts?.entity_type;
const descendantCountsToDisplay = getTileDescendantCounts(entityData, entity_type);

if (!entityData && !isLoading) {
// No entity data found for this tile and not loading = the entity failed to index
return <ErrorTile entity_type={entity_type} id={rest.id} />;
}

return (
<>
{!isFirstTile && !isSampleSibling && entity_type !== 'Donor' && <DownIcon />}
Expand All @@ -57,7 +38,7 @@ function ProvTableTile({
{...rest}
/>
)}
{isLastTile && entity_type !== 'Donor' && descendantCounts?.[entity_type] > 0 && (
{isLastTile && entity_type !== 'Donor' && (descendantCounts?.[entity_type] ?? 0) > 0 && (
<ProvTableDerivedLink uuid={uuid} type={entity_type} />
)}
</>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,15 +39,6 @@ export const getAncestorsQuery = (uuid: string) => ({
},
},
},
{
bool: {
must_not: {
term: {
'ancestors.entity_type.keyword': 'Dataset',
},
},
},
},
],
},
},
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import React, { ComponentProps, ComponentType } from 'react';
import Typography from '@mui/material/Typography';
import IconButton from '@mui/material/IconButton';
import ContentCopyIcon from '@mui/icons-material/ContentCopyRounded';

import Tile from 'js/shared-styles/tiles/Tile/';
Expand All @@ -12,6 +11,7 @@ import ContactUsLink from 'js/shared-styles/Links/ContactUsLink';
import { useHandleCopyClick } from 'js/hooks/useCopyText';
import Paper from '@mui/material/Paper';
import Stack from '@mui/material/Stack';
import IconLink from 'js/shared-styles/Links/iconLinks/IconLink';
import EntityTileFooter from '../EntityTileFooter/index';
import EntityTileBody from '../EntityTileBody/index';
import { StyledIcon } from './style';
Expand Down Expand Up @@ -73,56 +73,25 @@ function ErrorTile({ entity_type, id }: Pick<EntityTileProps, 'id' | 'entity_typ
})}
variant="outlined"
>
<Stack direction="row" gap={1} color="error.main" alignItems="center">
<StatusIcon status="error" sx={{ fontSize: '1.5rem' }} />
<Typography variant="subtitle1">{id}</Typography>
<IconButton onClick={() => copy(id)}>
<ContentCopyIcon color="info" />
</IconButton>
<Stack direction="row" gap={1}>
<StatusIcon status="error" sx={{ alignSelf: 'start', fontSize: '1.25rem' }} />
<Typography variant="body2">
Unable to load {entityTypeLowercase}. <ContactUsLink capitalize /> with the{' '}
<IconLink
href="#"
onClick={(e) => {
e.preventDefault();
copy(id);
}}
icon={<ContentCopyIcon />}
>
{entityTypeLowercase} ID
</IconLink>
for more information.
</Typography>
</Stack>
<Typography variant="body2">
Unable to load {entityTypeLowercase}. <ContactUsLink capitalize /> with the {entityTypeLowercase} ID for more
information.
</Typography>
</Paper>
);
// return (
// <Tile
// icon={
// <StatusIcon
// status="error"
// sx={{ fontSize: '1.5rem', marginRight: (theme) => theme.spacing(1), alignSelf: 'start' }}
// />
// }
// bodyContent={
// <>
// <Tile.Title>Unable to load {entityTypeLowercase}.</Tile.Title>
// <Typography variant="body2">
// Please <ContactUsLink /> with the {entityTypeLowercase}&apos;s ID for more information.
// </Typography>
// <Typography variant="body2">
// ID:{' '}
// <IconLink
// onClick={(e) => {
// e.preventDefault();
// copy(id);
// }}
// href="#"
// icon={
// <IconButton>
// <ContentCopyIcon />
// </IconButton>
// }
// >
// {id}
// </IconLink>
// </Typography>
// </>
// }
// footerContent={undefined}
// tileWidth={tileWidth}
// />
// );
}

export { tileWidth, ErrorTile };
Expand Down
3 changes: 3 additions & 0 deletions context/app/static/js/components/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,9 @@ export interface Entity {
contacts: ContactAPIResponse[];
contributors: ContributorAPIResponse[];
created_timestamp: number;
/** @deprecated Use `ancestor_ids` and `useEntitiesData` instead */
ancestors: Entity[];
ancestor_ids: string[];
// eslint-disable-next-line no-use-before-define -- Donor is defined later in the file and extends Entity
donor: Donor;
descendant_counts: { entity_type: Record<string, number> };
Expand All @@ -47,6 +49,7 @@ export interface Entity {
dag_provenance_list: DagProvenanceType[];
[key: string]: unknown;
};
/** @deprecated Use `descendant_ids` and `useEntitiesData` instead */
descendants: Entity[];
group_name: string;
created_by_user_displayname: string;
Expand Down
20 changes: 19 additions & 1 deletion context/app/static/js/hooks/useEntityData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,30 @@ import { useMemo } from 'react';
import { useSearchHits } from 'js/hooks/useSearchData';
import { Entity } from 'js/components/types';

export const useEntityQuery = (uuid: string | string[], source?: string[]) => {
return useMemo(
() => ({
query: { ids: { values: typeof uuid === 'string' ? [uuid] : uuid } },
_source: source,
}),
[uuid, source],
);
};

export function useEntityData(uuid: string, source?: string[]): [Entity, boolean] {
const query = useMemo(() => ({ query: { ids: { values: [uuid] } }, _source: source }), [uuid, source]);
const query = useEntityQuery(uuid, source);

const { searchHits, isLoading } = useSearchHits<Entity>(query);

return [searchHits[0]?._source, isLoading];
}

export function useEntitiesData(uuids: string[], source?: string[]): [Entity[], boolean] {
const query = useEntityQuery(uuids, source);

const { searchHits, isLoading } = useSearchHits<Entity>(query);

return [searchHits.map((hit) => hit._source), isLoading];
}

export default useEntityData;
12 changes: 5 additions & 7 deletions context/app/static/js/pages/search/DevSearch.jsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import React from 'react';
import { ExistsQuery, BoolMustNot, TermQuery } from 'searchkit';
import { ExistsQuery, BoolMustNot, BoolMust, TermQuery } from 'searchkit';

import { Alert } from 'js/shared-styles/alerts';
import { useAppContext } from 'js/components/Contexts';
Expand Down Expand Up @@ -52,8 +52,8 @@ function DevSearch() {
listFilter('mapped_data_types', 'mapped_data_types'),
listFilter('metadata.metadata.assay_category', 'assay_category'),
listFilter('metadata.metadata.assay_type', 'assay_type'),
checkboxFilter('is_derived', 'Is derived?', TermQuery('ancestors.entity_type', 'dataset')),
checkboxFilter('is_raw', 'Is raw?', BoolMustNot(TermQuery('ancestors.entity_type', 'dataset'))),
checkboxFilter('is_derived', 'Is derived?', BoolMust(TermQuery('processing.keyword', 'processed'))),
checkboxFilter('is_raw', 'Is raw?', BoolMust(TermQuery('processing.keyword', 'raw'))),
hierarchicalFilter({
fields: {
parent: { id: 'metadata.metadata.analyte_class.keyword' },
Expand Down Expand Up @@ -88,10 +88,8 @@ function DevSearch() {
checkboxFilter('no_metadata', 'No metadata?', BoolMustNot(ExistsQuery('metadata.metadata'))),
checkboxFilter('has_files', 'Has files?', ExistsQuery('files')),
checkboxFilter('no_files', 'No files?', BoolMustNot(ExistsQuery('files'))),
checkboxFilter('has_rui_sample', 'Spatial Sample?', ExistsQuery('rui_location')),
checkboxFilter('no_rui_sample', 'Not Spatial Sample?', BoolMustNot(ExistsQuery('rui_location'))),
checkboxFilter('has_rui_dataset', 'Spatial Dataset?', ExistsQuery('ancestors.rui_location')),
checkboxFilter('no_rui_dataset', 'Not Spatial Dataset?', BoolMustNot(ExistsQuery('ancestors.rui_location'))),
checkboxFilter('is_spatial', 'Spatial?', ExistsQuery('rui_location')),
checkboxFilter('not_spatial', 'Not Spatial?', BoolMustNot(ExistsQuery('rui_location'))),
checkboxFilter('has_errors', 'Validation Errors?', ExistsQuery('mapper_metadata.validation_errors')),
checkboxFilter(
'no_errors',
Expand Down

0 comments on commit 1b0081a

Please sign in to comment.