@databiosphere/findable-ui 21.2.0 → 21.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.release-please-manifest.json +1 -1
- package/CHANGELOG.md +7 -0
- package/lib/common/entities.d.ts +33 -0
- package/lib/components/DataDictionary/common/utils.d.ts +38 -0
- package/lib/components/DataDictionary/common/utils.js +122 -0
- package/lib/components/Filter/components/Filter/filter.js +1 -1
- package/lib/components/Filter/components/FilterLabel/filterLabel.d.ts +3 -1
- package/lib/components/Filter/components/FilterLabel/filterLabel.js +4 -2
- package/lib/components/Index/components/Tabs/common/utils.js +2 -1
- package/lib/components/Table/components/TableHead/tableHead.js +4 -1
- package/lib/components/common/Tabs/tabs.d.ts +2 -0
- package/lib/components/common/Tabs/tabs.js +14 -1
- package/lib/config/entities.d.ts +5 -1
- package/lib/hooks/useCategoryFilter.js +1 -0
- package/lib/providers/config.js +9 -2
- package/package.json +1 -1
- package/src/common/entities.ts +37 -0
- package/src/components/DataDictionary/common/utils.ts +160 -0
- package/src/components/Filter/components/Filter/filter.tsx +1 -0
- package/src/components/Filter/components/FilterLabel/filterLabel.tsx +16 -10
- package/src/components/Index/components/Tabs/common/utils.ts +2 -0
- package/src/components/Table/components/TableHead/tableHead.tsx +26 -15
- package/src/components/common/Tabs/tabs.tsx +33 -3
- package/src/config/entities.ts +10 -1
- package/src/hooks/useCategoryFilter.ts +1 -0
- package/src/providers/config.tsx +10 -2
- package/tests/dataDictionary_utils.test.ts +153 -0
- package/types/data-explorer-ui.d.ts +2 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,12 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [21.3.0](https://github.com/DataBiosphere/findable-ui/compare/v21.2.0...v21.3.0) (2025-02-14)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add data dictionary tooltip support [#4131](https://github.com/DataBiosphere/findable-ui/issues/4131) ([#320](https://github.com/DataBiosphere/findable-ui/issues/320)) ([849e5cf](https://github.com/DataBiosphere/findable-ui/commit/849e5cf5898f71be210436863c9b10baf85a8427))
|
|
9
|
+
|
|
3
10
|
## [21.2.0](https://github.com/DataBiosphere/findable-ui/compare/v21.1.1...v21.2.0) (2025-02-12)
|
|
4
11
|
|
|
5
12
|
|
package/lib/common/entities.d.ts
CHANGED
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model of a value of a metadata class.
|
|
3
|
+
*/
|
|
4
|
+
export interface Attribute {
|
|
5
|
+
description: string;
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
1
9
|
/**
|
|
2
10
|
* Filterable metadata keys.
|
|
3
11
|
*/
|
|
@@ -10,10 +18,34 @@ export interface CategoryTag {
|
|
|
10
18
|
onRemove: () => void;
|
|
11
19
|
superseded: boolean;
|
|
12
20
|
}
|
|
21
|
+
/**
|
|
22
|
+
* Model of a metadata class, to be specified manually or built from LinkML schema.
|
|
23
|
+
*/
|
|
24
|
+
export interface Class {
|
|
25
|
+
attributes: Attribute[];
|
|
26
|
+
description: string;
|
|
27
|
+
key: string;
|
|
28
|
+
label: string;
|
|
29
|
+
name: string;
|
|
30
|
+
}
|
|
13
31
|
/**
|
|
14
32
|
* Category values to be used as keys. For example, "Homo sapiens" or "10X 3' v2 sequencing".
|
|
15
33
|
*/
|
|
16
34
|
export type CategoryValueKey = unknown;
|
|
35
|
+
/**
|
|
36
|
+
* Model of a metadata dictionary containing a set of classes and their definitions.
|
|
37
|
+
*/
|
|
38
|
+
export interface DataDictionary {
|
|
39
|
+
classes: Class[];
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Label and description values from a data dictionary that are added to a site
|
|
43
|
+
* config value.
|
|
44
|
+
*/
|
|
45
|
+
export interface DataDictionaryAnnotation {
|
|
46
|
+
description: string;
|
|
47
|
+
label: string;
|
|
48
|
+
}
|
|
17
49
|
/**
|
|
18
50
|
* Set of selected category values.
|
|
19
51
|
*/
|
|
@@ -63,6 +95,7 @@ export interface SelectCategoryValueView {
|
|
|
63
95
|
* View model of category, for multiselect categories.
|
|
64
96
|
*/
|
|
65
97
|
export interface SelectCategoryView {
|
|
98
|
+
annotation?: DataDictionaryAnnotation;
|
|
66
99
|
isDisabled?: boolean;
|
|
67
100
|
key: CategoryKey;
|
|
68
101
|
label: string;
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import { DataDictionaryAnnotation } from "../../../common/entities";
|
|
2
|
+
import { SiteConfig } from "../../../config/entities";
|
|
3
|
+
/**
|
|
4
|
+
* Annotate each entity column configuration with data dictionary values. Specifically,
|
|
5
|
+
* look up label and description for each column key.
|
|
6
|
+
* @param siteConfig - Site configuration to annotate.
|
|
7
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
8
|
+
*/
|
|
9
|
+
export declare function annotateColumnConfig(siteConfig: SiteConfig, annotationsByKey: Record<string, DataDictionaryAnnotation>): void;
|
|
10
|
+
/**
|
|
11
|
+
* Annotate filter and colummn configuration with data dictionary values. Note this
|
|
12
|
+
* functionality mutates the site config. A possible future improvement would be to
|
|
13
|
+
* create either a specific "raw" or "annotated" type to indicate clearly the point
|
|
14
|
+
* at which the config has been annotated.
|
|
15
|
+
* @param siteConfig - The site configuration to annotate.
|
|
16
|
+
*/
|
|
17
|
+
export declare function annotateSiteConfig(siteConfig: SiteConfig): void;
|
|
18
|
+
/**
|
|
19
|
+
* Annotate entity configuration with data dictionary values. Specifically, look
|
|
20
|
+
* up label and description for each entity key.
|
|
21
|
+
* @param siteConfig - The site configuration to annotate.
|
|
22
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
23
|
+
*/
|
|
24
|
+
export declare function annotateEntityConfig(siteConfig: SiteConfig, annotationsByKey: Record<string, DataDictionaryAnnotation>): void;
|
|
25
|
+
/**
|
|
26
|
+
* Annotate top-level (app-wide) category config with data dictionary values.
|
|
27
|
+
* Specifically, look up label and description for each filter key.
|
|
28
|
+
* @param siteConfig - Site configuration to annotate.
|
|
29
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
30
|
+
*/
|
|
31
|
+
export declare function annotateDefaultCategoryConfig(siteConfig: SiteConfig, annotationsByKey: Record<string, DataDictionaryAnnotation>): void;
|
|
32
|
+
/**
|
|
33
|
+
* Annotate entity-specific category config with data dictionary values. Specifically,
|
|
34
|
+
* look up label and description for each category key.
|
|
35
|
+
* @param siteConfig - Site configuration to annotate.
|
|
36
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
37
|
+
*/
|
|
38
|
+
export declare function annotateEntityCategoryConfig(siteConfig: SiteConfig, annotationsByKey: Record<string, DataDictionaryAnnotation>): void;
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Annotate each entity column configuration with data dictionary values. Specifically,
|
|
3
|
+
* look up label and description for each column key.
|
|
4
|
+
* @param siteConfig - Site configuration to annotate.
|
|
5
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
6
|
+
*/
|
|
7
|
+
export function annotateColumnConfig(siteConfig, annotationsByKey) {
|
|
8
|
+
// Annotate every column in every entity.
|
|
9
|
+
siteConfig.entities.forEach((entity) => {
|
|
10
|
+
entity.list.columns.forEach((columnConfig) => {
|
|
11
|
+
// Find the annotation for the column key.
|
|
12
|
+
const annotation = annotationsByKey[columnConfig.id];
|
|
13
|
+
if (!annotation) {
|
|
14
|
+
return;
|
|
15
|
+
}
|
|
16
|
+
if (!columnConfig.meta) {
|
|
17
|
+
columnConfig.meta = {};
|
|
18
|
+
}
|
|
19
|
+
columnConfig.meta.annotation = annotation;
|
|
20
|
+
});
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Annotate filter and colummn configuration with data dictionary values. Note this
|
|
25
|
+
* functionality mutates the site config. A possible future improvement would be to
|
|
26
|
+
* create either a specific "raw" or "annotated" type to indicate clearly the point
|
|
27
|
+
* at which the config has been annotated.
|
|
28
|
+
* @param siteConfig - The site configuration to annotate.
|
|
29
|
+
*/
|
|
30
|
+
export function annotateSiteConfig(siteConfig) {
|
|
31
|
+
// Build and map data dictionary annotations by key.
|
|
32
|
+
const { dataDictionary } = siteConfig;
|
|
33
|
+
if (!dataDictionary) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
const annotationsByKey = keyAnnotationsByKey(dataDictionary);
|
|
37
|
+
// Annotate elements of site config.
|
|
38
|
+
annotateEntityConfig(siteConfig, annotationsByKey);
|
|
39
|
+
annotateDefaultCategoryConfig(siteConfig, annotationsByKey);
|
|
40
|
+
annotateEntityCategoryConfig(siteConfig, annotationsByKey);
|
|
41
|
+
annotateColumnConfig(siteConfig, annotationsByKey);
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Annotate entity configuration with data dictionary values. Specifically, look
|
|
45
|
+
* up label and description for each entity key.
|
|
46
|
+
* @param siteConfig - The site configuration to annotate.
|
|
47
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
48
|
+
*/
|
|
49
|
+
export function annotateEntityConfig(siteConfig, annotationsByKey) {
|
|
50
|
+
// Annotate every entity.
|
|
51
|
+
siteConfig.entities.forEach((entityConfig) => {
|
|
52
|
+
// Check entity for a data dictionary key.
|
|
53
|
+
const { key } = entityConfig;
|
|
54
|
+
if (!key) {
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
// Find corresponding annotation for the key and set on entity config.
|
|
58
|
+
entityConfig.annotation = annotationsByKey[key];
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Annotate top-level (app-wide) category config with data dictionary values.
|
|
63
|
+
* Specifically, look up label and description for each filter key.
|
|
64
|
+
* @param siteConfig - Site configuration to annotate.
|
|
65
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
66
|
+
*/
|
|
67
|
+
export function annotateDefaultCategoryConfig(siteConfig, annotationsByKey) {
|
|
68
|
+
const { categoryGroupConfig } = siteConfig;
|
|
69
|
+
if (categoryGroupConfig) {
|
|
70
|
+
annotateCategoryGroupConfig(categoryGroupConfig, annotationsByKey);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Annotate entity-specific category config with data dictionary values. Specifically,
|
|
75
|
+
* look up label and description for each category key.
|
|
76
|
+
* @param siteConfig - Site configuration to annotate.
|
|
77
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
78
|
+
*/
|
|
79
|
+
export function annotateEntityCategoryConfig(siteConfig, annotationsByKey) {
|
|
80
|
+
// Annotate every category in every entity.
|
|
81
|
+
siteConfig.entities.forEach((entityConfig) => {
|
|
82
|
+
const { categoryGroupConfig } = entityConfig;
|
|
83
|
+
if (categoryGroupConfig) {
|
|
84
|
+
annotateCategoryGroupConfig(categoryGroupConfig, annotationsByKey);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Annonate category group configuration with data dictionary values.
|
|
90
|
+
* @param categoryGroupConfig - Category group to annotate.
|
|
91
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
92
|
+
*/
|
|
93
|
+
function annotateCategoryGroupConfig(categoryGroupConfig, annotationsByKey) {
|
|
94
|
+
categoryGroupConfig.categoryGroups.forEach((categoryGroup) => {
|
|
95
|
+
categoryGroup.categoryConfigs.forEach((categorConfig) => {
|
|
96
|
+
categorConfig.annotation = annotationsByKey[categorConfig.key];
|
|
97
|
+
});
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Transform a data dictionary into a key-annotation map. Build annotations for both
|
|
102
|
+
* classes and attributes and add to map.
|
|
103
|
+
* @param dataDictionary - Data dictionary to transform into a key-annotation map.
|
|
104
|
+
* @returns Key-annotation map.
|
|
105
|
+
*/
|
|
106
|
+
function keyAnnotationsByKey(dataDictionary) {
|
|
107
|
+
return dataDictionary.classes.reduce((acc, cls) => {
|
|
108
|
+
// Add class to map.
|
|
109
|
+
acc[cls.key] = {
|
|
110
|
+
description: cls.description,
|
|
111
|
+
label: cls.label,
|
|
112
|
+
};
|
|
113
|
+
// Add each class attribute to the map.
|
|
114
|
+
cls.attributes.forEach((attribute) => {
|
|
115
|
+
acc[attribute.key] = {
|
|
116
|
+
description: attribute.description,
|
|
117
|
+
label: attribute.label,
|
|
118
|
+
};
|
|
119
|
+
});
|
|
120
|
+
return acc;
|
|
121
|
+
}, {});
|
|
122
|
+
}
|
|
@@ -48,7 +48,7 @@ export const Filter = ({ categorySection, categoryView, closeAncestor, isFilterD
|
|
|
48
48
|
trackFilterOpened?.({ category: categoryView.key });
|
|
49
49
|
};
|
|
50
50
|
return (React.createElement(React.Fragment, null,
|
|
51
|
-
React.createElement(FilterLabel, { count: categoryView.values.length, disabled: categoryView.isDisabled, isOpen: isOpen, label: categoryView.label, onClick: onOpenFilter }),
|
|
51
|
+
React.createElement(FilterLabel, { annotation: categoryView.annotation, count: categoryView.values.length, disabled: categoryView.isDisabled, isOpen: isOpen, label: categoryView.label, onClick: onOpenFilter }),
|
|
52
52
|
React.createElement(FilterPopover, { anchorPosition: anchorPosition, anchorReference: "anchorPosition", marginThreshold: 0, onClose: onCloseFilters, open: isOpen, slotProps: slotProps, TransitionComponent: TransitionComponent, transitionDuration: TransitionDuration },
|
|
53
53
|
isOpen && isFilterDrawer && (React.createElement(IconButton, { Icon: CloseRounded, onClick: onCloseFilters, size: "medium" })),
|
|
54
54
|
React.createElement(FilterMenu, { categorySection: categorySection, categoryKey: categoryView.key, categoryLabel: categoryView.label, isFilterDrawer: isFilterDrawer, onFilter: onFilter, onCloseFilter: onCloseFilter, values: categoryView.values })),
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { MouseEvent } from "react";
|
|
2
|
+
import { DataDictionaryAnnotation } from "../../../../common/entities";
|
|
2
3
|
export interface FilterLabelProps {
|
|
4
|
+
annotation?: DataDictionaryAnnotation;
|
|
3
5
|
count?: number;
|
|
4
6
|
disabled?: boolean;
|
|
5
7
|
isOpen: boolean;
|
|
6
8
|
label: string;
|
|
7
9
|
onClick: (event: MouseEvent<HTMLButtonElement>) => void;
|
|
8
10
|
}
|
|
9
|
-
export declare const FilterLabel: ({ count, disabled, isOpen, label, onClick, }: FilterLabelProps) => JSX.Element;
|
|
11
|
+
export declare const FilterLabel: ({ annotation, count, disabled, isOpen, label, onClick, }: FilterLabelProps) => JSX.Element;
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
|
|
2
2
|
import React from "react";
|
|
3
|
+
import { Tooltip } from "../../../DataDictionary/components/Tooltip/tooltip";
|
|
3
4
|
import { FilterLabel as Label } from "./filterLabel.styles";
|
|
4
|
-
export const FilterLabel = ({ count, disabled = false, isOpen, label, onClick, }) => {
|
|
5
|
+
export const FilterLabel = ({ annotation, count, disabled = false, isOpen, label, onClick, }) => {
|
|
5
6
|
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
|
|
6
|
-
return (React.createElement(
|
|
7
|
+
return (React.createElement(Tooltip, { description: annotation?.description, title: annotation?.label },
|
|
8
|
+
React.createElement(Label, { color: "inherit", disabled: disabled, endIcon: React.createElement(ArrowDropDownRoundedIcon, { fontSize: "small" }), fullWidth: true, isOpen: isOpen, onClick: onClick }, filterLabel)));
|
|
7
9
|
};
|
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* @returns tabs list.
|
|
5
5
|
*/
|
|
6
6
|
export function getEntityListTabs(entities) {
|
|
7
|
-
return entities.reduce((acc, { label, listView: { enableTab = true } = {}, route, tabIcon: icon, tabIconPosition: iconPosition, }) => {
|
|
7
|
+
return entities.reduce((acc, { annotation, label, listView: { enableTab = true } = {}, route, tabIcon: icon, tabIconPosition: iconPosition, }) => {
|
|
8
8
|
if (enableTab) {
|
|
9
9
|
acc.push({
|
|
10
|
+
annotation,
|
|
10
11
|
icon,
|
|
11
12
|
iconPosition,
|
|
12
13
|
label,
|
|
@@ -2,6 +2,7 @@ import SouthRoundedIcon from "@mui/icons-material/SouthRounded";
|
|
|
2
2
|
import { TableHead as MTableHead, TableRow as MTableRow, TableCell, TableSortLabel, } from "@mui/material";
|
|
3
3
|
import { flexRender } from "@tanstack/react-table";
|
|
4
4
|
import React, { Fragment } from "react";
|
|
5
|
+
import { Tooltip } from "../../../DataDictionary/components/Tooltip/tooltip";
|
|
5
6
|
import { ROW_DIRECTION } from "../../common/entities";
|
|
6
7
|
import { getTableCellAlign, getTableCellPadding, } from "../TableCell/common/utils";
|
|
7
8
|
import { handleToggleSorting } from "../TableFeatures/RowSorting/utils";
|
|
@@ -11,6 +12,8 @@ export const TableHead = ({ rowDirection, tableInstance, }) => {
|
|
|
11
12
|
tableInstance.getHeaderGroups().map((headerGroup) => (React.createElement(MTableHead, { key: headerGroup.id },
|
|
12
13
|
React.createElement(MTableRow, null, headerGroup.headers.map(({ column, getContext, id }) => {
|
|
13
14
|
const { columnDef, getIsGrouped, getIsSorted } = column;
|
|
14
|
-
|
|
15
|
+
const annotation = columnDef.meta?.annotation;
|
|
16
|
+
return getIsGrouped() ? null : (React.createElement(TableCell, { key: id, align: getTableCellAlign(column), padding: getTableCellPadding(id) },
|
|
17
|
+
React.createElement(Tooltip, { description: annotation?.description, title: annotation?.label }, shouldSortColumn(tableInstance, column) ? (React.createElement(TableSortLabel, { IconComponent: SouthRoundedIcon, active: Boolean(getIsSorted()), direction: getIsSorted() || undefined, disabled: isSortDisabled(tableInstance), onClick: (mouseEvent) => handleToggleSorting(mouseEvent, tableInstance, column) }, flexRender(columnDef.header, getContext()))) : (flexRender(columnDef.header, getContext())))));
|
|
15
18
|
})))))));
|
|
16
19
|
};
|
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { TabProps as MTabProps, TabsProps as MTabsProps } from "@mui/material";
|
|
2
2
|
import { ReactNode } from "react";
|
|
3
|
+
import { DataDictionaryAnnotation } from "../../../common/entities";
|
|
3
4
|
export type TabsValue = MTabsProps["value"];
|
|
4
5
|
export type TabValue = MTabProps["value"];
|
|
5
6
|
export type OnTabChangeFn = (tabValue: TabValue) => void;
|
|
6
7
|
export interface Tab {
|
|
8
|
+
annotation?: DataDictionaryAnnotation;
|
|
7
9
|
count?: string;
|
|
8
10
|
icon?: MTabProps["icon"];
|
|
9
11
|
iconPosition?: MTabProps["iconPosition"];
|
|
@@ -1,6 +1,19 @@
|
|
|
1
1
|
import { Tabs as MTabs, } from "@mui/material";
|
|
2
2
|
import React from "react";
|
|
3
|
+
import { Tooltip } from "../../DataDictionary/components/Tooltip/tooltip";
|
|
3
4
|
import { Tab, TabScrollFuzz } from "./tabs.styles";
|
|
4
5
|
export const Tabs = ({ className, onTabChange, tabs, value, }) => {
|
|
5
|
-
return (React.createElement(MTabs, { allowScrollButtonsMobile: true, className: className, onChange: (_, tabValue) => onTabChange(tabValue), ScrollButtonComponent: TabScrollFuzz, value: value }, tabs.map(({ count, icon, iconPosition = "start", label, value: tabValue }, t) => (React.createElement(Tab, { icon: icon, iconPosition: icon ? iconPosition : undefined, key: `${label}${t}`, label:
|
|
6
|
+
return (React.createElement(MTabs, { allowScrollButtonsMobile: true, className: className, onChange: (_, tabValue) => onTabChange(tabValue), ScrollButtonComponent: TabScrollFuzz, value: value }, tabs.map(({ annotation, count, icon, iconPosition = "start", label, value: tabValue, }, t) => (React.createElement(Tab, { icon: icon, iconPosition: icon ? iconPosition : undefined, key: `${label}${t}`, label: buildTabLabel(label, count, annotation), value: tabValue })))));
|
|
6
7
|
};
|
|
8
|
+
/**
|
|
9
|
+
* Build a tab value from a tab config. Specifically, display the tab label
|
|
10
|
+
* with a tooltip annotation if necessary.
|
|
11
|
+
* @param label - Tab display value.
|
|
12
|
+
* @param count - Optional count to display next to the tab label.
|
|
13
|
+
* @param annotation - Data dictionary annotation.
|
|
14
|
+
* @returns Tab label with optional count and tooltip.
|
|
15
|
+
*/
|
|
16
|
+
function buildTabLabel(label, count, annotation) {
|
|
17
|
+
return (React.createElement(Tooltip, { description: annotation?.description, title: annotation?.label },
|
|
18
|
+
React.createElement("span", null, count ? `${label} (${count})` : label)));
|
|
19
|
+
}
|
package/lib/config/entities.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { TabProps as MTabProps, Theme, ThemeOptions } from "@mui/material";
|
|
2
2
|
import { CellContext, ColumnDef, ColumnMeta, ColumnSort, GroupingState, RowData, Table, TableOptions } from "@tanstack/react-table";
|
|
3
3
|
import { JSXElementConstructor, ReactNode } from "react";
|
|
4
|
-
import { SelectCategoryValueView, SelectedFilter } from "../common/entities";
|
|
4
|
+
import { DataDictionary, DataDictionaryAnnotation, SelectCategoryValueView, SelectedFilter } from "../common/entities";
|
|
5
5
|
import { HeroTitle } from "../components/common/Title/title";
|
|
6
6
|
import { FooterProps } from "../components/Layout/components/Footer/footer";
|
|
7
7
|
import { HeaderProps } from "../components/Layout/components/Header/header";
|
|
@@ -70,6 +70,7 @@ export interface CategoryGroup {
|
|
|
70
70
|
* Model of category configured in site config.
|
|
71
71
|
*/
|
|
72
72
|
export interface CategoryConfig {
|
|
73
|
+
annotation?: DataDictionaryAnnotation;
|
|
73
74
|
key: string;
|
|
74
75
|
label: string;
|
|
75
76
|
mapSelectCategoryValue?: (selectCategoryValue: SelectCategoryValueView) => SelectCategoryValueView;
|
|
@@ -127,6 +128,7 @@ export type EntityPath = string;
|
|
|
127
128
|
* the detail and the list page configuration.
|
|
128
129
|
*/
|
|
129
130
|
export interface EntityConfig<T = any, I = any> extends TabConfig {
|
|
131
|
+
annotation?: DataDictionaryAnnotation;
|
|
130
132
|
apiPath?: EntityPath;
|
|
131
133
|
categoryGroupConfig?: CategoryGroupConfig;
|
|
132
134
|
detail: BackPageConfig;
|
|
@@ -137,6 +139,7 @@ export interface EntityConfig<T = any, I = any> extends TabConfig {
|
|
|
137
139
|
getId?: GetIdFunction<T>;
|
|
138
140
|
getTitle?: GetTitleFunction<T>;
|
|
139
141
|
hideTabs?: boolean;
|
|
142
|
+
key?: string;
|
|
140
143
|
list: ListConfig<T>;
|
|
141
144
|
listView?: ListViewConfig;
|
|
142
145
|
options?: Options;
|
|
@@ -295,6 +298,7 @@ export interface SiteConfig {
|
|
|
295
298
|
categoryGroupConfig?: CategoryGroupConfig;
|
|
296
299
|
contentDir?: string;
|
|
297
300
|
contentThemeOptionsFn?: ThemeOptionsFn;
|
|
301
|
+
dataDictionary?: DataDictionary;
|
|
298
302
|
dataSource: DataSourceConfig;
|
|
299
303
|
entities: EntityConfig[];
|
|
300
304
|
explorerTitle: HeroTitle;
|
|
@@ -27,6 +27,7 @@ function buildCategoryView(category, categoryValueViews, categoryConfigs) {
|
|
|
27
27
|
const categoryConfig = findCategoryConfig(category.key, categoryConfigs);
|
|
28
28
|
const mapSelectCategoryValue = categoryConfig?.mapSelectCategoryValue || getSelectCategoryValue;
|
|
29
29
|
return {
|
|
30
|
+
annotation: categoryConfig?.annotation,
|
|
30
31
|
isDisabled: false,
|
|
31
32
|
key: category.key,
|
|
32
33
|
label: getCategoryLabel(category.key, categoryConfig),
|
package/lib/providers/config.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, { createContext } from "react";
|
|
1
|
+
import React, { createContext, useState } from "react";
|
|
2
|
+
import { annotateSiteConfig } from "../components/DataDictionary/common/utils";
|
|
2
3
|
import { getDefaultConfig, getDefaultEntityConfig, getEntityConfig, } from "../config/utils";
|
|
3
4
|
export const ConfigContext = createContext({
|
|
4
5
|
config: getDefaultConfig(),
|
|
@@ -7,7 +8,13 @@ export const ConfigContext = createContext({
|
|
|
7
8
|
entityListType: "",
|
|
8
9
|
});
|
|
9
10
|
export function ConfigProvider({ children, config, entityListType = "", }) {
|
|
10
|
-
|
|
11
|
+
// Annote config on init. Note config is mutated but using state here to
|
|
12
|
+
// ensure annotated config is calculated once and is used rather than the raw config.
|
|
13
|
+
const [annotatedConfig] = useState(() => {
|
|
14
|
+
annotateSiteConfig(config);
|
|
15
|
+
return config;
|
|
16
|
+
});
|
|
17
|
+
const { entities } = annotatedConfig;
|
|
11
18
|
const defaultEntityListType = config.redirectRootToPath.slice(1);
|
|
12
19
|
const entityName = entityListType || defaultEntityListType;
|
|
13
20
|
const entityConfig = getEntityConfig(entities, entityName);
|
package/package.json
CHANGED
package/src/common/entities.ts
CHANGED
|
@@ -1,3 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Model of a value of a metadata class.
|
|
3
|
+
*/
|
|
4
|
+
export interface Attribute {
|
|
5
|
+
description: string;
|
|
6
|
+
key: string;
|
|
7
|
+
label: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
1
10
|
/**
|
|
2
11
|
* Filterable metadata keys.
|
|
3
12
|
*/
|
|
@@ -12,11 +21,38 @@ export interface CategoryTag {
|
|
|
12
21
|
superseded: boolean;
|
|
13
22
|
}
|
|
14
23
|
|
|
24
|
+
/**
|
|
25
|
+
* Model of a metadata class, to be specified manually or built from LinkML schema.
|
|
26
|
+
*/
|
|
27
|
+
export interface Class {
|
|
28
|
+
attributes: Attribute[];
|
|
29
|
+
description: string;
|
|
30
|
+
key: string;
|
|
31
|
+
label: string;
|
|
32
|
+
name: string;
|
|
33
|
+
}
|
|
34
|
+
|
|
15
35
|
/**
|
|
16
36
|
* Category values to be used as keys. For example, "Homo sapiens" or "10X 3' v2 sequencing".
|
|
17
37
|
*/
|
|
18
38
|
export type CategoryValueKey = unknown;
|
|
19
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Model of a metadata dictionary containing a set of classes and their definitions.
|
|
42
|
+
*/
|
|
43
|
+
export interface DataDictionary {
|
|
44
|
+
classes: Class[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Label and description values from a data dictionary that are added to a site
|
|
49
|
+
* config value.
|
|
50
|
+
*/
|
|
51
|
+
export interface DataDictionaryAnnotation {
|
|
52
|
+
description: string;
|
|
53
|
+
label: string;
|
|
54
|
+
}
|
|
55
|
+
|
|
20
56
|
/**
|
|
21
57
|
* Set of selected category values.
|
|
22
58
|
*/
|
|
@@ -72,6 +108,7 @@ export interface SelectCategoryValueView {
|
|
|
72
108
|
* View model of category, for multiselect categories.
|
|
73
109
|
*/
|
|
74
110
|
export interface SelectCategoryView {
|
|
111
|
+
annotation?: DataDictionaryAnnotation;
|
|
75
112
|
isDisabled?: boolean;
|
|
76
113
|
key: CategoryKey;
|
|
77
114
|
label: string;
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Attribute,
|
|
3
|
+
Class,
|
|
4
|
+
DataDictionary,
|
|
5
|
+
DataDictionaryAnnotation,
|
|
6
|
+
} from "../../../common/entities";
|
|
7
|
+
import { CategoryGroupConfig, SiteConfig } from "../../../config/entities";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Annotate each entity column configuration with data dictionary values. Specifically,
|
|
11
|
+
* look up label and description for each column key.
|
|
12
|
+
* @param siteConfig - Site configuration to annotate.
|
|
13
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
14
|
+
*/
|
|
15
|
+
export function annotateColumnConfig(
|
|
16
|
+
siteConfig: SiteConfig,
|
|
17
|
+
annotationsByKey: Record<string, DataDictionaryAnnotation>
|
|
18
|
+
): void {
|
|
19
|
+
// Annotate every column in every entity.
|
|
20
|
+
siteConfig.entities.forEach((entity) => {
|
|
21
|
+
entity.list.columns.forEach((columnConfig) => {
|
|
22
|
+
// Find the annotation for the column key.
|
|
23
|
+
const annotation = annotationsByKey[columnConfig.id];
|
|
24
|
+
if (!annotation) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
if (!columnConfig.meta) {
|
|
29
|
+
columnConfig.meta = {};
|
|
30
|
+
}
|
|
31
|
+
columnConfig.meta.annotation = annotation;
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Annotate filter and colummn configuration with data dictionary values. Note this
|
|
38
|
+
* functionality mutates the site config. A possible future improvement would be to
|
|
39
|
+
* create either a specific "raw" or "annotated" type to indicate clearly the point
|
|
40
|
+
* at which the config has been annotated.
|
|
41
|
+
* @param siteConfig - The site configuration to annotate.
|
|
42
|
+
*/
|
|
43
|
+
export function annotateSiteConfig(siteConfig: SiteConfig): void {
|
|
44
|
+
// Build and map data dictionary annotations by key.
|
|
45
|
+
const { dataDictionary } = siteConfig;
|
|
46
|
+
if (!dataDictionary) {
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
const annotationsByKey = keyAnnotationsByKey(dataDictionary);
|
|
50
|
+
|
|
51
|
+
// Annotate elements of site config.
|
|
52
|
+
annotateEntityConfig(siteConfig, annotationsByKey);
|
|
53
|
+
annotateDefaultCategoryConfig(siteConfig, annotationsByKey);
|
|
54
|
+
annotateEntityCategoryConfig(siteConfig, annotationsByKey);
|
|
55
|
+
annotateColumnConfig(siteConfig, annotationsByKey);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Annotate entity configuration with data dictionary values. Specifically, look
|
|
60
|
+
* up label and description for each entity key.
|
|
61
|
+
* @param siteConfig - The site configuration to annotate.
|
|
62
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
63
|
+
*/
|
|
64
|
+
export function annotateEntityConfig(
|
|
65
|
+
siteConfig: SiteConfig,
|
|
66
|
+
annotationsByKey: Record<string, DataDictionaryAnnotation>
|
|
67
|
+
): void {
|
|
68
|
+
// Annotate every entity.
|
|
69
|
+
siteConfig.entities.forEach((entityConfig) => {
|
|
70
|
+
// Check entity for a data dictionary key.
|
|
71
|
+
const { key } = entityConfig;
|
|
72
|
+
if (!key) {
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Find corresponding annotation for the key and set on entity config.
|
|
77
|
+
entityConfig.annotation = annotationsByKey[key];
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Annotate top-level (app-wide) category config with data dictionary values.
|
|
83
|
+
* Specifically, look up label and description for each filter key.
|
|
84
|
+
* @param siteConfig - Site configuration to annotate.
|
|
85
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
86
|
+
*/
|
|
87
|
+
export function annotateDefaultCategoryConfig(
|
|
88
|
+
siteConfig: SiteConfig,
|
|
89
|
+
annotationsByKey: Record<string, DataDictionaryAnnotation>
|
|
90
|
+
): void {
|
|
91
|
+
const { categoryGroupConfig } = siteConfig;
|
|
92
|
+
if (categoryGroupConfig) {
|
|
93
|
+
annotateCategoryGroupConfig(categoryGroupConfig, annotationsByKey);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Annotate entity-specific category config with data dictionary values. Specifically,
|
|
99
|
+
* look up label and description for each category key.
|
|
100
|
+
* @param siteConfig - Site configuration to annotate.
|
|
101
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
102
|
+
*/
|
|
103
|
+
export function annotateEntityCategoryConfig(
|
|
104
|
+
siteConfig: SiteConfig,
|
|
105
|
+
annotationsByKey: Record<string, DataDictionaryAnnotation>
|
|
106
|
+
): void {
|
|
107
|
+
// Annotate every category in every entity.
|
|
108
|
+
siteConfig.entities.forEach((entityConfig) => {
|
|
109
|
+
const { categoryGroupConfig } = entityConfig;
|
|
110
|
+
if (categoryGroupConfig) {
|
|
111
|
+
annotateCategoryGroupConfig(categoryGroupConfig, annotationsByKey);
|
|
112
|
+
}
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Annonate category group configuration with data dictionary values.
|
|
118
|
+
* @param categoryGroupConfig - Category group to annotate.
|
|
119
|
+
* @param annotationsByKey - Data dictionary annotations keyed by key.
|
|
120
|
+
*/
|
|
121
|
+
function annotateCategoryGroupConfig(
|
|
122
|
+
categoryGroupConfig: CategoryGroupConfig,
|
|
123
|
+
annotationsByKey: Record<string, DataDictionaryAnnotation>
|
|
124
|
+
): void {
|
|
125
|
+
categoryGroupConfig.categoryGroups.forEach((categoryGroup) => {
|
|
126
|
+
categoryGroup.categoryConfigs.forEach((categorConfig) => {
|
|
127
|
+
categorConfig.annotation = annotationsByKey[categorConfig.key];
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Transform a data dictionary into a key-annotation map. Build annotations for both
|
|
134
|
+
* classes and attributes and add to map.
|
|
135
|
+
* @param dataDictionary - Data dictionary to transform into a key-annotation map.
|
|
136
|
+
* @returns Key-annotation map.
|
|
137
|
+
*/
|
|
138
|
+
function keyAnnotationsByKey(
|
|
139
|
+
dataDictionary: DataDictionary
|
|
140
|
+
): Record<string, DataDictionaryAnnotation> {
|
|
141
|
+
return dataDictionary.classes.reduce(
|
|
142
|
+
(acc: Record<string, DataDictionaryAnnotation>, cls: Class) => {
|
|
143
|
+
// Add class to map.
|
|
144
|
+
acc[cls.key] = {
|
|
145
|
+
description: cls.description,
|
|
146
|
+
label: cls.label,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Add each class attribute to the map.
|
|
150
|
+
cls.attributes.forEach((attribute: Attribute) => {
|
|
151
|
+
acc[attribute.key] = {
|
|
152
|
+
description: attribute.description,
|
|
153
|
+
label: attribute.label,
|
|
154
|
+
};
|
|
155
|
+
});
|
|
156
|
+
return acc;
|
|
157
|
+
},
|
|
158
|
+
{} as Record<string, DataDictionaryAnnotation>
|
|
159
|
+
);
|
|
160
|
+
}
|
|
@@ -1,8 +1,11 @@
|
|
|
1
1
|
import ArrowDropDownRoundedIcon from "@mui/icons-material/ArrowDropDownRounded";
|
|
2
2
|
import React, { MouseEvent } from "react";
|
|
3
|
+
import { DataDictionaryAnnotation } from "../../../../common/entities";
|
|
4
|
+
import { Tooltip } from "../../../DataDictionary/components/Tooltip/tooltip";
|
|
3
5
|
import { FilterLabel as Label } from "./filterLabel.styles";
|
|
4
6
|
|
|
5
7
|
export interface FilterLabelProps {
|
|
8
|
+
annotation?: DataDictionaryAnnotation;
|
|
6
9
|
count?: number;
|
|
7
10
|
disabled?: boolean;
|
|
8
11
|
isOpen: boolean;
|
|
@@ -11,6 +14,7 @@ export interface FilterLabelProps {
|
|
|
11
14
|
}
|
|
12
15
|
|
|
13
16
|
export const FilterLabel = ({
|
|
17
|
+
annotation,
|
|
14
18
|
count,
|
|
15
19
|
disabled = false,
|
|
16
20
|
isOpen,
|
|
@@ -19,15 +23,17 @@ export const FilterLabel = ({
|
|
|
19
23
|
}: FilterLabelProps): JSX.Element => {
|
|
20
24
|
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
|
|
21
25
|
return (
|
|
22
|
-
<
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
26
|
+
<Tooltip description={annotation?.description} title={annotation?.label}>
|
|
27
|
+
<Label
|
|
28
|
+
color="inherit"
|
|
29
|
+
disabled={disabled}
|
|
30
|
+
endIcon={<ArrowDropDownRoundedIcon fontSize="small" />}
|
|
31
|
+
fullWidth
|
|
32
|
+
isOpen={isOpen}
|
|
33
|
+
onClick={onClick}
|
|
34
|
+
>
|
|
35
|
+
{filterLabel}
|
|
36
|
+
</Label>
|
|
37
|
+
</Tooltip>
|
|
32
38
|
);
|
|
33
39
|
};
|
|
@@ -11,6 +11,7 @@ export function getEntityListTabs(entities: EntityConfig[]): Tab[] {
|
|
|
11
11
|
(
|
|
12
12
|
acc: Tab[],
|
|
13
13
|
{
|
|
14
|
+
annotation,
|
|
14
15
|
label,
|
|
15
16
|
listView: { enableTab = true } = {},
|
|
16
17
|
route,
|
|
@@ -20,6 +21,7 @@ export function getEntityListTabs(entities: EntityConfig[]): Tab[] {
|
|
|
20
21
|
) => {
|
|
21
22
|
if (enableTab) {
|
|
22
23
|
acc.push({
|
|
24
|
+
annotation,
|
|
23
25
|
icon,
|
|
24
26
|
iconPosition,
|
|
25
27
|
label,
|
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
} from "@mui/material";
|
|
8
8
|
import { flexRender, RowData } from "@tanstack/react-table";
|
|
9
9
|
import React, { Fragment } from "react";
|
|
10
|
+
import { Tooltip } from "../../../DataDictionary/components/Tooltip/tooltip";
|
|
10
11
|
import { ROW_DIRECTION } from "../../common/entities";
|
|
11
12
|
import {
|
|
12
13
|
getTableCellAlign,
|
|
@@ -28,27 +29,37 @@ export const TableHead = <T extends RowData>({
|
|
|
28
29
|
<MTableRow>
|
|
29
30
|
{headerGroup.headers.map(({ column, getContext, id }) => {
|
|
30
31
|
const { columnDef, getIsGrouped, getIsSorted } = column;
|
|
32
|
+
const annotation = columnDef.meta?.annotation;
|
|
31
33
|
return getIsGrouped() ? null : (
|
|
32
34
|
<TableCell
|
|
33
35
|
key={id}
|
|
34
36
|
align={getTableCellAlign(column)}
|
|
35
37
|
padding={getTableCellPadding(id)}
|
|
36
38
|
>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
39
|
+
<Tooltip
|
|
40
|
+
description={annotation?.description}
|
|
41
|
+
title={annotation?.label}
|
|
42
|
+
>
|
|
43
|
+
{shouldSortColumn(tableInstance, column) ? (
|
|
44
|
+
<TableSortLabel
|
|
45
|
+
IconComponent={SouthRoundedIcon}
|
|
46
|
+
active={Boolean(getIsSorted())}
|
|
47
|
+
direction={getIsSorted() || undefined}
|
|
48
|
+
disabled={isSortDisabled(tableInstance)}
|
|
49
|
+
onClick={(mouseEvent) =>
|
|
50
|
+
handleToggleSorting(
|
|
51
|
+
mouseEvent,
|
|
52
|
+
tableInstance,
|
|
53
|
+
column
|
|
54
|
+
)
|
|
55
|
+
}
|
|
56
|
+
>
|
|
57
|
+
{flexRender(columnDef.header, getContext())}
|
|
58
|
+
</TableSortLabel>
|
|
59
|
+
) : (
|
|
60
|
+
flexRender(columnDef.header, getContext())
|
|
61
|
+
)}
|
|
62
|
+
</Tooltip>
|
|
52
63
|
</TableCell>
|
|
53
64
|
);
|
|
54
65
|
})}
|
|
@@ -3,7 +3,9 @@ import {
|
|
|
3
3
|
Tabs as MTabs,
|
|
4
4
|
TabsProps as MTabsProps,
|
|
5
5
|
} from "@mui/material";
|
|
6
|
-
import React, { ReactNode } from "react";
|
|
6
|
+
import React, { ReactElement, ReactNode } from "react";
|
|
7
|
+
import { DataDictionaryAnnotation } from "../../../common/entities";
|
|
8
|
+
import { Tooltip } from "../../DataDictionary/components/Tooltip/tooltip";
|
|
7
9
|
import { Tab, TabScrollFuzz } from "./tabs.styles";
|
|
8
10
|
|
|
9
11
|
export type TabsValue = MTabsProps["value"]; // any
|
|
@@ -11,6 +13,7 @@ export type TabValue = MTabProps["value"]; // any
|
|
|
11
13
|
export type OnTabChangeFn = (tabValue: TabValue) => void; // Function invoked when selected tab value changes.
|
|
12
14
|
|
|
13
15
|
export interface Tab {
|
|
16
|
+
annotation?: DataDictionaryAnnotation;
|
|
14
17
|
count?: string;
|
|
15
18
|
icon?: MTabProps["icon"]; // element or string
|
|
16
19
|
iconPosition?: MTabProps["iconPosition"]; // "bottom" or "end" or "start" or "top
|
|
@@ -41,14 +44,21 @@ export const Tabs = ({
|
|
|
41
44
|
>
|
|
42
45
|
{tabs.map(
|
|
43
46
|
(
|
|
44
|
-
{
|
|
47
|
+
{
|
|
48
|
+
annotation,
|
|
49
|
+
count,
|
|
50
|
+
icon,
|
|
51
|
+
iconPosition = "start",
|
|
52
|
+
label,
|
|
53
|
+
value: tabValue,
|
|
54
|
+
},
|
|
45
55
|
t
|
|
46
56
|
) => (
|
|
47
57
|
<Tab
|
|
48
58
|
icon={icon}
|
|
49
59
|
iconPosition={icon ? iconPosition : undefined}
|
|
50
60
|
key={`${label}${t}`}
|
|
51
|
-
label={
|
|
61
|
+
label={buildTabLabel(label, count, annotation)}
|
|
52
62
|
value={tabValue}
|
|
53
63
|
/>
|
|
54
64
|
)
|
|
@@ -56,3 +66,23 @@ export const Tabs = ({
|
|
|
56
66
|
</MTabs>
|
|
57
67
|
);
|
|
58
68
|
};
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Build a tab value from a tab config. Specifically, display the tab label
|
|
72
|
+
* with a tooltip annotation if necessary.
|
|
73
|
+
* @param label - Tab display value.
|
|
74
|
+
* @param count - Optional count to display next to the tab label.
|
|
75
|
+
* @param annotation - Data dictionary annotation.
|
|
76
|
+
* @returns Tab label with optional count and tooltip.
|
|
77
|
+
*/
|
|
78
|
+
function buildTabLabel(
|
|
79
|
+
label: ReactNode,
|
|
80
|
+
count?: string,
|
|
81
|
+
annotation?: DataDictionaryAnnotation
|
|
82
|
+
): ReactElement {
|
|
83
|
+
return (
|
|
84
|
+
<Tooltip description={annotation?.description} title={annotation?.label}>
|
|
85
|
+
<span>{count ? `${label} (${count})` : label}</span>
|
|
86
|
+
</Tooltip>
|
|
87
|
+
);
|
|
88
|
+
}
|
package/src/config/entities.ts
CHANGED
|
@@ -10,7 +10,12 @@ import {
|
|
|
10
10
|
TableOptions,
|
|
11
11
|
} from "@tanstack/react-table";
|
|
12
12
|
import { JSXElementConstructor, ReactNode } from "react";
|
|
13
|
-
import {
|
|
13
|
+
import {
|
|
14
|
+
DataDictionary,
|
|
15
|
+
DataDictionaryAnnotation,
|
|
16
|
+
SelectCategoryValueView,
|
|
17
|
+
SelectedFilter,
|
|
18
|
+
} from "../common/entities";
|
|
14
19
|
import { HeroTitle } from "../components/common/Title/title";
|
|
15
20
|
import { FooterProps } from "../components/Layout/components/Footer/footer";
|
|
16
21
|
import { HeaderProps } from "../components/Layout/components/Header/header";
|
|
@@ -87,6 +92,7 @@ export interface CategoryGroup {
|
|
|
87
92
|
* Model of category configured in site config.
|
|
88
93
|
*/
|
|
89
94
|
export interface CategoryConfig {
|
|
95
|
+
annotation?: DataDictionaryAnnotation;
|
|
90
96
|
key: string;
|
|
91
97
|
label: string;
|
|
92
98
|
mapSelectCategoryValue?: (
|
|
@@ -170,6 +176,7 @@ export type EntityPath = string;
|
|
|
170
176
|
*/
|
|
171
177
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any -- This config model is part of a generic array
|
|
172
178
|
export interface EntityConfig<T = any, I = any> extends TabConfig {
|
|
179
|
+
annotation?: DataDictionaryAnnotation;
|
|
173
180
|
apiPath?: EntityPath;
|
|
174
181
|
categoryGroupConfig?: CategoryGroupConfig;
|
|
175
182
|
detail: BackPageConfig;
|
|
@@ -180,6 +187,7 @@ export interface EntityConfig<T = any, I = any> extends TabConfig {
|
|
|
180
187
|
getId?: GetIdFunction<T>;
|
|
181
188
|
getTitle?: GetTitleFunction<T>;
|
|
182
189
|
hideTabs?: boolean;
|
|
190
|
+
key?: string; // Optional data dictionary key
|
|
183
191
|
list: ListConfig<T>;
|
|
184
192
|
listView?: ListViewConfig;
|
|
185
193
|
options?: Options;
|
|
@@ -366,6 +374,7 @@ export interface SiteConfig {
|
|
|
366
374
|
categoryGroupConfig?: CategoryGroupConfig;
|
|
367
375
|
contentDir?: string;
|
|
368
376
|
contentThemeOptionsFn?: ThemeOptionsFn;
|
|
377
|
+
dataDictionary?: DataDictionary;
|
|
369
378
|
dataSource: DataSourceConfig;
|
|
370
379
|
entities: EntityConfig[];
|
|
371
380
|
explorerTitle: HeroTitle;
|
|
@@ -77,6 +77,7 @@ function buildCategoryView(
|
|
|
77
77
|
const mapSelectCategoryValue =
|
|
78
78
|
categoryConfig?.mapSelectCategoryValue || getSelectCategoryValue;
|
|
79
79
|
return {
|
|
80
|
+
annotation: categoryConfig?.annotation,
|
|
80
81
|
isDisabled: false,
|
|
81
82
|
key: category.key,
|
|
82
83
|
label: getCategoryLabel(category.key, categoryConfig),
|
package/src/providers/config.tsx
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import React, { createContext, ReactNode } from "react";
|
|
1
|
+
import React, { createContext, ReactNode, useState } from "react";
|
|
2
|
+
import { annotateSiteConfig } from "../components/DataDictionary/common/utils";
|
|
2
3
|
import { EntityConfig, SiteConfig } from "../config/entities";
|
|
3
4
|
import {
|
|
4
5
|
getDefaultConfig,
|
|
@@ -31,7 +32,14 @@ export function ConfigProvider({
|
|
|
31
32
|
config,
|
|
32
33
|
entityListType = "",
|
|
33
34
|
}: ConfigProps): JSX.Element {
|
|
34
|
-
|
|
35
|
+
// Annote config on init. Note config is mutated but using state here to
|
|
36
|
+
// ensure annotated config is calculated once and is used rather than the raw config.
|
|
37
|
+
const [annotatedConfig] = useState(() => {
|
|
38
|
+
annotateSiteConfig(config);
|
|
39
|
+
return config;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const { entities } = annotatedConfig;
|
|
35
43
|
const defaultEntityListType = config.redirectRootToPath.slice(1);
|
|
36
44
|
const entityName = entityListType || defaultEntityListType;
|
|
37
45
|
const entityConfig = getEntityConfig(entities, entityName);
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import {
|
|
2
|
+
annotateColumnConfig,
|
|
3
|
+
annotateDefaultCategoryConfig,
|
|
4
|
+
annotateEntityCategoryConfig,
|
|
5
|
+
annotateEntityConfig,
|
|
6
|
+
} from "../src/components/DataDictionary/common/utils";
|
|
7
|
+
import { SiteConfig } from "../src/config/entities";
|
|
8
|
+
|
|
9
|
+
describe("Data Dictionary", () => {
|
|
10
|
+
it("annotates entity", () => {
|
|
11
|
+
const key = "entity";
|
|
12
|
+
|
|
13
|
+
// Create annotation for column and add to dummy annotation map.
|
|
14
|
+
const annotation = {
|
|
15
|
+
description: "description for entity",
|
|
16
|
+
label: "entity",
|
|
17
|
+
};
|
|
18
|
+
const annotationsByKey = {
|
|
19
|
+
[key]: annotation,
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
// Create dummy site config.
|
|
23
|
+
const siteConfig = {
|
|
24
|
+
entities: [
|
|
25
|
+
{
|
|
26
|
+
key,
|
|
27
|
+
list: {
|
|
28
|
+
columns: [{ id: "col150" }, { id: "col1" }],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
} as unknown as SiteConfig;
|
|
33
|
+
|
|
34
|
+
// Annotate
|
|
35
|
+
annotateEntityConfig(siteConfig, annotationsByKey);
|
|
36
|
+
|
|
37
|
+
// Confirm entity is annotated.
|
|
38
|
+
const entity = siteConfig.entities[0];
|
|
39
|
+
expect(entity.annotation).toEqual(annotation);
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it("annotates entity category config", () => {
|
|
43
|
+
const key = "filter0";
|
|
44
|
+
|
|
45
|
+
// Create annotation for column and add to dummy annotation map.
|
|
46
|
+
const annotation = {
|
|
47
|
+
description: "description for filter 0",
|
|
48
|
+
label: "filter 0",
|
|
49
|
+
};
|
|
50
|
+
const annotationsByKey = {
|
|
51
|
+
[key]: annotation,
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// Create dummy site config.
|
|
55
|
+
const siteConfig = {
|
|
56
|
+
entities: [
|
|
57
|
+
{
|
|
58
|
+
categoryGroupConfig: {
|
|
59
|
+
categoryGroups: [
|
|
60
|
+
{
|
|
61
|
+
categoryConfigs: [
|
|
62
|
+
{
|
|
63
|
+
key,
|
|
64
|
+
label: "filter 0",
|
|
65
|
+
},
|
|
66
|
+
],
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
},
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
} as unknown as SiteConfig;
|
|
73
|
+
|
|
74
|
+
// Annotate
|
|
75
|
+
annotateEntityCategoryConfig(siteConfig, annotationsByKey);
|
|
76
|
+
|
|
77
|
+
// Confirm filter is annotated.
|
|
78
|
+
const categoryConfig =
|
|
79
|
+
siteConfig.entities[0].categoryGroupConfig?.categoryGroups[0]
|
|
80
|
+
.categoryConfigs[0];
|
|
81
|
+
expect(categoryConfig?.annotation).toEqual(annotation);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("annotates default category config", () => {
|
|
85
|
+
const key = "filter0";
|
|
86
|
+
|
|
87
|
+
// Create annotation for column and add to dummy annotation map.
|
|
88
|
+
const annotation = {
|
|
89
|
+
description: "description for filter 0",
|
|
90
|
+
label: "filter 0",
|
|
91
|
+
};
|
|
92
|
+
const annotationsByKey = {
|
|
93
|
+
[key]: annotation,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
// Create dummy site config.
|
|
97
|
+
const siteConfig = {
|
|
98
|
+
categoryGroupConfig: {
|
|
99
|
+
categoryGroups: [
|
|
100
|
+
{
|
|
101
|
+
categoryConfigs: [
|
|
102
|
+
{
|
|
103
|
+
key,
|
|
104
|
+
label: "filter 0",
|
|
105
|
+
},
|
|
106
|
+
],
|
|
107
|
+
},
|
|
108
|
+
],
|
|
109
|
+
},
|
|
110
|
+
} as unknown as SiteConfig;
|
|
111
|
+
|
|
112
|
+
// Annotate
|
|
113
|
+
annotateDefaultCategoryConfig(siteConfig, annotationsByKey);
|
|
114
|
+
|
|
115
|
+
// Confirm filter is annotated.
|
|
116
|
+
const categoryConfig =
|
|
117
|
+
siteConfig.categoryGroupConfig?.categoryGroups[0].categoryConfigs[0];
|
|
118
|
+
expect(categoryConfig?.annotation).toEqual(annotation);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("annotates column", () => {
|
|
122
|
+
const key = "col0";
|
|
123
|
+
|
|
124
|
+
// Create annotation for column and add to dummy annotation map.
|
|
125
|
+
const annotation = {
|
|
126
|
+
description: "description for column 0",
|
|
127
|
+
label: "column 0",
|
|
128
|
+
};
|
|
129
|
+
const annotationsByKey = {
|
|
130
|
+
[key]: annotation,
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
// Create dummy site config.
|
|
134
|
+
const siteConfig = {
|
|
135
|
+
entities: [
|
|
136
|
+
{
|
|
137
|
+
list: {
|
|
138
|
+
columns: [{ id: key }, { id: "col1" }],
|
|
139
|
+
},
|
|
140
|
+
name: "entity",
|
|
141
|
+
},
|
|
142
|
+
],
|
|
143
|
+
} as unknown as SiteConfig;
|
|
144
|
+
|
|
145
|
+
// Annotate
|
|
146
|
+
annotateColumnConfig(siteConfig, annotationsByKey);
|
|
147
|
+
|
|
148
|
+
// Confirm column 0 is annotated and column 1 is not.
|
|
149
|
+
const columns = siteConfig.entities[0].list.columns ?? [];
|
|
150
|
+
expect((columns[0]?.meta as any)?.annotation).toEqual(annotation);
|
|
151
|
+
expect((columns[1]?.meta as any)?.annotation).toBeUndefined();
|
|
152
|
+
});
|
|
153
|
+
});
|
|
@@ -18,6 +18,7 @@ import type {} from "@mui/material/Typography";
|
|
|
18
18
|
import type {} from "@tanstack/react-table";
|
|
19
19
|
import { RowData } from "@tanstack/react-table";
|
|
20
20
|
import { DataLayer } from "../src/common/analytics/entities";
|
|
21
|
+
import { DataDictionaryAnnotation } from "../src/common/entities";
|
|
21
22
|
import {
|
|
22
23
|
CustomFeatureInitialTableState,
|
|
23
24
|
CustomFeatureInstance,
|
|
@@ -268,6 +269,7 @@ declare module "@tanstack/react-table" {
|
|
|
268
269
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- TData and TValue are unused variables.
|
|
269
270
|
interface ColumnMeta<TData extends RowData, TValue> {
|
|
270
271
|
align?: TableCellProps["align"];
|
|
272
|
+
annotation?: DataDictionaryAnnotation;
|
|
271
273
|
columnPinned?: boolean;
|
|
272
274
|
header?: string;
|
|
273
275
|
width?: GridTrackSize;
|