feat(homepage): support rendering top domains module in the new home page (#14010)

Co-authored-by: Chris Collins <chriscollins3456@gmail.com>
This commit is contained in:
purnimagarg1 2025-07-14 00:51:18 +05:30 committed by GitHub
parent aca7c103c8
commit a5d4adcaab
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 231 additions and 11 deletions

View File

@ -3,6 +3,7 @@ import React, { useMemo } from 'react';
import { ModuleProps } from '@app/homeV3/module/types';
import SampleLargeModule from '@app/homeV3/modules/SampleLargeModule';
import YourAssetsModule from '@app/homeV3/modules/YourAssetsModule';
import TopDomainsModule from '@app/homeV3/modules/domains/TopDomainsModule';
import { DataHubPageModuleType } from '@types';
@ -10,6 +11,8 @@ export default function Module(props: ModuleProps) {
const { module } = props;
const Component = useMemo(() => {
if (module.properties.type === DataHubPageModuleType.OwnedAssets) return YourAssetsModule;
if (module.properties.type === DataHubPageModuleType.Domains) return TopDomainsModule;
// TODO: remove the sample large module once we have other modules to fill this out
console.error(`Issue finding module with type ${module.properties.type}`);
return SampleLargeModule;

View File

@ -6,8 +6,8 @@ interface Props {
icon: string;
title: string;
description: string;
linkText: string;
onLinkClick: () => void;
linkText?: string;
onLinkClick?: () => void;
}
const Container = styled.div`
@ -41,9 +41,11 @@ export default function EmptyContent({ icon, title, description, linkText, onLin
{title}
</Text>
<Text color="gray">{description}</Text>
<Button variant="text" onClick={onLinkClick}>
{linkText} <Icon icon="ArrowRight" color="primary" source="phosphor" size="md" />
</Button>
{linkText && onLinkClick && (
<Button variant="text" onClick={onLinkClick}>
{linkText} <Icon icon="ArrowRight" color="primary" source="phosphor" size="md" />
</Button>
)}
</Container>
);
}

View File

@ -8,14 +8,15 @@ import { Entity } from '@types';
interface Props {
entity: Entity;
customDetailsRenderer?: (entity: Entity) => void;
}
export default function EntityItem({ entity }: Props) {
export default function EntityItem({ entity, customDetailsRenderer }: Props) {
const entityRegistry = useEntityRegistryV2();
return (
<Link to={entityRegistry.getEntityUrl(entity.type, entity.urn)}>
<AutoCompleteEntityItem entity={entity} key={entity.urn} />
<AutoCompleteEntityItem entity={entity} key={entity.urn} customDetailsRenderer={customDetailsRenderer} />
</Link>
);
}

View File

@ -0,0 +1,41 @@
import React from 'react';
import { useUserContext } from '@app/context/useUserContext';
import { useGetDomains } from '@app/homeV2/content/tabs/discovery/sections/domains/useGetDomains';
import EmptyContent from '@app/homeV3/module/components/EmptyContent';
import EntityItem from '@app/homeV3/module/components/EntityItem';
import LargeModule from '@app/homeV3/module/components/LargeModule';
import { ModuleProps } from '@app/homeV3/module/types';
import useGetDomainUtils from '@app/homeV3/modules/domains/useDomainModuleUtils';
const TopDomainsModule = (props: ModuleProps) => {
const { user } = useUserContext();
const { domains, loading } = useGetDomains(user);
const { renderDomainCounts, navigateToDomains } = useGetDomainUtils({ domains });
return (
<LargeModule {...props} loading={loading} onClickViewAll={navigateToDomains}>
{domains.length === 0 ? (
<EmptyContent
icon="Globe"
title="No Domains Created"
description="Start by creating a domain in order to see it on your list"
linkText="Configure your data domains"
onLinkClick={navigateToDomains}
/>
) : (
domains.map((domain) => (
<EntityItem
entity={domain.entity}
key={domain.entity.urn}
customDetailsRenderer={renderDomainCounts}
/>
))
)}
</LargeModule>
);
};
export default TopDomainsModule;

View File

@ -0,0 +1,111 @@
import { render } from '@testing-library/react';
import { act, renderHook } from '@testing-library/react-hooks';
import { createMemoryHistory } from 'history';
import React from 'react';
import { Router } from 'react-router-dom';
import { describe, expect, it, vi } from 'vitest';
import useDomainModuleUtils from '@app/homeV3/modules/domains/useDomainModuleUtils';
import { PageRoutes } from '@conf/Global';
import { Domain, Entity, EntityType } from '@types';
// Mock Text component
vi.mock('@components', () => ({
Text: ({ children }: { children: React.ReactNode }) => <span>{children}</span>,
}));
describe('useDomainModuleUtils', () => {
const domains = [
{
entity: {
urn: 'urn:li:domain:1',
dataProducts: { total: 2 },
type: EntityType.Domain,
id: '1',
} as Domain,
assetCount: 5,
},
{
entity: {
urn: 'urn:li:domain:2',
dataProducts: { total: 0 },
type: EntityType.Domain,
id: '2',
} as Domain,
assetCount: 0,
},
{
entity: {
urn: 'urn:li:domain:3',
dataProducts: { total: 0 },
type: EntityType.Domain,
id: '3',
} as Domain,
assetCount: 7,
},
];
it('should call history.push with correct route whe navigating to domains', () => {
const history = createMemoryHistory();
const wrapperWithHistory = ({ children }: { children: React.ReactNode }) => (
<Router history={history}>{children}</Router>
);
const { result } = renderHook(() => useDomainModuleUtils({ domains }), { wrapper: wrapperWithHistory });
act(() => {
result.current.navigateToDomains();
});
expect(history.location.pathname).toBe(PageRoutes.DOMAINS);
});
it('should return correct JSX for domain with assets and data products', () => {
const { result } = renderHook(() => useDomainModuleUtils({ domains }));
const entity: Entity = { urn: 'urn:li:domain:1' } as Entity;
const jsx = result.current.renderDomainCounts(entity);
const { container } = render(jsx);
expect(container.textContent).toContain('5 assets');
expect(container.textContent).toContain(', ');
expect(container.textContent).toContain('2 data products');
});
it('should return correct JSX for domain with assets but no data products', () => {
const { result } = renderHook(() => useDomainModuleUtils({ domains }));
const entity: Entity = { urn: 'urn:li:domain:3' } as Entity;
const jsx = result.current.renderDomainCounts(entity);
const { container } = render(jsx);
expect(container.textContent).not.toContain(', ');
expect(container.textContent).toContain('7 assets');
expect(container.textContent).not.toContain('data product');
});
it('should return correct JSX for domain with no assets or data products', () => {
const { result } = renderHook(() => useDomainModuleUtils({ domains }));
const entity: Entity = { urn: 'urn:li:domain:2' } as Entity;
const jsx = result.current.renderDomainCounts(entity);
const { container } = render(jsx);
expect(container.textContent).toBe('');
});
it('should return empty fragment for unknown domain', () => {
const { result } = renderHook(() => useDomainModuleUtils({ domains }));
const entity: Entity = { urn: 'urn:li:domain:unknown' } as Entity;
const jsx = result.current.renderDomainCounts(entity);
const { container } = render(jsx);
expect(container.textContent).toBe('');
});
});

View File

@ -0,0 +1,49 @@
import { Text } from '@components';
import React from 'react';
import { useHistory } from 'react-router';
import { formatNumber } from '@app/shared/formatNumber';
import { pluralize } from '@app/shared/textUtil';
import { PageRoutes } from '@conf/Global';
import { Domain, Entity } from '@types';
interface Props {
domains: {
entity: Domain;
assetCount: number;
}[];
}
const useDomainModuleUtils = ({ domains }: Props) => {
const history = useHistory();
const navigateToDomains = () => {
history.push(PageRoutes.DOMAINS);
};
const renderDomainCounts = (entity: Entity) => {
const domainEntity = domains.find((domain) => domain.entity.urn === entity.urn);
const assetCount = domainEntity?.assetCount || 0;
const dataProductCount = (domainEntity as any)?.entity?.dataProducts?.total || 0;
return (
<>
{assetCount > 0 && (
<Text size="sm" color="gray">
{formatNumber(assetCount)} {pluralize(assetCount, 'asset')}{' '}
</Text>
)}
{dataProductCount > 0 && (
<Text size="sm" color="gray">
, {formatNumber(dataProductCount)} data {pluralize(dataProductCount, 'product')}
</Text>
)}
</>
);
};
return { navigateToDomains, renderDomainCounts };
};
export default useDomainModuleUtils;

View File

@ -44,7 +44,6 @@ const ContentContainer = styled.div`
flex-direction: row;
gap: 16px;
overflow: hidden;
width: 100%;
`;
const DescriptionContainer = styled.div`
@ -80,6 +79,7 @@ interface EntityAutocompleteItemProps {
siblings?: Entity[];
matchedFields?: MatchedField[];
variant?: EntityItemVariant;
customDetailsRenderer?: (entity: Entity) => void;
}
export default function AutoCompleteEntityItem({
@ -88,6 +88,7 @@ export default function AutoCompleteEntityItem({
siblings,
matchedFields,
variant,
customDetailsRenderer,
}: EntityAutocompleteItemProps) {
const theme = useTheme();
const entityRegistry = useEntityRegistryV2();
@ -153,9 +154,13 @@ export default function AutoCompleteEntityItem({
</ContentContainer>
<TypeContainer>
<Text color={variantProps?.typeColor} colorLevel={variantProps?.typeColorLevel} size="sm">
{displayType}
</Text>
{customDetailsRenderer ? (
customDetailsRenderer(entity)
) : (
<Text color={variantProps?.typeColor} colorLevel={variantProps?.typeColorLevel} size="sm">
{displayType}
</Text>
)}
</TypeContainer>
</Container>
);

View File

@ -3,12 +3,15 @@ import React, { useMemo } from 'react';
import styled from 'styled-components';
import { getEntityPlatforms } from '@app/entityV2/shared/containers/profile/header/utils';
import { DomainColoredIcon } from '@app/entityV2/shared/links/DomainColoredIcon';
import { PlatformIcon } from '@app/searchV2/autoCompleteV2/components/icon/PlatformIcon';
import { SingleEntityIcon } from '@app/searchV2/autoCompleteV2/components/icon/SingleEntityIcon';
import { EntityIconProps } from '@app/searchV2/autoCompleteV2/components/icon/types';
import useUniqueEntitiesByPlatformUrn from '@app/searchV2/autoCompleteV2/components/icon/useUniqueEntitiesByPlatformUrn';
import { useEntityRegistryV2 } from '@app/useEntityRegistry';
import { Domain, EntityType } from '@types';
const Container = styled.div`
display: flex;
justify-content: center;
@ -28,6 +31,7 @@ const IconContainer = styled.div`
const ICON_SIZE = 20;
const SIBLING_ICON_SIZE = 16;
const DOMAIN_ICON_SIZE = 28;
export default function DefaultEntityIcon({ entity, siblings }: EntityIconProps) {
const entityRegistry = useEntityRegistryV2();
@ -42,6 +46,10 @@ export default function DefaultEntityIcon({ entity, siblings }: EntityIconProps)
const properties = entityRegistry.getGenericEntityProperties(entity.type, entity);
const { platforms } = getEntityPlatforms(entity.type, properties);
if (entity.type === EntityType.Domain) {
return <DomainColoredIcon domain={entity as Domain} size={DOMAIN_ICON_SIZE} />;
}
if (!hasSiblings && (platforms?.length ?? 0) > 1) {
return (
<Container>