Skip to content

Commit

Permalink
feat: add data dictionary tooltip support #4131 (#320)
Browse files Browse the repository at this point in the history
  • Loading branch information
MillenniumFalconMechanic authored Feb 14, 2025
1 parent 4f084ba commit 849e5cf
Show file tree
Hide file tree
Showing 12 changed files with 451 additions and 31 deletions.
37 changes: 37 additions & 0 deletions src/common/entities.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,12 @@
/**
* Model of a value of a metadata class.
*/
export interface Attribute {
description: string;
key: string;
label: string;
}

/**
* Filterable metadata keys.
*/
Expand All @@ -12,11 +21,38 @@ export interface CategoryTag {
superseded: boolean;
}

/**
* Model of a metadata class, to be specified manually or built from LinkML schema.
*/
export interface Class {
attributes: Attribute[];
description: string;
key: string;
label: string;
name: string;
}

/**
* Category values to be used as keys. For example, "Homo sapiens" or "10X 3' v2 sequencing".
*/
export type CategoryValueKey = unknown;

/**
* Model of a metadata dictionary containing a set of classes and their definitions.
*/
export interface DataDictionary {
classes: Class[];
}

/**
* Label and description values from a data dictionary that are added to a site
* config value.
*/
export interface DataDictionaryAnnotation {
description: string;
label: string;
}

