mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-27 18:07:57 +00:00
fix(customHomePage): fixes for hierarchy view (#14365)
This commit is contained in:
parent
ef200eff7b
commit
a9b7d79220
@ -3,10 +3,10 @@ import { isEqual } from 'lodash';
|
||||
import { useEffect } from 'react';
|
||||
|
||||
import { useDomainsContext } from '@app/domainV2/DomainsContext';
|
||||
import EntityRegistry from '@app/entity/EntityRegistry';
|
||||
import { GenericEntityProperties } from '@app/entity/shared/types';
|
||||
import usePrevious from '@app/shared/usePrevious';
|
||||
import { useEntityRegistry } from '@app/useEntityRegistry';
|
||||
import { EntityRegistry } from '@src/entityRegistryContext';
|
||||
|
||||
import { ListDomainsDocument, ListDomainsQuery } from '@graphql/domain.generated';
|
||||
import { Entity, EntityType } from '@types';
|
||||
@ -135,11 +135,13 @@ export function useUpdateDomainEntityDataOnChange(entityData: GenericEntityPrope
|
||||
export function useSortedDomains<T extends Entity>(domains?: Array<T>, sortBy?: 'displayName') {
|
||||
const entityRegistry = useEntityRegistry();
|
||||
if (!domains || !sortBy) return domains;
|
||||
return [...domains].sort((a, b) => {
|
||||
const nameA = entityRegistry.getDisplayName(EntityType.Domain, a) || '';
|
||||
const nameB = entityRegistry.getDisplayName(EntityType.Domain, b) || '';
|
||||
return nameA.localeCompare(nameB);
|
||||
});
|
||||
return [...domains].sort((a, b) => compareDomainsByDisplayName(a, b, entityRegistry));
|
||||
}
|
||||
|
||||
export function compareDomainsByDisplayName<T extends Entity>(domainA: T, domainB: T, entityRegistry: EntityRegistry) {
|
||||
const nameA = entityRegistry.getDisplayName(EntityType.Domain, domainA) || '';
|
||||
const nameB = entityRegistry.getDisplayName(EntityType.Domain, domainB) || '';
|
||||
return nameA.localeCompare(nameB);
|
||||
}
|
||||
|
||||
export function getParentDomains<T extends Entity>(domain: T, entityRegistry: EntityRegistry) {
|
||||
|
||||
@ -77,6 +77,7 @@ describe('useLoader hook', () => {
|
||||
parentValue: mockParentValue,
|
||||
metadata: undefined,
|
||||
maxNumberToLoad: mockMaxNumberOfChildrenToLoad,
|
||||
forceHasAsyncChildren: false,
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -95,6 +96,7 @@ describe('useLoader hook', () => {
|
||||
parentValue: mockParentValue,
|
||||
metadata: mockMetadata,
|
||||
maxNumberToLoad: mockMaxNumberOfChildrenToLoad,
|
||||
forceHasAsyncChildren: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@ -11,6 +11,7 @@ export default function useChildrenDomainsLoader({
|
||||
parentValue,
|
||||
metadata,
|
||||
maxNumberToLoad,
|
||||
forceHasAsyncChildren,
|
||||
}: ChildrenLoaderInputType): ChildrenLoaderResultType {
|
||||
const totalNumberOfChildren = metadata?.totalNumberOfChildren ?? maxNumberToLoad;
|
||||
const numberOfAlreadyLoadedChildren = metadata?.numberOfLoadedChildren ?? 0;
|
||||
@ -27,7 +28,7 @@ export default function useChildrenDomainsLoader({
|
||||
numberOfChildrenToLoad,
|
||||
numberOfChildrenToLoad === 0,
|
||||
);
|
||||
const nodes = useTreeNodesFromDomains(domains);
|
||||
const nodes = useTreeNodesFromDomains(domains, forceHasAsyncChildren);
|
||||
|
||||
const isLoading = useMemo(() => {
|
||||
if (!shouldLoad) return false;
|
||||
|
||||
@ -11,6 +11,7 @@ export default function useChildrenGlossaryLoader({
|
||||
parentValue,
|
||||
metadata,
|
||||
maxNumberToLoad,
|
||||
forceHasAsyncChildren,
|
||||
}: ChildrenLoaderInputType): ChildrenLoaderResultType {
|
||||
const totalNumberOfChildren = metadata?.totalNumberOfChildren ?? maxNumberToLoad;
|
||||
const numberOfAlreadyLoadedChildren = metadata?.numberOfLoadedChildren ?? 0;
|
||||
@ -27,7 +28,11 @@ export default function useChildrenGlossaryLoader({
|
||||
count: numberOfChildrenToLoad,
|
||||
skip: numberOfChildrenToLoad === 0,
|
||||
});
|
||||
const { treeNodes: nodes } = useTreeNodesFromGlossaryNodesAndTerms(glossaryNodes, glossaryTerms, true);
|
||||
const { treeNodes: nodes } = useTreeNodesFromGlossaryNodesAndTerms(
|
||||
glossaryNodes,
|
||||
glossaryTerms,
|
||||
forceHasAsyncChildren,
|
||||
);
|
||||
|
||||
const isLoading = useMemo(() => {
|
||||
if (!shouldLoad) return false;
|
||||
|
||||
@ -26,6 +26,7 @@ export default function useLoader(
|
||||
parentValue,
|
||||
metadata,
|
||||
maxNumberToLoad,
|
||||
forceHasAsyncChildren: relatedEntitiesOrFilters !== undefined,
|
||||
});
|
||||
|
||||
const relatedEntitiesResponse = loadRelatedEntities?.({
|
||||
|
||||
@ -16,6 +16,7 @@ export interface ChildrenLoaderInputType {
|
||||
maxNumberToLoad: number;
|
||||
dependenciesIsLoading?: boolean;
|
||||
orFilters?: AndFilterInput[];
|
||||
forceHasAsyncChildren?: boolean;
|
||||
}
|
||||
|
||||
export interface ChildrenLoaderResultType {
|
||||
|
||||
@ -0,0 +1,90 @@
|
||||
import { describe, expect } from 'vitest';
|
||||
|
||||
import { sortDomainTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/domains/utils';
|
||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||
import { getTestEntityRegistry } from '@utils/test-utils/TestPageContainer';
|
||||
|
||||
import { Entity, EntityType } from '@types';
|
||||
|
||||
// Helper to create tree nodes
|
||||
const createTreeNode = (entity: Entity): TreeNode => ({
|
||||
value: entity.urn,
|
||||
label: entity.urn,
|
||||
entity,
|
||||
});
|
||||
|
||||
describe('utils', () => {
|
||||
describe('sortDomainTreeNodes', () => {
|
||||
it('sorts domain nodes by display name and keeps non-domains in original order', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
// Create test nodes (intentionally out of order)
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({ urn: 'dataset:1', type: EntityType.Dataset, properties: { name: 'Users' } } as Entity), // Non-domain
|
||||
createTreeNode({
|
||||
urn: 'domain:1',
|
||||
type: EntityType.Domain,
|
||||
properties: { name: 'Marketing' },
|
||||
} as Entity), // Domain (Marketing)
|
||||
createTreeNode({
|
||||
urn: 'dashboard:1',
|
||||
type: EntityType.Dashboard,
|
||||
properties: { name: 'Metrics' },
|
||||
} as Entity), // Non-domain
|
||||
createTreeNode({
|
||||
urn: 'domain:2',
|
||||
type: EntityType.Domain,
|
||||
properties: { name: 'Engineering' },
|
||||
} as Entity), // Domain (Engineering)
|
||||
];
|
||||
|
||||
const sorted = sortDomainTreeNodes(nodes, entityRegistry);
|
||||
|
||||
// Verify domains are sorted first by display name (Engineering, Marketing)
|
||||
expect(sorted[0].entity.urn).toBe('domain:2'); // Engineering
|
||||
expect(sorted[1].entity.urn).toBe('domain:1'); // Marketing
|
||||
|
||||
// Verify non-domains maintain original relative order
|
||||
expect(sorted[2].entity.urn).toBe('dataset:1'); // Users (first non-domain)
|
||||
expect(sorted[3].entity.urn).toBe('dashboard:1'); // Metrics (second non-domain)
|
||||
});
|
||||
|
||||
it('handles empty input', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
const sorted = sortDomainTreeNodes([], entityRegistry);
|
||||
expect(sorted).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles all domain nodes', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({ urn: 'domain:3', type: EntityType.Domain, properties: { name: 'Z' } } as Entity),
|
||||
createTreeNode({ urn: 'domain:4', type: EntityType.Domain, properties: { name: 'A' } } as Entity),
|
||||
createTreeNode({ urn: 'domain:5', type: EntityType.Domain, properties: { name: 'M' } } as Entity),
|
||||
];
|
||||
|
||||
const sorted = sortDomainTreeNodes(nodes, entityRegistry);
|
||||
|
||||
// Should be sorted alphabetically: A, M, Z
|
||||
expect(sorted.map((n) => n.entity.urn)).toEqual([
|
||||
'domain:4', // A
|
||||
'domain:5', // M
|
||||
'domain:3', // Z
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles all non-domain nodes', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({ urn: 'dataset:2', type: EntityType.Dataset, properties: { name: 'Beta' } } as Entity),
|
||||
createTreeNode({ urn: 'chart:1', type: EntityType.Chart, properties: { name: 'Alpha' } } as Entity),
|
||||
];
|
||||
|
||||
const sorted = sortDomainTreeNodes(nodes, entityRegistry);
|
||||
|
||||
// Should maintain original order
|
||||
expect(sorted.map((n) => n.entity.urn)).toEqual(['dataset:2', 'chart:1']);
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -258,7 +258,7 @@ describe('useSelectableDomainTree hook', () => {
|
||||
|
||||
renderHook(() => useSelectableDomainTree([]));
|
||||
|
||||
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith(rootDomains);
|
||||
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith(rootDomains, false);
|
||||
});
|
||||
|
||||
it('should handle mixed states correctly', () => {
|
||||
@ -287,7 +287,7 @@ describe('useSelectableDomainTree hook', () => {
|
||||
|
||||
renderHook(() => useSelectableDomainTree([]));
|
||||
|
||||
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith([mockDomain1]);
|
||||
expect(mockUseTreeNodesFromDomains).toHaveBeenCalledWith([mockDomain1], false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { sortDomainTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/domains/utils';
|
||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
||||
|
||||
export default function useDomainTreeNodesSorter() {
|
||||
const entityRegistry = useEntityRegistryV2();
|
||||
|
||||
return useCallback((nodes: TreeNode[]) => sortDomainTreeNodes(nodes, entityRegistry), [entityRegistry]);
|
||||
}
|
||||
@ -1,3 +1,4 @@
|
||||
import useDomainTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainTreeNodesSorter';
|
||||
import useDomainsByUrns from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainsByUrns';
|
||||
import useTreeNodesFromDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromListDomains';
|
||||
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
|
||||
@ -6,7 +7,8 @@ export default function useDomainsTree(domainsUrns: string[], shouldShowRelatedE
|
||||
const { domains, loading } = useDomainsByUrns(domainsUrns);
|
||||
const treeNodes = useTreeNodesFromDomains(domains, shouldShowRelatedEntities);
|
||||
|
||||
const tree = useTree(treeNodes);
|
||||
const nodesSorter = useDomainTreeNodesSorter();
|
||||
const tree = useTree(treeNodes, nodesSorter);
|
||||
|
||||
return {
|
||||
tree,
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import useDomainTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useDomainTreeNodesSorter';
|
||||
import useInitialDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useInitialDomains';
|
||||
import useRootDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useRootDomains';
|
||||
import useTreeNodesFromFlatDomains from '@app/homeV3/modules/hierarchyViewModule/components/domains/hooks/useTreeNodesFromFlatDomains';
|
||||
@ -10,13 +11,14 @@ import { mergeTrees } from '@app/homeV3/modules/hierarchyViewModule/treeView/uti
|
||||
export default function useSelectableDomainTree(initialSelectedDomainUrns: string[] | undefined) {
|
||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>(initialSelectedDomainUrns ?? []);
|
||||
const tree = useTree();
|
||||
const nodesSorter = useDomainTreeNodesSorter();
|
||||
const tree = useTree(undefined, nodesSorter);
|
||||
|
||||
const { domains: initialDomains } = useInitialDomains(initialSelectedDomainUrns ?? []);
|
||||
const initialSelectedTreeNodes = useTreeNodesFromFlatDomains(initialDomains);
|
||||
|
||||
const { domains: rootDomains, loading: rootDomainsLoading } = useRootDomains();
|
||||
const rootTreeNodes = useTreeNodesFromDomains(rootDomains);
|
||||
const rootTreeNodes = useTreeNodesFromDomains(rootDomains, false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
|
||||
@ -1,8 +1,10 @@
|
||||
import { compareDomainsByDisplayName } from '@app/domainV2/utils';
|
||||
import { DomainItem } from '@app/homeV3/modules/hierarchyViewModule/components/domains/types';
|
||||
import { unwrapParentEntitiesToTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/utils';
|
||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||
import { EntityRegistry } from '@src/entityRegistryContext';
|
||||
|
||||
import { Domain } from '@types';
|
||||
import { Domain, EntityType } from '@types';
|
||||
|
||||
export function convertDomainToTreeNode(domain: DomainItem, forceHasAsyncChildren = false): TreeNode {
|
||||
return {
|
||||
@ -17,3 +19,15 @@ export function convertDomainToTreeNode(domain: DomainItem, forceHasAsyncChildre
|
||||
export function unwrapFlatDomainsToTreeNodes(domains: Domain[] | undefined): TreeNode[] | undefined {
|
||||
return unwrapParentEntitiesToTreeNodes(domains, (item) => [...(item.parentDomains?.domains ?? [])].reverse());
|
||||
}
|
||||
|
||||
export function sortDomainTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry) {
|
||||
const domainTreeNodes = nodes.filter((node) => node.entity.type === EntityType.Domain);
|
||||
const anotherTreeNodes = nodes.filter((node) => node.entity.type !== EntityType.Domain);
|
||||
|
||||
return [
|
||||
...[...domainTreeNodes].sort((nodeA, nodeB) =>
|
||||
compareDomainsByDisplayName(nodeA.entity, nodeB.entity, entityRegistry),
|
||||
),
|
||||
...anotherTreeNodes,
|
||||
];
|
||||
}
|
||||
|
||||
@ -1,14 +1,21 @@
|
||||
import { Input } from '@components';
|
||||
import { Form } from 'antd';
|
||||
import React from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem';
|
||||
import RelatedEntitiesSection from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/relatedEntities/RelatedEntitiesSection';
|
||||
import SelectAssetsSection from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/SelectAssetsSection';
|
||||
|
||||
const FormWrapper = styled.div`
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 16px;
|
||||
`;
|
||||
|
||||
export default function HierarchyViewModuleForm() {
|
||||
return (
|
||||
<>
|
||||
<Form.Item
|
||||
<FormWrapper>
|
||||
<FormItem
|
||||
name="name"
|
||||
rules={[
|
||||
{
|
||||
@ -18,10 +25,10 @@ export default function HierarchyViewModuleForm() {
|
||||
]}
|
||||
>
|
||||
<Input label="Name" placeholder="Choose a name for your module" isRequired />
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
|
||||
<SelectAssetsSection />
|
||||
<RelatedEntitiesSection />
|
||||
</>
|
||||
</FormWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,16 @@
|
||||
import { Form } from 'antd';
|
||||
import styled from 'styled-components';
|
||||
|
||||
const FormItem = styled(Form.Item)`
|
||||
margin-bottom: 0;
|
||||
|
||||
.ant-form-item {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.ant-form-item-control-input {
|
||||
min-height: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export default FormItem;
|
||||
@ -2,6 +2,7 @@ import { Form } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
|
||||
import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext';
|
||||
import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem';
|
||||
import {
|
||||
FORM_FIELD_RELATED_ENTITIES_FILTER,
|
||||
FORM_FIELD_SHOW_RELATED_ENTITIES,
|
||||
@ -46,11 +47,11 @@ export default function RelatedEntitiesSection() {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Form.Item name={FORM_FIELD_SHOW_RELATED_ENTITIES}>
|
||||
<FormItem name={FORM_FIELD_SHOW_RELATED_ENTITIES}>
|
||||
<ShowRelatedEntitiesSwitch isChecked={isChecked} onChange={toggleShowRelatedEntitiesSwitch} />
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
|
||||
<Form.Item name={FORM_FIELD_RELATED_ENTITIES_FILTER}>
|
||||
<FormItem name={FORM_FIELD_RELATED_ENTITIES_FILTER}>
|
||||
{isChecked && (
|
||||
<LogicalFiltersBuilder
|
||||
filters={relatedEntitiesFilter ?? defaultRelatedEntitiesFilter ?? EMPTY_FILTER}
|
||||
@ -58,7 +59,7 @@ export default function RelatedEntitiesSection() {
|
||||
properties={properties}
|
||||
/>
|
||||
)}
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -21,7 +21,7 @@ export default function ShowRelatedEntitiesSwitch({ isChecked, onChange }: Props
|
||||
return (
|
||||
<Wrapper>
|
||||
<LabelContainer>
|
||||
<Text weight="bold" lineHeight="sm">
|
||||
<Text weight="bold" color="gray" lineHeight="sm">
|
||||
Show Related Entities
|
||||
</Text>
|
||||
<Text color="gray" lineHeight="sm">
|
||||
|
||||
@ -1,14 +1,18 @@
|
||||
import { Text } from '@components';
|
||||
import { Form } from 'antd';
|
||||
import React, { useCallback } from 'react';
|
||||
import styled from 'styled-components';
|
||||
|
||||
import DomainsSelectableTreeView from '@app/homeV3/modules/hierarchyViewModule/components/domains/DomainsSelectableTreeView';
|
||||
import { useHierarchyFormContext } from '@app/homeV3/modules/hierarchyViewModule/components/form/HierarchyFormContext';
|
||||
import FormItem from '@app/homeV3/modules/hierarchyViewModule/components/form/components/FormItem';
|
||||
import { FORM_FIELD_ASSET_TYPE } from '@app/homeV3/modules/hierarchyViewModule/components/form/constants';
|
||||
import EntityTypeTabs from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/assetTypeTabs/AssetTypeTabs';
|
||||
import GlossarySelectableTreeView from '@app/homeV3/modules/hierarchyViewModule/components/glossary/GlossarySelectableTreeView';
|
||||
import { ASSET_TYPE_DOMAINS, ASSET_TYPE_GLOSSARY } from '@app/homeV3/modules/hierarchyViewModule/constants';
|
||||
|
||||
const Wrapper = styled.div``;
|
||||
|
||||
export default function SelectAssetsSection() {
|
||||
const form = Form.useFormInstance();
|
||||
|
||||
@ -30,28 +34,30 @@ export default function SelectAssetsSection() {
|
||||
key: ASSET_TYPE_DOMAINS,
|
||||
label: 'Domains',
|
||||
content: (
|
||||
<Form.Item name="domainAssets">
|
||||
<FormItem name="domainAssets">
|
||||
<DomainsSelectableTreeView />
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: ASSET_TYPE_GLOSSARY,
|
||||
label: 'Glossary',
|
||||
content: (
|
||||
<Form.Item name="glossaryAssets">
|
||||
<FormItem name="glossaryAssets">
|
||||
<GlossarySelectableTreeView />
|
||||
</Form.Item>
|
||||
</FormItem>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<>
|
||||
<Text weight="bold">Search and Select Assets</Text>
|
||||
<Form.Item name={FORM_FIELD_ASSET_TYPE}>
|
||||
<Wrapper>
|
||||
<Text color="gray" weight="bold">
|
||||
Search and Select Assets
|
||||
</Text>
|
||||
<FormItem name={FORM_FIELD_ASSET_TYPE}>
|
||||
<EntityTypeTabs tabs={tabs} onTabClick={onTabClick} defaultKey={assetType ?? defaultAssetsType} />
|
||||
</Form.Item>
|
||||
</>
|
||||
</FormItem>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
@ -0,0 +1,197 @@
|
||||
import { sortGlossaryTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/glossary/utils';
|
||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||
import { getTestEntityRegistry } from '@utils/test-utils/TestPageContainer';
|
||||
|
||||
import { Entity, EntityType } from '@types';
|
||||
|
||||
// Helper to create tree nodes
|
||||
const createTreeNode = (entity: Entity): TreeNode => ({
|
||||
value: entity.urn,
|
||||
label: entity.urn,
|
||||
entity,
|
||||
});
|
||||
|
||||
describe('utils', () => {
|
||||
describe('sortGlossaryTreeNodes', () => {
|
||||
it('categorizes and sorts glossary nodes, terms, and others', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
|
||||
// Create test nodes in shuffled order
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({
|
||||
urn: 'urn:dataset:1',
|
||||
type: EntityType.Dataset,
|
||||
properties: { name: 'Users' },
|
||||
} as Entity), // Other
|
||||
createTreeNode({
|
||||
urn: 'urn:glossary:term:1',
|
||||
type: EntityType.GlossaryTerm,
|
||||
properties: { name: 'Terminal' },
|
||||
} as Entity), // Term
|
||||
createTreeNode({
|
||||
urn: 'urn:glossary:node:1',
|
||||
type: EntityType.GlossaryNode,
|
||||
properties: { name: 'Zebra' },
|
||||
} as Entity), // Node
|
||||
createTreeNode({
|
||||
urn: 'urn:chart:1',
|
||||
type: EntityType.Chart,
|
||||
properties: { name: 'Revenue' },
|
||||
} as Entity), // Other
|
||||
createTreeNode({
|
||||
urn: 'urn:glossary:term:2',
|
||||
type: EntityType.GlossaryTerm,
|
||||
properties: { name: 'Apple' },
|
||||
} as Entity), // Term
|
||||
createTreeNode({
|
||||
urn: 'urn:glossary:node:2',
|
||||
type: EntityType.GlossaryNode,
|
||||
properties: { name: 'Alpha' },
|
||||
} as Entity), // Node
|
||||
];
|
||||
|
||||
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
|
||||
|
||||
// Verify grouping order: Nodes -> Terms -> Others
|
||||
expect(sorted[0].entity.type).toBe(EntityType.GlossaryNode);
|
||||
expect(sorted[1].entity.type).toBe(EntityType.GlossaryNode);
|
||||
expect(sorted[2].entity.type).toBe(EntityType.GlossaryTerm);
|
||||
expect(sorted[3].entity.type).toBe(EntityType.GlossaryTerm);
|
||||
expect(sorted[4].entity.type).toBe(EntityType.Dataset);
|
||||
expect(sorted[5].entity.type).toBe(EntityType.Chart);
|
||||
|
||||
// Verify sorted order within groups
|
||||
expect(sorted[0].entity.urn).toBe('urn:glossary:node:2'); // Alpha
|
||||
expect(sorted[1].entity.urn).toBe('urn:glossary:node:1'); // Zebra
|
||||
expect(sorted[2].entity.urn).toBe('urn:glossary:term:2'); // Apple
|
||||
expect(sorted[3].entity.urn).toBe('urn:glossary:term:1'); // Terminal
|
||||
|
||||
// Verify others maintain original relative order
|
||||
expect(sorted[4].entity.urn).toBe('urn:dataset:1');
|
||||
expect(sorted[5].entity.urn).toBe('urn:chart:1');
|
||||
});
|
||||
|
||||
it('handles empty input', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
const sorted = sortGlossaryTreeNodes([], entityRegistry);
|
||||
expect(sorted).toEqual([]);
|
||||
});
|
||||
|
||||
it('handles only glossary nodes', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({
|
||||
urn: 'urn:gn:1',
|
||||
type: EntityType.GlossaryNode,
|
||||
properties: { name: 'Beta' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:gn:2',
|
||||
type: EntityType.GlossaryNode,
|
||||
properties: { name: 'Alpha' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:gn:3',
|
||||
type: EntityType.GlossaryNode,
|
||||
properties: { name: 'Gamma' },
|
||||
} as Entity),
|
||||
];
|
||||
|
||||
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
|
||||
|
||||
expect(sorted.map((n) => n.entity.urn)).toEqual([
|
||||
'urn:gn:2', // Alpha
|
||||
'urn:gn:1', // Beta
|
||||
'urn:gn:3', // Gamma
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles only glossary terms', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({
|
||||
urn: 'urn:gt:1',
|
||||
type: EntityType.GlossaryTerm,
|
||||
properties: { name: 'Zulu' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:gt:2',
|
||||
type: EntityType.GlossaryTerm,
|
||||
properties: { name: 'Alpha' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:gt:3',
|
||||
type: EntityType.GlossaryTerm,
|
||||
properties: { name: 'Mike' },
|
||||
} as Entity),
|
||||
];
|
||||
|
||||
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
|
||||
|
||||
expect(sorted.map((n) => n.entity.urn)).toEqual([
|
||||
'urn:gt:2', // Alpha
|
||||
'urn:gt:3', // Mike
|
||||
'urn:gt:1', // Zulu
|
||||
]);
|
||||
});
|
||||
|
||||
it('handles only other entities', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({
|
||||
urn: 'urn:ds:1',
|
||||
type: EntityType.Dataset,
|
||||
properties: { name: 'Dataset 1' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:dash:1',
|
||||
type: EntityType.Dashboard,
|
||||
properties: { name: 'Dashboard 1' },
|
||||
} as Entity),
|
||||
];
|
||||
|
||||
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
|
||||
|
||||
// Should maintain original order
|
||||
expect(sorted.map((n) => n.entity.urn)).toEqual(['urn:ds:1', 'urn:dash:1']);
|
||||
});
|
||||
|
||||
it('handles identical display names', () => {
|
||||
const entityRegistry = getTestEntityRegistry();
|
||||
|
||||
const nodes: TreeNode[] = [
|
||||
createTreeNode({
|
||||
urn: 'urn:gn:2',
|
||||
type: EntityType.GlossaryNode,
|
||||
properties: { name: 'Same' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:gn:1',
|
||||
type: EntityType.GlossaryNode,
|
||||
properties: { name: 'Same' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:gt:2',
|
||||
type: EntityType.GlossaryTerm,
|
||||
properties: { name: 'Same' },
|
||||
} as Entity),
|
||||
createTreeNode({
|
||||
urn: 'urn:gt:1',
|
||||
type: EntityType.GlossaryTerm,
|
||||
properties: { name: 'Same' },
|
||||
} as Entity),
|
||||
];
|
||||
|
||||
const sorted = sortGlossaryTreeNodes(nodes, entityRegistry);
|
||||
|
||||
// Should maintain original order within groups for identical names
|
||||
expect(sorted[0].entity.urn).toBe('urn:gn:2');
|
||||
expect(sorted[1].entity.urn).toBe('urn:gn:1');
|
||||
expect(sorted[2].entity.urn).toBe('urn:gt:2');
|
||||
expect(sorted[3].entity.urn).toBe('urn:gt:1');
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -1,4 +1,5 @@
|
||||
import useGlossaryNodesAndTermsByUrns from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryNodesAndTermsByUrns';
|
||||
import useGlossaryTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryTreeNodesSorter';
|
||||
import useTreeNodesFromGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useTreeNodesFromGlossaryNodesAndTerms';
|
||||
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
|
||||
|
||||
@ -11,7 +12,8 @@ export default function useGlossaryTree(glossaryNodesAndTermsUrns: string[], sho
|
||||
shouldShowRelatedEntities,
|
||||
);
|
||||
|
||||
const tree = useTree(treeNodes);
|
||||
const nodesSorter = useGlossaryTreeNodesSorter();
|
||||
const tree = useTree(treeNodes, nodesSorter);
|
||||
|
||||
return {
|
||||
tree,
|
||||
|
||||
@ -0,0 +1,11 @@
|
||||
import { useCallback } from 'react';
|
||||
|
||||
import { sortGlossaryTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/glossary/utils';
|
||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
|
||||
|
||||
export default function useGlossaryTreeNodesSorter() {
|
||||
const entityRegistry = useEntityRegistryV2();
|
||||
|
||||
return useCallback((nodes: TreeNode[]) => sortGlossaryTreeNodes(nodes, entityRegistry), [entityRegistry]);
|
||||
}
|
||||
@ -1,13 +1,15 @@
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import useGlossaryNodesAndTermsByUrns from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryNodesAndTermsByUrns';
|
||||
import useGlossaryTreeNodesSorter from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useGlossaryTreeNodesSorter';
|
||||
import useRootGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useRootGlossaryNodesAndTerms';
|
||||
import useTreeNodesFromFlatGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useTreeNodesFromFlatGlossaryNodesAndTerms';
|
||||
import useTreeNodesFromGlossaryNodesAndTerms from '@app/homeV3/modules/hierarchyViewModule/components/glossary/hooks/useTreeNodesFromGlossaryNodesAndTerms';
|
||||
import useTree from '@app/homeV3/modules/hierarchyViewModule/treeView/useTree';
|
||||
|
||||
export default function useSelectableGlossaryTree(initialSelectedGlossaryNodesAndTermsUrns: string[]) {
|
||||
const tree = useTree();
|
||||
const nodesSorter = useGlossaryTreeNodesSorter();
|
||||
const tree = useTree(undefined, nodesSorter);
|
||||
|
||||
const [isInitialized, setIsInitialized] = useState<boolean>(false);
|
||||
const [selectedValues, setSelectedValues] = useState<string[]>(initialSelectedGlossaryNodesAndTermsUrns ?? []);
|
||||
|
||||
@ -12,11 +12,8 @@ export default function useTreeNodesFromGlossaryNodesAndTerms(
|
||||
forceHasAsyncChildren?: boolean,
|
||||
) {
|
||||
const glossaryNodesTreeNodes = useMemo(
|
||||
() =>
|
||||
glossaryNodes?.map((glossaryNode) =>
|
||||
convertGlossaryNodeToTreeNode(glossaryNode, !!forceHasAsyncChildren),
|
||||
) ?? [],
|
||||
[glossaryNodes, forceHasAsyncChildren],
|
||||
() => glossaryNodes?.map((glossaryNode) => convertGlossaryNodeToTreeNode(glossaryNode)) ?? [],
|
||||
[glossaryNodes],
|
||||
);
|
||||
const glossaryTermsTreeNodes = useMemo(
|
||||
() =>
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
import { sortGlossaryNodes } from '@app/entityV2/glossaryNode/utils';
|
||||
import { sortGlossaryTerms } from '@app/entityV2/glossaryTerm/utils';
|
||||
import { unwrapParentEntitiesToTreeNodes } from '@app/homeV3/modules/hierarchyViewModule/components/form/sections/selectAssets/utils';
|
||||
import { GlossaryNodeType, GlossaryTermType } from '@app/homeV3/modules/hierarchyViewModule/components/glossary/types';
|
||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||
import { EntityRegistry } from '@src/entityRegistryContext';
|
||||
|
||||
import { GlossaryNode, GlossaryTerm } from '@types';
|
||||
import { EntityType, GlossaryNode, GlossaryTerm } from '@types';
|
||||
|
||||
export function convertGlossaryNodeToTreeNode(glossaryNode: GlossaryNodeType, forceHasAsyncChildren = false): TreeNode {
|
||||
const childrenNodesCount = glossaryNode.childrenCount?.nodesCount ?? 0;
|
||||
@ -34,3 +37,25 @@ export function unwrapFlatGlossaryNodesToTreeNodes(glossaryItems: GlossaryNode[]
|
||||
export function unwrapFlatGlossaryTermsToTreeNodes(glossaryItems: GlossaryTerm[] | undefined): TreeNode[] | undefined {
|
||||
return unwrapParentEntitiesToTreeNodes(glossaryItems, (item) => [...(item.parentNodes?.nodes ?? [])].reverse());
|
||||
}
|
||||
|
||||
function sortGlossaryNodeTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry): TreeNode[] {
|
||||
return [...nodes].sort((nodeA, nodeB) => sortGlossaryNodes(entityRegistry, nodeA.entity, nodeB.entity));
|
||||
}
|
||||
|
||||
function sortGlossaryTermTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry): TreeNode[] {
|
||||
return [...nodes].sort((nodeA, nodeB) => sortGlossaryTerms(entityRegistry, nodeA.entity, nodeB.entity));
|
||||
}
|
||||
|
||||
export function sortGlossaryTreeNodes(nodes: TreeNode[], entityRegistry: EntityRegistry): TreeNode[] {
|
||||
const glossaryNodeNodes = nodes.filter((node) => node.entity.type === EntityType.GlossaryNode);
|
||||
const glossaryTermNodes = nodes.filter((node) => node.entity.type === EntityType.GlossaryTerm);
|
||||
const anotherNodes = nodes.filter(
|
||||
(node) => ![EntityType.GlossaryNode, EntityType.GlossaryTerm].includes(node.entity.type),
|
||||
);
|
||||
|
||||
return [
|
||||
...sortGlossaryNodeTreeNodes(glossaryNodeNodes, entityRegistry),
|
||||
...sortGlossaryTermTreeNodes(glossaryTermNodes, entityRegistry),
|
||||
...anotherNodes,
|
||||
];
|
||||
}
|
||||
|
||||
@ -19,7 +19,7 @@ function createTreeNode(value: string, label?: string, children?: TreeNode[], en
|
||||
describe('useTree hook', () => {
|
||||
describe('initialization', () => {
|
||||
it('should initialize with empty array when no initial tree provided', () => {
|
||||
const { result } = renderHook(() => useTree());
|
||||
const { result } = renderHook(() => useTree(undefined));
|
||||
|
||||
expect(result.current.nodes).toEqual([]);
|
||||
});
|
||||
@ -59,6 +59,25 @@ describe('useTree hook', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('sorting of tree', () => {
|
||||
it('should sort tree if sorter is passed', () => {
|
||||
const tree = [
|
||||
createTreeNode('root2', '', [createTreeNode('root2-child2'), createTreeNode('root2-child1')]),
|
||||
createTreeNode('root1', '', [createTreeNode('root1-child2'), createTreeNode('root1-child1')]),
|
||||
];
|
||||
|
||||
const nodesSorter = (nodes: TreeNode[]) =>
|
||||
nodes.sort((nodeA, nodeB) => nodeA.value.localeCompare(nodeB.value));
|
||||
|
||||
const { result } = renderHook(() => useTree(tree, nodesSorter));
|
||||
|
||||
expect(result.current.nodes).toStrictEqual([
|
||||
createTreeNode('root1', '', [createTreeNode('root1-child1'), createTreeNode('root1-child2')]),
|
||||
createTreeNode('root2', '', [createTreeNode('root2-child1'), createTreeNode('root2-child2')]),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('replace function', () => {
|
||||
it('should replace entire tree with new nodes', () => {
|
||||
const initialNodes = [createTreeNode('old')];
|
||||
|
||||
@ -1,15 +1,25 @@
|
||||
import { useCallback, useEffect, useState } from 'react';
|
||||
import { useCallback, useEffect, useMemo, useState } from 'react';
|
||||
|
||||
import { TreeNode } from '@app/homeV3/modules/hierarchyViewModule/treeView/types';
|
||||
import { mergeTrees, updateNodeInTree, updateTree } from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
||||
import {
|
||||
mergeTrees,
|
||||
sortTree,
|
||||
updateNodeInTree,
|
||||
updateTree,
|
||||
} from '@app/homeV3/modules/hierarchyViewModule/treeView/utils';
|
||||
|
||||
export default function useTree(tree?: TreeNode[]) {
|
||||
export default function useTree(tree: TreeNode[] | undefined, nodesSorter?: (nodes: TreeNode[]) => TreeNode[]) {
|
||||
const [nodes, setNodes] = useState<TreeNode[]>(tree ?? []);
|
||||
|
||||
useEffect(() => {
|
||||
if (tree !== undefined) setNodes(tree);
|
||||
}, [tree]);
|
||||
|
||||
const sortedNodes = useMemo(() => {
|
||||
if (!nodesSorter) return nodes;
|
||||
return sortTree(nodes, nodesSorter);
|
||||
}, [nodes, nodesSorter]);
|
||||
|
||||
const replace = useCallback((newNodes: TreeNode[]) => setNodes(newNodes), []);
|
||||
|
||||
const merge = useCallback((treeToMerge: TreeNode[]) => {
|
||||
@ -25,7 +35,7 @@ export default function useTree(tree?: TreeNode[]) {
|
||||
}, []);
|
||||
|
||||
return {
|
||||
nodes,
|
||||
nodes: sortedNodes,
|
||||
replace,
|
||||
merge,
|
||||
update,
|
||||
|
||||
@ -179,3 +179,16 @@ export function getTopLevelSelectedValuesFromTree(selectedValues: string[], tree
|
||||
|
||||
return [...result];
|
||||
}
|
||||
|
||||
export function sortTree(tree: TreeNode[], nodesSorter: (nodes: TreeNode[]) => TreeNode[]): TreeNode[] {
|
||||
function traverse(node: TreeNode) {
|
||||
if (!node.children?.length) return node;
|
||||
|
||||
return {
|
||||
...node,
|
||||
children: nodesSorter(node.children).map(traverse),
|
||||
};
|
||||
}
|
||||
|
||||
return nodesSorter(tree).map(traverse);
|
||||
}
|
||||
|
||||
@ -17,6 +17,7 @@ import { DataPlatformInstanceEntity } from '@app/entity/dataPlatformInstance/Dat
|
||||
import { DataProductEntity } from '@app/entity/dataProduct/DataProductEntity';
|
||||
import { DatasetEntity } from '@app/entity/dataset/DatasetEntity';
|
||||
import { DomainEntity } from '@app/entity/domain/DomainEntity';
|
||||
import GlossaryNodeEntity from '@app/entity/glossaryNode/GlossaryNodeEntity';
|
||||
import { GlossaryTermEntity } from '@app/entity/glossaryTerm/GlossaryTermEntity';
|
||||
import { GroupEntity } from '@app/entity/group/Group';
|
||||
import { MLFeatureTableEntity } from '@app/entity/mlFeatureTable/MLFeatureTableEntity';
|
||||
@ -48,6 +49,7 @@ export function getTestEntityRegistry() {
|
||||
entityRegistry.register(new TagEntity());
|
||||
entityRegistry.register(new DataFlowEntity());
|
||||
entityRegistry.register(new DataJobEntity());
|
||||
entityRegistry.register(new GlossaryNodeEntity());
|
||||
entityRegistry.register(new GlossaryTermEntity());
|
||||
entityRegistry.register(new MLFeatureTableEntity());
|
||||
entityRegistry.register(new MLModelEntity());
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user