diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java index a535b6bf67..101ecdbf86 100644 --- a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/GmsGraphQLEngine.java @@ -202,6 +202,7 @@ import com.linkedin.datahub.graphql.resolvers.search.SearchResolver; import com.linkedin.datahub.graphql.resolvers.settings.applications.UpdateApplicationsSettingsResolver; import com.linkedin.datahub.graphql.resolvers.settings.docPropagation.DocPropagationSettingsResolver; import com.linkedin.datahub.graphql.resolvers.settings.docPropagation.UpdateDocPropagationSettingsResolver; +import com.linkedin.datahub.graphql.resolvers.settings.homePage.GlobalHomePageSettingsResolver; import com.linkedin.datahub.graphql.resolvers.settings.user.UpdateCorpUserViewsSettingsResolver; import com.linkedin.datahub.graphql.resolvers.settings.user.UpdateUserHomePageSettingsResolver; import com.linkedin.datahub.graphql.resolvers.settings.view.GlobalViewsSettingsResolver; @@ -771,6 +772,8 @@ public class GmsGraphQLEngine { configureMetadataAttributionResolver(builder); configureVersionPropertiesResolvers(builder); configureVersionSetResolvers(builder); + configureGlobalHomePageSettingsResolvers(builder); + configurePageTemplateRowResolvers(builder); } private void configureOrganisationRoleResolvers(RuntimeWiring.Builder builder) { @@ -1088,7 +1091,10 @@ public class GmsGraphQLEngine { "listBusinessAttributes", new ListBusinessAttributesResolver(this.entityClient)) .dataFetcher( "docPropagationSettings", - new DocPropagationSettingsResolver(this.settingsService))); + new DocPropagationSettingsResolver(this.settingsService)) + .dataFetcher( + "globalHomePageSettings", + new GlobalHomePageSettingsResolver(this.settingsService))); } private DataFetcher getEntitiesResolver() { @@ -3498,4 +3504,35 @@ public class GmsGraphQLEngine { "versionsSearch", new VersionsSearchResolver(this.entityClient, this.viewService))); } + + private void configureGlobalHomePageSettingsResolvers(final RuntimeWiring.Builder builder) { + builder.type( + "GlobalHomePageSettings", + typeWiring -> + typeWiring.dataFetcher( + "defaultTemplate", + new LoadableTypeResolver<>( + dataHubPageTemplateType, + (env) -> { + final GlobalHomePageSettings homePageSettings = env.getSource(); + return homePageSettings.getDefaultTemplate() != null + ? homePageSettings.getDefaultTemplate().getUrn() + : null; + }))); + } + + private void configurePageTemplateRowResolvers(final RuntimeWiring.Builder builder) { + builder.type( + "DataHubPageTemplateRow", + typeWiring -> + typeWiring.dataFetcher( + "modules", + new LoadableTypeBatchResolver<>( + dataHubPageModuleType, + (env) -> + ((DataHubPageTemplateRow) env.getSource()) + .getModules().stream() + .map(DataHubPageModule::getUrn) + .collect(Collectors.toList())))); + } } diff --git a/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/settings/homePage/GlobalHomePageSettingsResolver.java b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/settings/homePage/GlobalHomePageSettingsResolver.java new file mode 100644 index 0000000000..49fc4f648e --- /dev/null +++ b/datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/settings/homePage/GlobalHomePageSettingsResolver.java @@ -0,0 +1,63 @@ +package com.linkedin.datahub.graphql.resolvers.settings.homePage; + +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.concurrency.GraphQLConcurrencyUtils; +import com.linkedin.datahub.graphql.generated.DataHubPageTemplate; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.datahub.graphql.generated.GlobalHomePageSettings; +import com.linkedin.metadata.service.SettingsService; +import com.linkedin.settings.global.GlobalSettingsInfo; +import graphql.schema.DataFetcher; +import graphql.schema.DataFetchingEnvironment; +import java.util.Objects; +import java.util.concurrent.CompletableFuture; +import javax.annotation.Nonnull; +import lombok.extern.slf4j.Slf4j; + +/** Retrieves the Global Settings related to the Home Page feature. */ +@Slf4j +public class GlobalHomePageSettingsResolver + implements DataFetcher> { + + private final SettingsService _settingsService; + + public GlobalHomePageSettingsResolver(final SettingsService settingsService) { + _settingsService = Objects.requireNonNull(settingsService, "settingsService must not be null"); + } + + @Override + public CompletableFuture get(final DataFetchingEnvironment environment) + throws Exception { + final QueryContext context = environment.getContext(); + return GraphQLConcurrencyUtils.supplyAsync( + () -> { + try { + final GlobalSettingsInfo globalSettings = + _settingsService.getGlobalSettings(context.getOperationContext()); + final GlobalHomePageSettings defaultSettings = new GlobalHomePageSettings(); + return globalSettings != null && globalSettings.hasHomePage() + ? mapGlobalHomePageSettings(globalSettings.getHomePage()) + : defaultSettings; + } catch (Exception e) { + throw new RuntimeException("Failed to retrieve Global Home Page Settings", e); + } + }, + this.getClass().getSimpleName(), + "get"); + } + + private static GlobalHomePageSettings mapGlobalHomePageSettings( + @Nonnull final com.linkedin.settings.global.GlobalHomePageSettings settings) { + final GlobalHomePageSettings result = new GlobalHomePageSettings(); + + // Map defaultTemplate settings field + if (settings.hasDefaultTemplate()) { + DataHubPageTemplate template = new DataHubPageTemplate(); + template.setUrn(settings.getDefaultTemplate().toString()); + template.setType(EntityType.DATAHUB_PAGE_TEMPLATE); + result.setDefaultTemplate(template); + } + + return result; + } +} diff --git a/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/settings/homePage/GlobalHomePageSettingsResolverTest.java b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/settings/homePage/GlobalHomePageSettingsResolverTest.java new file mode 100644 index 0000000000..42ec898bac --- /dev/null +++ b/datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/resolvers/settings/homePage/GlobalHomePageSettingsResolverTest.java @@ -0,0 +1,117 @@ +package com.linkedin.datahub.graphql.resolvers.settings.homePage; + +import static com.linkedin.datahub.graphql.TestUtils.*; +import static org.mockito.ArgumentMatchers.any; +import static org.testng.Assert.*; + +import com.linkedin.common.urn.Urn; +import com.linkedin.common.urn.UrnUtils; +import com.linkedin.data.template.SetMode; +import com.linkedin.datahub.graphql.QueryContext; +import com.linkedin.datahub.graphql.generated.DataHubPageTemplate; +import com.linkedin.datahub.graphql.generated.EntityType; +import com.linkedin.metadata.service.SettingsService; +import com.linkedin.settings.global.GlobalHomePageSettings; +import com.linkedin.settings.global.GlobalSettingsInfo; +import graphql.schema.DataFetchingEnvironment; +import java.util.concurrent.CompletionException; +import org.mockito.Mockito; +import org.testng.Assert; +import org.testng.annotations.Test; + +public class GlobalHomePageSettingsResolverTest { + + private static final Urn TEST_TEMPLATE_URN = + UrnUtils.getUrn("urn:li:dataHubPageTemplate:test-template"); + private static final Urn TEST_USER_URN = UrnUtils.getUrn("urn:li:corpuser:test"); + + @Test + public void testGetSuccessNullSettings() throws Exception { + SettingsService mockService = initSettingsService(null); + GlobalHomePageSettingsResolver resolver = new GlobalHomePageSettingsResolver(mockService); + + QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString()); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + com.linkedin.datahub.graphql.generated.GlobalHomePageSettings result = + resolver.get(mockEnv).get(); + + Assert.assertNotNull(result); // Empty settings + Assert.assertNull(result.getDefaultTemplate()); + } + + @Test + public void testGetSuccessEmptySettings() throws Exception { + SettingsService mockService = initSettingsService(new GlobalHomePageSettings()); + GlobalHomePageSettingsResolver resolver = new GlobalHomePageSettingsResolver(mockService); + + QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString()); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + com.linkedin.datahub.graphql.generated.GlobalHomePageSettings result = + resolver.get(mockEnv).get(); + + Assert.assertNull(result.getDefaultTemplate()); + } + + @Test + public void testGetSuccessExistingSettings() throws Exception { + SettingsService mockService = + initSettingsService(new GlobalHomePageSettings().setDefaultTemplate(TEST_TEMPLATE_URN)); + GlobalHomePageSettingsResolver resolver = new GlobalHomePageSettingsResolver(mockService); + + QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString()); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + com.linkedin.datahub.graphql.generated.GlobalHomePageSettings result = + resolver.get(mockEnv).get(); + + Assert.assertNotNull(result.getDefaultTemplate()); + DataHubPageTemplate template = result.getDefaultTemplate(); + Assert.assertEquals(template.getUrn(), TEST_TEMPLATE_URN.toString()); + Assert.assertEquals(template.getType(), EntityType.DATAHUB_PAGE_TEMPLATE); + } + + @Test + public void testGetSuccessSettingsWithoutDefaultTemplate() throws Exception { + SettingsService mockService = initSettingsService(new GlobalHomePageSettings()); + GlobalHomePageSettingsResolver resolver = new GlobalHomePageSettingsResolver(mockService); + + QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString()); + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + com.linkedin.datahub.graphql.generated.GlobalHomePageSettings result = + resolver.get(mockEnv).get(); + + Assert.assertNull(result.getDefaultTemplate()); + } + + @Test + public void testGetException() throws Exception { + SettingsService mockService = Mockito.mock(SettingsService.class); + Mockito.doThrow(RuntimeException.class).when(mockService).getGlobalSettings(any()); + + GlobalHomePageSettingsResolver resolver = new GlobalHomePageSettingsResolver(mockService); + + DataFetchingEnvironment mockEnv = Mockito.mock(DataFetchingEnvironment.class); + QueryContext mockContext = getMockAllowContext(TEST_USER_URN.toString()); + Mockito.when(mockEnv.getContext()).thenReturn(mockContext); + + assertThrows(CompletionException.class, () -> resolver.get(mockEnv).join()); + } + + private static SettingsService initSettingsService( + GlobalHomePageSettings existingHomePageSettings) { + SettingsService mockService = Mockito.mock(SettingsService.class); + + Mockito.when(mockService.getGlobalSettings(any())) + .thenReturn( + new GlobalSettingsInfo().setHomePage(existingHomePageSettings, SetMode.IGNORE_NULL)); + + return mockService; + } +} diff --git a/datahub-web-react/src/app/AppProviders.tsx b/datahub-web-react/src/app/AppProviders.tsx index 684c084da7..7d86b47993 100644 --- a/datahub-web-react/src/app/AppProviders.tsx +++ b/datahub-web-react/src/app/AppProviders.tsx @@ -1,6 +1,7 @@ import React from 'react'; import EntityRegistryProvider from '@app/EntityRegistryProvider'; +import GlobalSettingsProvider from '@app/context/GlobalSettingsProvider'; import UserContextProvider from '@app/context/UserContextProvider'; import { NavBarProvider } from '@app/homeV2/layout/navBarRedesign/NavBarContext'; import SearchContextProvider from '@app/search/context/SearchContextProvider'; @@ -16,19 +17,21 @@ interface Props { export default function AppProviders({ children }: Props) { return ( - - - - - - - {children} - - - - - - + + + + + + + + {children} + + + + + + + ); } diff --git a/datahub-web-react/src/app/context/GlobalSettingsContext.tsx b/datahub-web-react/src/app/context/GlobalSettingsContext.tsx new file mode 100644 index 0000000000..f4b0f35129 --- /dev/null +++ b/datahub-web-react/src/app/context/GlobalSettingsContext.tsx @@ -0,0 +1,18 @@ +import React, { useContext } from 'react'; + +import { GetHomePageSettingsQuery } from '@graphql/app.generated'; + +export const DEFAULT_GLOBAL_SETTINGS = { + globalHomePageSettings: { + defaultTemplate: null, + }, +}; + +export const GlobalSettingsContext = React.createContext<{ + settings: GetHomePageSettingsQuery; + loaded: boolean; +}>({ settings: DEFAULT_GLOBAL_SETTINGS, loaded: false }); + +export function useGlobalSettings() { + return useContext(GlobalSettingsContext); +} diff --git a/datahub-web-react/src/app/context/GlobalSettingsProvider.tsx b/datahub-web-react/src/app/context/GlobalSettingsProvider.tsx new file mode 100644 index 0000000000..e8423f732a --- /dev/null +++ b/datahub-web-react/src/app/context/GlobalSettingsProvider.tsx @@ -0,0 +1,20 @@ +import React from 'react'; + +import { DEFAULT_GLOBAL_SETTINGS, GlobalSettingsContext } from '@app/context/GlobalSettingsContext'; + +import { useGetHomePageSettingsQuery } from '@graphql/app.generated'; + +export default function GlobalSettingsProvider({ children }: { children: React.ReactNode }) { + const { data } = useGetHomePageSettingsQuery(); + + return ( + + {children} + + ); +} diff --git a/datahub-web-react/src/app/homeV2/reference/sections/assets/useGetAssetsYouOwn.tsx b/datahub-web-react/src/app/homeV2/reference/sections/assets/useGetAssetsYouOwn.tsx index 86d581ad62..f7bcf85796 100644 --- a/datahub-web-react/src/app/homeV2/reference/sections/assets/useGetAssetsYouOwn.tsx +++ b/datahub-web-react/src/app/homeV2/reference/sections/assets/useGetAssetsYouOwn.tsx @@ -10,7 +10,11 @@ const MAX_ASSETS_TO_FETCH = 50; export const useGetAssetsYouOwn = (user?: CorpUser | null, count = MAX_ASSETS_TO_FETCH) => { const { groupUrns, loading: groupDataLoading } = useGetUserGroupUrns(user?.urn); - const { loading, data, error } = useGetSearchResultsForMultipleQuery({ + const { + loading: searchLoading, + data, + error, + } = useGetSearchResultsForMultipleQuery({ variables: { input: { query: '*', @@ -38,6 +42,7 @@ export const useGetAssetsYouOwn = (user?: CorpUser | null, count = MAX_ASSETS_TO const entities = originEntities.map((entity) => entityRegistry.getGenericEntityProperties(entity.type, entity)) || []; const total = data?.searchAcrossEntities?.total || 0; + const loading = searchLoading || groupDataLoading || !data; - return { originEntities, entities, loading: loading || groupDataLoading, error, total }; + return { originEntities, entities, loading, error, total }; }; diff --git a/datahub-web-react/src/app/homeV3/HomePageContent.tsx b/datahub-web-react/src/app/homeV3/HomePageContent.tsx index a9b66b79e3..955ed05caa 100644 --- a/datahub-web-react/src/app/homeV3/HomePageContent.tsx +++ b/datahub-web-react/src/app/homeV3/HomePageContent.tsx @@ -1,34 +1,26 @@ import React from 'react'; +import { useGlobalSettings } from '@app/context/GlobalSettingsContext'; +import { useUserContext } from '@app/context/useUserContext'; import { Announcements } from '@app/homeV3/announcements/Announcements'; -import Module from '@app/homeV3/module/Module'; -import { ModuleProps } from '@app/homeV3/module/types'; import { CenteredContainer, ContentContainer, ContentDiv } from '@app/homeV3/styledComponents'; - -const SAMPLE_MODULES: ModuleProps[] = [ - { - name: 'Your Assets', - description: 'These are assets you are the owner', - type: 'yourAssets', - visibility: 'personal', - }, - { - name: 'Sample large module', - description: 'Description of the sample module', - type: 'sampleLarge', - visibility: 'global', - }, -]; +import TemplateRow from '@app/homeV3/templateRow/TemplateRow'; const HomePageContent = () => { + const { settings } = useGlobalSettings(); + const { user } = useUserContext(); + + const template = user?.settings?.homePage?.pageTemplate || settings.globalHomePageSettings?.defaultTemplate; + return ( - {SAMPLE_MODULES.map((sampleModule) => ( - - ))} + {template?.properties.rows.map((row, i) => { + const key = `templateRow-${i}`; + return ; + })} diff --git a/datahub-web-react/src/app/homeV3/module/Module.tsx b/datahub-web-react/src/app/homeV3/module/Module.tsx index be6d149f72..978c6ab53c 100644 --- a/datahub-web-react/src/app/homeV3/module/Module.tsx +++ b/datahub-web-react/src/app/homeV3/module/Module.tsx @@ -4,13 +4,16 @@ import { ModuleProps } from '@app/homeV3/module/types'; import SampleLargeModule from '@app/homeV3/modules/SampleLargeModule'; import YourAssetsModule from '@app/homeV3/modules/YourAssetsModule'; -export default function Module(props: ModuleProps) { - const Component = useMemo(() => { - // TODO: implement logic to map props.type to component - if (props.type === 'sampleLarge') return SampleLargeModule; - if (props.type === 'yourAssets') return YourAssetsModule; - return SampleLargeModule; - }, [props.type]); +import { DataHubPageModuleType } from '@types'; - return ; +export default function Module(props: ModuleProps) { + const { module } = props; + const Component = useMemo(() => { + if (module.properties.type === DataHubPageModuleType.OwnedAssets) return YourAssetsModule; + // 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; + }, [module.properties.type]); + + return ; } diff --git a/datahub-web-react/src/app/homeV3/module/components/LargeModule.tsx b/datahub-web-react/src/app/homeV3/module/components/LargeModule.tsx index c59877418f..a213fd5120 100644 --- a/datahub-web-react/src/app/homeV3/module/components/LargeModule.tsx +++ b/datahub-web-react/src/app/homeV3/module/components/LargeModule.tsx @@ -3,10 +3,8 @@ import React from 'react'; import styled from 'styled-components'; import ModuleContainer from '@app/homeV3/module/components/ModuleContainer'; -import ModuleDescription from '@app/homeV3/module/components/ModuleDescription'; import ModuleMenu from '@app/homeV3/module/components/ModuleMenu'; import ModuleName from '@app/homeV3/module/components/ModuleName'; -import PublicModuleBadge from '@app/homeV3/module/components/PublicModuleBadge'; import { ModuleProps } from '@app/homeV3/module/types'; const ModuleHeader = styled.div` @@ -51,20 +49,15 @@ interface Props extends ModuleProps { loading?: boolean; } -export default function LargeModule({ - children, - name, - description, - visibility, - loading, -}: React.PropsWithChildren) { +export default function LargeModule({ children, module, loading }: React.PropsWithChildren) { + const { name } = module.properties; return ( - + {/* TODO: implement description for modules CH-548 */} + {/* */} - diff --git a/datahub-web-react/src/app/homeV3/module/components/ModuleContainer.tsx b/datahub-web-react/src/app/homeV3/module/components/ModuleContainer.tsx index 1c9f845dc3..0ce69f67a7 100644 --- a/datahub-web-react/src/app/homeV3/module/components/ModuleContainer.tsx +++ b/datahub-web-react/src/app/homeV3/module/components/ModuleContainer.tsx @@ -5,6 +5,7 @@ const ModuleContainer = styled.div<{ $height: string }>` background: ${colors.white}; border: ${borders['1px']} ${colors.gray[100]}; border-radius: ${radius.lg}; + flex: 1; height: ${(props) => props.$height}; box-shadow: diff --git a/datahub-web-react/src/app/homeV3/module/types.ts b/datahub-web-react/src/app/homeV3/module/types.ts index 748c94e7ba..f9f10127f9 100644 --- a/datahub-web-react/src/app/homeV3/module/types.ts +++ b/datahub-web-react/src/app/homeV3/module/types.ts @@ -1,9 +1,7 @@ // TODO: adapt to DataHubPageModuleProperties +import { PageModuleFragment } from '@graphql/template.generated'; + // the current props are just to draft some components export interface ModuleProps { - name: string; - type: string; - visibility: 'personal' | 'global'; - description?: string; - isPublic?: boolean; + module: PageModuleFragment; } diff --git a/datahub-web-react/src/app/homeV3/templateRow/TemplateRow.tsx b/datahub-web-react/src/app/homeV3/templateRow/TemplateRow.tsx new file mode 100644 index 0000000000..a6dea03d01 --- /dev/null +++ b/datahub-web-react/src/app/homeV3/templateRow/TemplateRow.tsx @@ -0,0 +1,27 @@ +import React from 'react'; +import styled from 'styled-components'; + +import Module from '@app/homeV3/module/Module'; + +import { PageTemplateRowFragment } from '@graphql/template.generated'; + +const RowWrapper = styled.div` + display: flex; + gap: 16px; + width: 100%; + flex: 1; +`; + +interface Props { + row: PageTemplateRowFragment; +} + +export default function TemplateRow({ row }: Props) { + return ( + + {row.modules.map((module) => ( + + ))} + + ); +} diff --git a/datahub-web-react/src/graphql/app.graphql b/datahub-web-react/src/graphql/app.graphql index c792753221..8fb65f9343 100644 --- a/datahub-web-react/src/graphql/app.graphql +++ b/datahub-web-react/src/graphql/app.graphql @@ -132,6 +132,14 @@ query getDocPropagationSettings { } } +query getHomePageSettings { + globalHomePageSettings { + defaultTemplate { + ...pageTemplateFields + } + } +} + mutation updateGlobalViewsSettings($input: UpdateGlobalViewsSettingsInput!) { updateGlobalViewsSettings(input: $input) } diff --git a/datahub-web-react/src/graphql/me.graphql b/datahub-web-react/src/graphql/me.graphql index 99c932c7a5..6df6341576 100644 --- a/datahub-web-react/src/graphql/me.graphql +++ b/datahub-web-react/src/graphql/me.graphql @@ -37,7 +37,7 @@ query getMe { } homePage { pageTemplate { - urn + ...pageTemplateFields } dismissedAnnouncementUrns } diff --git a/datahub-web-react/src/graphql/template.graphql b/datahub-web-react/src/graphql/template.graphql new file mode 100644 index 0000000000..7991d596d9 --- /dev/null +++ b/datahub-web-react/src/graphql/template.graphql @@ -0,0 +1,33 @@ +fragment pageTemplateFields on DataHubPageTemplate { + urn + type + properties { + rows { + ...PageTemplateRow + } + surface { + surfaceType + } + visibility { + scope + } + } +} + +fragment PageTemplateRow on DataHubPageTemplateRow { + modules { + ...PageModule + } +} + +fragment PageModule on DataHubPageModule { + urn + type + properties { + name + type + visibility { + scope + } + } +}