/**
* Set of selected category values.
*/
Expand Down Expand Up @@ -72,6 +108,7 @@ export interface SelectCategoryValueView {
* View model of category, for multiselect categories.
*/
export interface SelectCategoryView {
annotation?: DataDictionaryAnnotation;
isDisabled?: boolean;
key: CategoryKey;
label: string;
Expand Down
160 changes: 160 additions & 0 deletions src/components/DataDictionary/common/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
import {
Attribute,
Class,
DataDictionary,
DataDictionaryAnnotation,
} from "../../../common/entities";
import { CategoryGroupConfig, SiteConfig } from "../../../config/entities";

/**
* Annotate each entity column configuration with data dictionary values. Specifically,
* look up label and description for each column key.
* @param siteConfig - Site configuration to annotate.
* @param annotationsByKey - Data dictionary annotations keyed by key.
*/
export function annotateColumnConfig(
siteConfig: SiteConfig,
annotationsByKey: Record<string, DataDictionaryAnnotation>
): void {
// Annotate every column in every entity.
siteConfig.entities.forEach((entity) => {
entity.list.columns.forEach((columnConfig) => {
// Find the annotation for the column key.
const annotation = annotationsByKey[columnConfig.id];
if (!annotation) {
return;
}

if (!columnConfig.meta) {
columnConfig.meta = {};
}
columnConfig.meta.annotation = annotation;
});
});
}

/**
* Annotate filter and colummn configuration with data dictionary values. Note this
* functionality mutates the site config. A possible future improvement would be to
* create either a specific "raw" or "annotated" type to indicate clearly the point
* at which the config has been annotated.
* @param siteConfig - The site configuration to annotate.
*/
export function annotateSiteConfig(siteConfig: SiteConfig): void {
// Build and map data dictionary annotations by key.
const { dataDictionary } = siteConfig;
if (!dataDictionary) {
return;
}
const annotationsByKey = keyAnnotationsByKey(dataDictionary);

// Annotate elements of site config.
annotateEntityConfig(siteConfig, annotationsByKey);
annotateDefaultCategoryConfig(siteConfig, annotationsByKey);
annotateEntityCategoryConfig(siteConfig, annotationsByKey);
annotateColumnConfig(siteConfig, annotationsByKey);
}

/**
* Annotate entity configuration with data dictionary values. Specifically, look
* up label and description for each entity key.
* @param siteConfig - The site configuration to annotate.
* @param annotationsByKey - Data dictionary annotations keyed by key.
*/
export function annotateEntityConfig(
siteConfig: SiteConfig,
annotationsByKey: Record<string, DataDictionaryAnnotation>
): void {
// Annotate every entity.
siteConfig.entities.forEach((entityConfig) => {
// Check entity for a data dictionary key.
const { key } = entityConfig;
if (!key) {
return;
}

// Find corresponding annotation for the key and set on entity config.
entityConfig.annotation = annotationsByKey[key];
});
}

/**
* Annotate top-level (app-wide) category config with data dictionary values.
* Specifically, look up label and description for each filter key.
* @param siteConfig - Site configuration to annotate.
* @param annotationsByKey - Data dictionary annotations keyed by key.
*/
export function annotateDefaultCategoryConfig(
siteConfig: SiteConfig,
annotationsByKey: Record<string, DataDictionaryAnnotation>
): void {
const { categoryGroupConfig } = siteConfig;
if (categoryGroupConfig) {
annotateCategoryGroupConfig(categoryGroupConfig, annotationsByKey);
}
}

/**
* Annotate entity-specific category config with data dictionary values. Specifically,
* look up label and description for each category key.
* @param siteConfig - Site configuration to annotate.
* @param annotationsByKey - Data dictionary annotations keyed by key.
*/
export function annotateEntityCategoryConfig(
siteConfig: SiteConfig,
annotationsByKey: Record<string, DataDictionaryAnnotation>
): void {
// Annotate every category in every entity.
siteConfig.entities.forEach((entityConfig) => {
const { categoryGroupConfig } = entityConfig;
if (categoryGroupConfig) {
annotateCategoryGroupConfig(categoryGroupConfig, annotationsByKey);
}
});
}

/**
* Annonate category group configuration with data dictionary values.
* @param categoryGroupConfig - Category group to annotate.
* @param annotationsByKey - Data dictionary annotations keyed by key.
*/
function annotateCategoryGroupConfig(
categoryGroupConfig: CategoryGroupConfig,
annotationsByKey: Record<string, DataDictionaryAnnotation>
): void {
categoryGroupConfig.categoryGroups.forEach((categoryGroup) => {
categoryGroup.categoryConfigs.forEach((categorConfig) => {
categorConfig.annotation = annotationsByKey[categorConfig.key];
});
});
}

/**
* Transform a data dictionary into a key-annotation map. Build annotations for both
* classes and attributes and add to map.
* @param dataDictionary - Data dictionary to transform into a key-annotation map.
* @returns Key-annotation map.
*/
function keyAnnotationsByKey(
dataDictionary: DataDictionary
): Record<string, DataDictionaryAnnotation> {
return dataDictionary.classes.reduce(
(acc: Record<string, DataDictionaryAnnotation>, cls: Class) => {
// Add class to map.
acc[cls.key] = {
description: cls.description,
label: cls.label,
};

// Add each class attribute to the map.
cls.attributes.forEach((attribute: Attribute) => {
acc[attribute.key] = {
description: attribute.description,
label: attribute.label,
};
});
return acc;
},
{} as Record<string, DataDictionaryAnnotation>
);
}
1 change: 1 addition & 0 deletions src/components/Filter/components/Filter/filter.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,6 +77,7 @@ export const Filter = ({
return (
<>
<FilterLabel
annotation={categoryView.annotation}
count={categoryView.values.length}
disabled={categoryView.isDisabled}
isOpen={isOpen}
Expand Down
26 changes: 16 additions & 10 deletions src/components/Filter/components/FilterLabel/filterLabel.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,11 @@
import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
import React, { MouseEvent } from "react";
import { DataDictionaryAnnotation } from "../../../../common/entities";
import { Tooltip } from "../../../DataDictionary/components/Tooltip/tooltip";
import { FilterLabel as Label } from "./filterLabel.styles";

export interface FilterLabelProps {
annotation?: DataDictionaryAnnotation;
count?: number;
disabled?: boolean;
isOpen: boolean;
Expand All @@ -11,6 +14,7 @@ export interface FilterLabelProps {
}

export const FilterLabel = ({
annotation,
count,
disabled = false,
isOpen,
Expand All @@ -19,15 +23,17 @@ export const FilterLabel = ({
}: FilterLabelProps): JSX.Element => {
const filterLabel = count ? `${label}\xa0(${count})` : label; // When the count is present, a non-breaking space is used to prevent it from being on its own line
return (
<Label
color="inherit"
disabled={disabled}
endIcon={<ArrowDropDownRoundedIcon fontSize="small" />}
fullWidth
isOpen={isOpen}
onClick={onClick}
>
{filterLabel}
</Label>
<Tooltip description={annotation?.description} title={annotation?.label}>
<Label
color="inherit"
disabled={disabled}
endIcon={<ArrowDropDownRoundedIcon fontSize="small" />}
fullWidth
isOpen={isOpen}
onClick={onClick}
>
{filterLabel}
</Label>
</Tooltip>
);
};
2 changes: 2 additions & 0 deletions src/components/Index/components/Tabs/common/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ export function getEntityListTabs(entities: EntityConfig[]): Tab[] {
(
acc: Tab[],
{
annotation,
label,
listView: { enableTab = true } = {},
route,
Expand All @@ -20,6 +21,7 @@ export function getEntityListTabs(entities: EntityConfig[]): Tab[] {
) => {
if (enableTab) {
acc.push({
annotation,
icon,
iconPosition,
label,
Expand Down
41 changes: 26 additions & 15 deletions src/components/Table/components/TableHead/tableHead.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
} from "@mui/material";
import { flexRender, RowData } from "@tanstack/react-table";
import React, { Fragment } from "react";
import { Tooltip } from "../../../DataDictionary/components/Tooltip/tooltip";
import { ROW_DIRECTION } from "../../common/entities";
import {
getTableCellAlign,
Expand All @@ -28,27 +29,37 @@ export const TableHead = <T extends RowData>({
<MTableRow>
{headerGroup.headers.map(({ column, getContext, id }) => {
const { columnDef, getIsGrouped, getIsSorted } = column;
const annotation = columnDef.meta?.annotation;
return getIsGrouped() ? null : (
<TableCell
key={id}
align={getTableCellAlign(column)}
padding={getTableCellPadding(id)}
>
{shouldSortColumn(tableInstance, column) ? (
<TableSortLabel
IconComponent={SouthRoundedIcon}
active={Boolean(getIsSorted())}
direction={getIsSorted() || undefined}
disabled={isSortDisabled(tableInstance)}
onClick={(mouseEvent) =>
handleToggleSorting(mouseEvent, tableInstance, column)
}
>
{flexRender(columnDef.header, getContext())}
</TableSortLabel>
) : (
flexRender(columnDef.header, getContext())
)}
<Tooltip
description={annotation?.description}
title={annotation?.label}
>
{shouldSortColumn(tableInstance, column) ? (
<TableSortLabel
IconComponent={SouthRoundedIcon}
active={Boolean(getIsSorted())}
direction={getIsSorted() || undefined}
disabled={isSortDisabled(tableInstance)}
onClick={(mouseEvent) =>
handleToggleSorting(
mouseEvent,
tableInstance,
column
)
}
>
{flexRender(columnDef.header, getContext())}
</TableSortLabel>
) : (
flexRender(columnDef.header, getContext())
)}
</Tooltip>
</TableCell>
);
})}
Expand Down
Loading

0 comments on commit 849e5cf

Please sign in to comment.