mirror of
https://github.com/datahub-project/datahub.git
synced 2025-12-28 02:17:53 +00:00
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:
parent
aca7c103c8
commit
a5d4adcaab
@ -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;
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@ -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;
|
||||
@ -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('');
|
||||
});
|
||||
});
|
||||
@ -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;
|
||||
@ -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>
|
||||
);
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user