From 8f21a6c6ce005e2988d0c6d6d7440b50a3df063c Mon Sep 17 00:00:00 2001 From: purnimagarg1 <139125209+purnimagarg1@users.noreply.github.com> Date: Wed, 22 Oct 2025 01:07:58 +0530 Subject: [PATCH] feat(homepage): add default platforms module on the homepage (#14942) Co-authored-by: Chris Collins --- .../src/main/resources/module.graphql | 4 + .../sections/platform/useGetPlatforms.tsx | 9 +- .../src/app/homeV3/module/Module.tsx | 2 + .../homeV3/module/components/EntityItem.tsx | 74 +++++++++------- .../src/app/homeV3/modules/constants.ts | 1 + .../modules/platforms/PlatformsModule.tsx | 77 ++++++++++++++++ .../__tests__/usePlatformsModuleUtils.test.ts | 87 +++++++++++++++++++ .../platforms/usePlatformsModuleUtils.ts | 34 ++++++++ .../__tests__/useAddModuleMenu.test.tsx | 6 +- .../addModuleMenu/useAddModuleMenu.tsx | 30 ++++++- .../autoCompleteV2/AutoCompleteEntityItem.tsx | 82 ++++++++++++----- .../linkedin/module/DataHubPageModuleType.pdl | 5 ++ .../src/main/resources/bootstrap_mcps.yaml | 2 +- .../bootstrap_mcps/page-modules.yaml | 12 +++ .../bootstrap_mcps/page-templates.yaml | 1 + .../metadata/service/PageModuleService.java | 3 +- 16 files changed, 368 insertions(+), 61 deletions(-) create mode 100644 datahub-web-react/src/app/homeV3/modules/platforms/PlatformsModule.tsx create mode 100644 datahub-web-react/src/app/homeV3/modules/platforms/__tests__/usePlatformsModuleUtils.test.ts create mode 100644 datahub-web-react/src/app/homeV3/modules/platforms/usePlatformsModuleUtils.ts diff --git a/datahub-graphql-core/src/main/resources/module.graphql b/datahub-graphql-core/src/main/resources/module.graphql index 23e0e4fe3a..69860c0735 100644 --- a/datahub-graphql-core/src/main/resources/module.graphql +++ b/datahub-graphql-core/src/main/resources/module.graphql @@ -247,6 +247,10 @@ enum DataHubPageModuleType { Module displaying the related terms of a given glossary term """ RELATED_TERMS + """ + Module displaying the platforms in the instance + """ + PLATFORMS } """ diff --git a/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/platform/useGetPlatforms.tsx b/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/platform/useGetPlatforms.tsx index 4ecfe7bfd1..bdf3683527 100644 --- a/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/platform/useGetPlatforms.tsx +++ b/datahub-web-react/src/app/homeV2/content/tabs/discovery/sections/platform/useGetPlatforms.tsx @@ -5,12 +5,17 @@ import { CorpUser, DataPlatform, ScenarioType } from '@types'; export const PLATFORMS_MODULE_ID = 'Platforms'; +const MAX_PLATFORMS_TO_FETCH = 10; + export type PlatformAndCount = { platform: DataPlatform; count: number; }; -export const useGetPlatforms = (user?: CorpUser | null): { platforms: PlatformAndCount[]; loading: boolean } => { +export const useGetPlatforms = ( + user?: CorpUser | null, + maxToFetch?: number, +): { platforms: PlatformAndCount[]; loading: boolean } => { const { localState } = useUserContext(); const { selectedViewUrn } = localState; const { data, loading } = useListRecommendationsQuery({ @@ -20,7 +25,7 @@ export const useGetPlatforms = (user?: CorpUser | null): { platforms: PlatformAn requestContext: { scenario: ScenarioType.Home, }, - limit: 10, + limit: maxToFetch || MAX_PLATFORMS_TO_FETCH, viewUrn: selectedViewUrn, }, }, diff --git a/datahub-web-react/src/app/homeV3/module/Module.tsx b/datahub-web-react/src/app/homeV3/module/Module.tsx index ba270dd7ee..48e2c34f67 100644 --- a/datahub-web-react/src/app/homeV3/module/Module.tsx +++ b/datahub-web-react/src/app/homeV3/module/Module.tsx @@ -14,6 +14,7 @@ import DocumentationModule from '@app/homeV3/modules/documentation/Documentation import TopDomainsModule from '@app/homeV3/modules/domains/TopDomainsModule'; import HierarchyViewModule from '@app/homeV3/modules/hierarchyViewModule/HierarchyViewModule'; import LinkModule from '@app/homeV3/modules/link/LinkModule'; +import PlatformsModule from '@app/homeV3/modules/platforms/PlatformsModule'; import { DataHubPageModuleType } from '@types'; @@ -32,6 +33,7 @@ function Module(props: ModuleProps) { if (module.properties.type === DataHubPageModuleType.ChildHierarchy) return ChildHierarchyModule; if (module.properties.type === DataHubPageModuleType.DataProducts) return DataProductsModule; if (module.properties.type === DataHubPageModuleType.RelatedTerms) return RelatedTermsModule; + if (module.properties.type === DataHubPageModuleType.Platforms) return PlatformsModule; // 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}`); diff --git a/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx b/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx index 5fbbe0b1bd..d580fa3548 100644 --- a/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx +++ b/datahub-web-react/src/app/homeV3/module/components/EntityItem.tsx @@ -23,6 +23,10 @@ interface Props { hideSubtitle?: boolean; hideMatches?: boolean; padding?: string; + // For custom click action on entity (either entire container or just name depending on navigateOnlyOnNameClick) + customOnEntityClick?: (entity: Entity) => void; + // For custom hover action on entity name + customHoverEntityName?: (entity: Entity, children: React.ReactNode) => React.ReactNode; } export default function EntityItem({ @@ -34,6 +38,8 @@ export default function EntityItem({ hideSubtitle, hideMatches, padding, + customOnEntityClick, + customHoverEntityName, }: Props) { const entityRegistry = useEntityRegistryV2(); const linkProps = useGetModalLinkProps(); @@ -50,37 +56,43 @@ export default function EntityItem({ [entity.urn, moduleType, templateType], ); + const autoCompleteItemProps = { + entity, + key: entity.urn, + hideSubtitle, + hideMatches, + padding, + customDetailsRenderer, + dragIconRenderer, + customHoverEntityName, + navigateOnlyOnNameClick, + customOnEntityClick, + }; + + if (customOnEntityClick && !navigateOnlyOnNameClick) { + return ( +
customOnEntityClick(entity)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + customOnEntityClick(entity); + } + }} + > + +
+ ); + } + + if (navigateOnlyOnNameClick) { + return ; + } + return ( - <> - {navigateOnlyOnNameClick ? ( - - ) : ( - - - - )} - + + + ); } diff --git a/datahub-web-react/src/app/homeV3/modules/constants.ts b/datahub-web-react/src/app/homeV3/modules/constants.ts index dee67f7d6e..4bb7562e95 100644 --- a/datahub-web-react/src/app/homeV3/modules/constants.ts +++ b/datahub-web-react/src/app/homeV3/modules/constants.ts @@ -33,6 +33,7 @@ export const DEFAULT_MODULE_URNS = [ 'urn:li:dataHubPageModule:child_hierarchy', 'urn:li:dataHubPageModule:data_products', 'urn:li:dataHubPageModule:related_terms', + 'urn:li:dataHubPageModule:platforms', ]; export const DEFAULT_TEMPLATE_URN = 'urn:li:dataHubPageTemplate:home_default_1'; diff --git a/datahub-web-react/src/app/homeV3/modules/platforms/PlatformsModule.tsx b/datahub-web-react/src/app/homeV3/modules/platforms/PlatformsModule.tsx new file mode 100644 index 0000000000..15ab917f89 --- /dev/null +++ b/datahub-web-react/src/app/homeV3/modules/platforms/PlatformsModule.tsx @@ -0,0 +1,77 @@ +import { Text, Tooltip } from '@components'; +import React from 'react'; + +import { useUserContext } from '@app/context/useUserContext'; +import { useGetPlatforms } from '@app/homeV2/content/tabs/discovery/sections/platform/useGetPlatforms'; +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 usePlatformModuleUtils from '@app/homeV3/modules/platforms/usePlatformsModuleUtils'; +import { formatNumber, formatNumberWithoutAbbreviation } from '@app/shared/formatNumber'; + +import { DataHubPageModuleType, Entity } from '@types'; + +const NUMBER_OF_PLATFORMS = 15; + +const PlatformsModule = (props: ModuleProps) => { + const { user } = useUserContext(); + const { platforms, loading } = useGetPlatforms(user, NUMBER_OF_PLATFORMS); + const { navigateToDataSources, handleEntityClick } = usePlatformModuleUtils(); + + const renderAssetCount = (entity: Entity) => { + const platformEntity = platforms.find((platform) => platform.platform.urn === entity.urn); + const assetCount = platformEntity?.count || 0; + + return ( + <> + {assetCount > 0 && ( + + {formatNumber(assetCount)} + + )} + + ); + }; + + const renderCustomTooltip = (entity: Entity, children: React.ReactNode) => { + const platformEntity = platforms.find((platform) => platform.platform.urn === entity.urn); + return ( + + {children} + + ); + }; + + return ( + + {platforms.length === 0 ? ( + + ) : ( +
+ {platforms.map((platform) => ( + + ))} +
+ )} +
+ ); +}; + +export default PlatformsModule; diff --git a/datahub-web-react/src/app/homeV3/modules/platforms/__tests__/usePlatformsModuleUtils.test.ts b/datahub-web-react/src/app/homeV3/modules/platforms/__tests__/usePlatformsModuleUtils.test.ts new file mode 100644 index 0000000000..e0d09161d8 --- /dev/null +++ b/datahub-web-react/src/app/homeV3/modules/platforms/__tests__/usePlatformsModuleUtils.test.ts @@ -0,0 +1,87 @@ +import { act, renderHook } from '@testing-library/react-hooks'; +import { type Mock, vi } from 'vitest'; + +import usePlatformsModuleUtils from '@app/homeV3/modules/platforms/usePlatformsModuleUtils'; +import { PLATFORM_FILTER_NAME } from '@app/search/utils/constants'; +import { navigateToSearchUrl } from '@app/searchV2/utils/navigateToSearchUrl'; +import { PageRoutes } from '@conf/Global'; + +// Mocks +const mockPush = vi.fn(); +const mockHistory = { push: mockPush }; + +vi.mock('react-router', () => ({ + useHistory: () => mockHistory, +})); + +vi.mock('@app/searchV2/utils/navigateToSearchUrl', () => ({ + navigateToSearchUrl: vi.fn(), +})); + +const entityMock = { urn: 'urn:li:entity', type: 'test-type' } as any; + +describe('usePlatformsModuleUtils', () => { + beforeEach(() => { + mockPush.mockClear(); + (navigateToSearchUrl as unknown as Mock).mockClear(); + }); + + it('should provide navigateToDataSources', () => { + const { result } = renderHook(() => usePlatformsModuleUtils()); + expect(result.current.navigateToDataSources).toBeInstanceOf(Function); + }); + + it('should provide handleEntityClick', () => { + const { result } = renderHook(() => usePlatformsModuleUtils()); + expect(result.current.handleEntityClick).toBeInstanceOf(Function); + }); + + it('navigateToDataSources should call history.push with INGESTION route', () => { + const { result } = renderHook(() => usePlatformsModuleUtils()); + act(() => { + result.current.navigateToDataSources(); + }); + expect(mockPush).toHaveBeenCalledWith({ pathname: PageRoutes.INGESTION }); + }); + + it('handleEntityClick should call navigateToSearchUrl with correct params', () => { + const { result } = renderHook(() => usePlatformsModuleUtils()); + act(() => { + result.current.handleEntityClick(entityMock); + }); + expect(navigateToSearchUrl).toHaveBeenCalledWith({ + history: mockHistory, + filters: [ + { + field: PLATFORM_FILTER_NAME, + values: [entityMock.urn], + }, + ], + }); + }); + + it('handleEntityClick should call navigateToSearchUrl with different entity', () => { + const anotherEntity = { urn: 'urn:li:other-entity', type: 'other-type' } as any; + const { result } = renderHook(() => usePlatformsModuleUtils()); + act(() => { + result.current.handleEntityClick(anotherEntity); + }); + expect(navigateToSearchUrl).toHaveBeenCalledWith({ + history: mockHistory, + filters: [ + { + field: PLATFORM_FILTER_NAME, + values: [anotherEntity.urn], + }, + ], + }); + }); + + it('should not call navigateToSearchUrl if entity is undefined', () => { + const { result } = renderHook(() => usePlatformsModuleUtils()); + act(() => { + result.current.handleEntityClick(undefined as any); + }); + expect(navigateToSearchUrl).not.toHaveBeenCalled(); + }); +}); diff --git a/datahub-web-react/src/app/homeV3/modules/platforms/usePlatformsModuleUtils.ts b/datahub-web-react/src/app/homeV3/modules/platforms/usePlatformsModuleUtils.ts new file mode 100644 index 0000000000..33b2dd9fbb --- /dev/null +++ b/datahub-web-react/src/app/homeV3/modules/platforms/usePlatformsModuleUtils.ts @@ -0,0 +1,34 @@ +import { useHistory } from 'react-router'; + +import { PLATFORM_FILTER_NAME } from '@app/search/utils/constants'; +import { navigateToSearchUrl } from '@app/searchV2/utils/navigateToSearchUrl'; +import { PageRoutes } from '@conf/Global'; + +import { Entity } from '@types'; + +const usePlatformsModuleUtils = () => { + const history = useHistory(); + + const navigateToDataSources = () => { + history.push({ + pathname: `${PageRoutes.INGESTION}`, + }); + }; + + const handleEntityClick = (entity: Entity) => { + if (!entity?.urn) return; + navigateToSearchUrl({ + history, + filters: [ + { + field: PLATFORM_FILTER_NAME, + values: [entity.urn], + }, + ], + }); + }; + + return { navigateToDataSources, handleEntityClick }; +}; + +export default usePlatformsModuleUtils; diff --git a/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/__tests__/useAddModuleMenu.test.tsx b/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/__tests__/useAddModuleMenu.test.tsx index 527e010274..dd9ddd6b08 100644 --- a/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/__tests__/useAddModuleMenu.test.tsx +++ b/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/__tests__/useAddModuleMenu.test.tsx @@ -155,11 +155,13 @@ describe('useAddModuleMenu', () => { // Check "Default" group - HomePage should have yourAssets and domains expect(items?.[1]).toHaveProperty('key', 'customLargeModulesGroup'); // @ts-expect-error SubMenuItem should have children - expect(items?.[1]?.children).toHaveLength(2); + expect(items?.[1]?.children).toHaveLength(3); // @ts-expect-error SubMenuItem should have children expect(items?.[1]?.children?.[0]).toHaveProperty('key', 'your-assets'); // @ts-expect-error SubMenuItem should have children expect(items?.[1]?.children?.[1]).toHaveProperty('key', 'domains'); + // @ts-expect-error SubMenuItem should have children + expect(items?.[1]?.children?.[2]).toHaveProperty('key', 'platforms'); }); it('should include admin created modules when available in global template for HomePage', () => { @@ -576,7 +578,7 @@ describe('useAddModuleMenu', () => { // Default modules should be HomePage defaults (not entity-specific) const defaultChildren = getChildren(result.current.items?.[1]); - expect(defaultChildren).toHaveLength(2); + expect(defaultChildren).toHaveLength(3); expect(defaultChildren[0]).toHaveProperty('key', 'your-assets'); expect(defaultChildren[1]).toHaveProperty('key', 'domains'); }); diff --git a/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/useAddModuleMenu.tsx b/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/useAddModuleMenu.tsx index d0ed76e20f..0543d169d9 100644 --- a/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/useAddModuleMenu.tsx +++ b/datahub-web-react/src/app/homeV3/template/components/addModuleMenu/useAddModuleMenu.tsx @@ -38,6 +38,17 @@ const DOMAINS_MODULE: PageModuleFragment = { }, }; +const PLATFORMS_MODULE: PageModuleFragment = { + urn: 'urn:li:dataHubPageModule:platforms', + type: EntityType.DatahubPageModule, + properties: { + name: 'Platforms', + type: DataHubPageModuleType.Platforms, + visibility: { scope: PageModuleScope.Global }, + params: {}, + }, +}; + export const ASSETS_MODULE: PageModuleFragment = { urn: 'urn:li:dataHubPageModule:assets', type: EntityType.DatahubPageModule, @@ -221,6 +232,23 @@ export default function useAddModuleMenu(position: ModulePositionInput, closeMen 'data-testid': 'add-domains-module', }; + const platforms = { + name: 'Platforms', + key: 'platforms', + label: ( + + ), + onClick: () => { + handleAddExistingModule(PLATFORMS_MODULE); + }, + 'data-testid': 'add-platforms-module', + }; + const assets = { name: 'Assets', key: 'assets', @@ -289,7 +317,7 @@ export default function useAddModuleMenu(position: ModulePositionInput, closeMen 'data-testid': 'add-related-terms-module', }; - const defaultHomeModules = [yourAssets, domains]; + const defaultHomeModules = [yourAssets, domains, platforms]; // TODO: make this a function to pull out and write unit tests for let defaultSummaryModules = [assets]; if (entityType === EntityType.Domain) { diff --git a/datahub-web-react/src/app/searchV2/autoCompleteV2/AutoCompleteEntityItem.tsx b/datahub-web-react/src/app/searchV2/autoCompleteV2/AutoCompleteEntityItem.tsx index abe604a906..806e577991 100644 --- a/datahub-web-react/src/app/searchV2/autoCompleteV2/AutoCompleteEntityItem.tsx +++ b/datahub-web-react/src/app/searchV2/autoCompleteV2/AutoCompleteEntityItem.tsx @@ -115,6 +115,8 @@ interface EntityAutocompleteItemProps { hideMatches?: boolean; padding?: string; onClick?: () => void; + customHoverEntityName?: (entity: Entity, children: React.ReactNode) => React.ReactNode; + customOnEntityClick?: (entity: Entity) => void; dataTestId?: string; } @@ -132,6 +134,8 @@ export default function AutoCompleteEntityItem({ hideMatches, padding, onClick, + customHoverEntityName, + customOnEntityClick, dataTestId, }: EntityAutocompleteItemProps) { const theme = useTheme(); @@ -146,27 +150,55 @@ export default function AutoCompleteEntityItem({ ? DisplayNameHoverFromSelf : DisplayNameHoverFromContainer; - const displayNameContent = variantProps?.nameCanBeHovered ? ( - - customOnEntityClick(entity)} + onKeyDown={(e) => { + if (e.key === 'Enter' || e.key === ' ') { + customOnEntityClick(entity); + } + }} + > + + + ); + } else if (variantProps?.nameCanBeHovered) { + displayNameContent = ( + + + + ); + } else { + displayNameContent = ( + - - ) : ( - - ); + ); + } return ( - - {displayNameContent} - + {customHoverEntityName ? ( + customHoverEntityName(entity, {displayNameContent}) + ) : ( + + {displayNameContent} + + )} {!hideSubtitle && (