From 169cd4fde94681ed261c44270ccd6c52d3fcf380 Mon Sep 17 00:00:00 2001 From: John Joyce Date: Wed, 2 Feb 2022 13:51:39 -0800 Subject: [PATCH] feat(containers): Adding Containers UI (as demo'd in Jan Townhall) (#4037) --- datahub-web-react/src/App.tsx | 3 +- datahub-web-react/src/Mocks.tsx | 3 + .../app/entity/chart/preview/ChartPreview.tsx | 4 + .../entity/container/ContainerEntitiesTab.tsx | 20 +++ .../app/entity/container/ContainerEntity.tsx | 159 ++++++++++++++++++ .../app/entity/container/preview/Preview.tsx | 50 ++++++ .../app/entity/dashboard/DashboardEntity.tsx | 2 + .../dashboard/preview/DashboardPreview.tsx | 4 + .../src/app/entity/dataset/DatasetEntity.tsx | 8 +- .../app/entity/dataset/preview/Preview.tsx | 12 +- .../containers/profile/EntityProfile.tsx | 1 - .../profile/header/EntityHeader.tsx | 67 +++++++- .../src/app/entity/shared/types.ts | 5 + .../src/app/entity/shared/utils.ts | 8 + .../src/app/preview/DefaultPreviewCard.tsx | 111 +++++++++--- .../__tests__/Recommendations.test.tsx | 2 + .../renderer/component/EntityNameList.tsx | 5 +- .../src/app/search/SearchFilterLabel.tsx | 17 +- .../src/app/search/SearchPage.tsx | 1 - .../utils/filtersToQueryStringParams.ts | 3 +- .../src/app/search/utils/useFilters.ts | 5 +- datahub-web-react/src/app/shared/textUtil.ts | 7 + datahub-web-react/src/graphql/chart.graphql | 16 +- .../src/graphql/container.graphql | 36 ++++ .../src/graphql/dataFlow.graphql | 13 +- datahub-web-react/src/graphql/dataset.graphql | 3 + datahub-web-react/src/graphql/domain.graphql | 2 +- .../src/graphql/fragments.graphql | 113 +++++-------- datahub-web-react/src/graphql/preview.graphql | 3 + datahub-web-react/src/graphql/search.graphql | 52 ++++++ .../examples/mce_files/test_containers.json | 30 +++- .../examples/recipes/mysql_to_datahub.yml | 2 + 32 files changed, 621 insertions(+), 146 deletions(-) create mode 100644 datahub-web-react/src/app/entity/container/ContainerEntitiesTab.tsx create mode 100644 datahub-web-react/src/app/entity/container/ContainerEntity.tsx create mode 100644 datahub-web-react/src/app/entity/container/preview/Preview.tsx create mode 100644 datahub-web-react/src/graphql/container.graphql diff --git a/datahub-web-react/src/App.tsx b/datahub-web-react/src/App.tsx index 252b87ed237cf5..0e122e9d320c82 100644 --- a/datahub-web-react/src/App.tsx +++ b/datahub-web-react/src/App.tsx @@ -5,7 +5,6 @@ import { BrowserRouter as Router } from 'react-router-dom'; import { ApolloClient, ApolloProvider, createHttpLink, InMemoryCache, ServerError } from '@apollo/client'; import { onError } from '@apollo/client/link/error'; import { ThemeProvider } from 'styled-components'; - import './App.less'; import { Routes } from './app/Routes'; import EntityRegistry from './app/entity/EntityRegistry'; @@ -30,6 +29,7 @@ import { MLFeatureTableEntity } from './app/entity/mlFeatureTable/MLFeatureTable import { MLModelEntity } from './app/entity/mlModel/MLModelEntity'; import { MLModelGroupEntity } from './app/entity/mlModelGroup/MLModelGroupEntity'; import { DomainEntity } from './app/entity/domain/DomainEntity'; +import { ContainerEntity } from './app/entity/container/ContainerEntity'; /* Construct Apollo Client @@ -96,6 +96,7 @@ const App: React.VFC = () => { register.register(new MLModelEntity()); register.register(new MLModelGroupEntity()); register.register(new DomainEntity()); + register.register(new ContainerEntity()); return register; }, []); diff --git a/datahub-web-react/src/Mocks.tsx b/datahub-web-react/src/Mocks.tsx index aa8307d853c27b..d1ff9ec03e3247 100644 --- a/datahub-web-react/src/Mocks.tsx +++ b/datahub-web-react/src/Mocks.tsx @@ -189,6 +189,7 @@ const dataset1 = { }, ], domain: null, + container: null, }; const dataset2 = { @@ -260,6 +261,7 @@ const dataset2 = { }, ], domain: null, + container: null, }; export const dataset3 = { @@ -447,6 +449,7 @@ export const dataset3 = { }, ], domain: null, + container: null, } as Dataset; export const dataset4 = { diff --git a/datahub-web-react/src/app/entity/chart/preview/ChartPreview.tsx b/datahub-web-react/src/app/entity/chart/preview/ChartPreview.tsx index a73531b55614e7..401bb07b511227 100644 --- a/datahub-web-react/src/app/entity/chart/preview/ChartPreview.tsx +++ b/datahub-web-react/src/app/entity/chart/preview/ChartPreview.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { AccessLevel, Domain, + Container, EntityType, GlobalTags, GlossaryTerms, @@ -22,6 +23,7 @@ export const ChartPreview = ({ tags, glossaryTerms, domain, + container, insights, logoUrl, }: { @@ -34,6 +36,7 @@ export const ChartPreview = ({ tags?: GlobalTags; glossaryTerms?: GlossaryTerms | null; domain?: Domain | null; + container?: Container | null; insights?: Array | null; logoUrl?: string | null; }): JSX.Element => { @@ -53,6 +56,7 @@ export const ChartPreview = ({ owners={owners} glossaryTerms={glossaryTerms || undefined} domain={domain} + container={container || undefined} insights={insights} /> ); diff --git a/datahub-web-react/src/app/entity/container/ContainerEntitiesTab.tsx b/datahub-web-react/src/app/entity/container/ContainerEntitiesTab.tsx new file mode 100644 index 00000000000000..45161a99a1067e --- /dev/null +++ b/datahub-web-react/src/app/entity/container/ContainerEntitiesTab.tsx @@ -0,0 +1,20 @@ +import React from 'react'; +import { useEntityData } from '../shared/EntityContext'; +import { EmbeddedListSearch } from '../shared/components/styled/search/EmbeddedListSearch'; + +export const ContainerEntitiesTab = () => { + const { urn } = useEntityData(); + + const fixedFilter = { + field: 'container', + value: urn, + }; + + return ( + + ); +}; diff --git a/datahub-web-react/src/app/entity/container/ContainerEntity.tsx b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx new file mode 100644 index 00000000000000..1f6e31b9182bfb --- /dev/null +++ b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx @@ -0,0 +1,159 @@ +import * as React from 'react'; +import { FolderOutlined } from '@ant-design/icons'; +import { Container, EntityType, SearchResult } from '../../../types.generated'; +import { Entity, IconStyleType, PreviewType } from '../Entity'; +import { Preview } from './preview/Preview'; +import { EntityProfile } from '../shared/containers/profile/EntityProfile'; +import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab'; +import { SidebarAboutSection } from '../shared/containers/profile/sidebar/SidebarAboutSection'; +import { SidebarOwnerSection } from '../shared/containers/profile/sidebar/Ownership/SidebarOwnerSection'; +import { getDataForEntityType } from '../shared/containers/profile/utils'; +import { useGetContainerQuery } from '../../../graphql/container.generated'; +import { ContainerEntitiesTab } from './ContainerEntitiesTab'; +import { SidebarRecommendationsSection } from '../shared/containers/profile/sidebar/Recommendations/SidebarRecommendationsSection'; +import { SidebarTagsSection } from '../shared/containers/profile/sidebar/SidebarTagsSection'; +import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import { SidebarDomainSection } from '../shared/containers/profile/sidebar/Domain/SidebarDomainSection'; + +/** + * Definition of the DataHub Container entity. + */ +export class ContainerEntity implements Entity { + type: EntityType = EntityType.Container; + + icon = (fontSize: number, styleType: IconStyleType) => { + if (styleType === IconStyleType.TAB_VIEW) { + return ; + } + + if (styleType === IconStyleType.HIGHLIGHT) { + return ; + } + + if (styleType === IconStyleType.SVG) { + return ( + + ); + } + + return ( + + ); + }; + + isSearchEnabled = () => true; + + isBrowseEnabled = () => false; + + isLineageEnabled = () => false; + + getAutoCompleteFieldName = () => 'name'; + + getPathName = () => 'container'; + + getEntityName = () => 'Container'; + + getCollectionName = () => 'Containers'; + + renderProfile = (urn: string) => ( + + ); + + renderPreview = (_: PreviewType, data: Container) => { + return ( + + ); + }; + + renderSearch = (result: SearchResult) => { + const data = result.entity as Container; + return ( + + ); + }; + + displayName = (data: Container) => { + return data?.properties?.name || data?.urn; + }; + + getOverridePropertiesFromEntity = (data: Container) => { + return { + name: this.displayName(data), + entityCount: data.entities?.total, + }; + }; + + getGenericEntityProperties = (data: Container) => { + return getDataForEntityType({ + data, + entityType: this.type, + getOverrideProperties: this.getOverridePropertiesFromEntity, + }); + }; +} diff --git a/datahub-web-react/src/app/entity/container/preview/Preview.tsx b/datahub-web-react/src/app/entity/container/preview/Preview.tsx new file mode 100644 index 00000000000000..7a6fcb915d306d --- /dev/null +++ b/datahub-web-react/src/app/entity/container/preview/Preview.tsx @@ -0,0 +1,50 @@ +import React from 'react'; +import { Container, EntityType, Owner, SearchInsight, SubTypes } from '../../../../types.generated'; +import DefaultPreviewCard from '../../../preview/DefaultPreviewCard'; +import { useEntityRegistry } from '../../../useEntityRegistry'; +import { IconStyleType } from '../../Entity'; + +export const Preview = ({ + urn, + name, + platformName, + platformLogo, + description, + owners, + insights, + subTypes, + logoComponent, + container, + entityCount, +}: { + urn: string; + name: string; + platformName: string; + platformLogo?: string | null; + description?: string | null; + owners?: Array | null; + insights?: Array | null; + subTypes?: SubTypes | null; + logoComponent?: JSX.Element; + container?: Container | null; + entityCount?: number; +}): JSX.Element => { + const entityRegistry = useEntityRegistry(); + const typeName = (subTypes?.typeNames?.length && subTypes?.typeNames[0]) || 'Container'; + return ( + + ); +}; diff --git a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx index f4a027deae3702..9072bc786a12b0 100644 --- a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx +++ b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx @@ -147,6 +147,7 @@ export class DashboardEntity implements Entity { glossaryTerms={data?.glossaryTerms} logoUrl={data?.platform?.properties?.logoUrl} domain={data.domain} + container={data.container} /> ); }; @@ -166,6 +167,7 @@ export class DashboardEntity implements Entity { insights={result.insights} logoUrl={data?.platform?.properties?.logoUrl || ''} domain={data.domain} + container={data.container} /> ); }; diff --git a/datahub-web-react/src/app/entity/dashboard/preview/DashboardPreview.tsx b/datahub-web-react/src/app/entity/dashboard/preview/DashboardPreview.tsx index 130e19a95ac824..0da44b7f88ec60 100644 --- a/datahub-web-react/src/app/entity/dashboard/preview/DashboardPreview.tsx +++ b/datahub-web-react/src/app/entity/dashboard/preview/DashboardPreview.tsx @@ -2,6 +2,7 @@ import React from 'react'; import { AccessLevel, Domain, + Container, EntityType, GlobalTags, GlossaryTerms, @@ -22,6 +23,7 @@ export const DashboardPreview = ({ tags, glossaryTerms, domain, + container, insights, logoUrl, }: { @@ -34,6 +36,7 @@ export const DashboardPreview = ({ tags?: GlobalTags; glossaryTerms?: GlossaryTerms | null; domain?: Domain | null; + container?: Container | null; insights?: Array | null; logoUrl?: string | null; }): JSX.Element => { @@ -51,6 +54,7 @@ export const DashboardPreview = ({ qualifier={access} owners={owners} tags={tags} + container={container || undefined} glossaryTerms={glossaryTerms || undefined} domain={domain} insights={insights} diff --git a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx index a1915555f1a8c5..25455feca246ad 100644 --- a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx +++ b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx @@ -52,7 +52,7 @@ export class DatasetEntity implements Entity { } return ( - { origin={data.origin} subtype={data.subTypes?.typeNames?.[0]} description={data.editableProperties?.description || data.properties?.description} - platformName={data.platform.displayName || data.platform.name} + platformName={data.platform.properties?.displayName || data.platform.name} platformLogo={data.platform.properties?.logoUrl} owners={data.ownership?.owners} globalTags={data.globalTags} glossaryTerms={data.glossaryTerms} domain={data.domain} + container={data.container} /> ); }; @@ -210,13 +211,14 @@ export class DatasetEntity implements Entity { name={data.name} origin={data.origin} description={data.editableProperties?.description || data.properties?.description} - platformName={data.platform.name} + platformName={data.platform.properties?.displayName || data.platform.name} platformLogo={data.platform.properties?.logoUrl} owners={data.ownership?.owners} globalTags={data.globalTags} domain={data.domain} glossaryTerms={data.glossaryTerms} subtype={data.subTypes?.typeNames?.[0]} + container={data.container} snippet={ // Add match highlights only if all the matched fields are in the FIELDS_TO_HIGHLIGHT result.matchedFields.length > 0 && diff --git a/datahub-web-react/src/app/entity/dataset/preview/Preview.tsx b/datahub-web-react/src/app/entity/dataset/preview/Preview.tsx index cb920e2dae4a7b..c04fad1bd1dcc1 100644 --- a/datahub-web-react/src/app/entity/dataset/preview/Preview.tsx +++ b/datahub-web-react/src/app/entity/dataset/preview/Preview.tsx @@ -7,10 +7,12 @@ import { GlossaryTerms, SearchInsight, Domain, + Container, } from '../../../../types.generated'; import DefaultPreviewCard from '../../../preview/DefaultPreviewCard'; import { useEntityRegistry } from '../../../useEntityRegistry'; -import { capitalizeFirstLetter } from '../../../shared/textUtil'; +import { capitalizeFirstLetterOnly } from '../../../shared/textUtil'; +import { IconStyleType } from '../../Entity'; export const Preview = ({ urn, @@ -26,6 +28,7 @@ export const Preview = ({ insights, glossaryTerms, subtype, + container, }: { urn: string; name: string; @@ -40,21 +43,24 @@ export const Preview = ({ insights?: Array | null; glossaryTerms?: GlossaryTerms | null; subtype?: string | null; + container?: Container | null; }): JSX.Element => { const entityRegistry = useEntityRegistry(); - const capitalPlatformName = capitalizeFirstLetter(platformName); + const capitalPlatformName = capitalizeFirstLetterOnly(platformName); return ( { const { urn, entityType, entityData } = useEntityData(); const entityRegistry = useEntityRegistry(); const [copiedUrn, setCopiedUrn] = useState(false); - const platformName = capitalizeFirstLetter(entityData?.platform?.name); + const basePlatformName = entityData?.platform?.properties?.displayName || entityData?.platform?.name; + const platformName = capitalizeFirstLetterOnly(basePlatformName); const platformLogoUrl = entityData?.platform?.properties?.logoUrl; const entityLogoComponent = entityRegistry.getIcon(entityType, 12, IconStyleType.ACCENT); - const entityTypeCased = entityRegistry.getEntityName(entityType); + const entityTypeCased = + (entityData?.subTypes?.typeNames?.length && capitalizeFirstLetterOnly(entityData?.subTypes.typeNames[0])) || + entityRegistry.getEntityName(entityType); const entityPath = useEntityPath(entityType, urn); const externalUrl = entityData?.externalUrl || undefined; const hasExternalUrl = !!externalUrl; + const entityCount = entityData?.entityCount; + const typeIcon = entityRegistry.getIcon(entityType, 12, IconStyleType.ACCENT); + const container = entityData?.container; return ( @@ -85,13 +120,33 @@ export const EntityHeader = () => { {platformName} {(platformLogoUrl || platformName) && } + {typeIcon && {typeIcon}} {entityData?.entityTypeOverride || entityTypeCased} + {container && ( + + + + + {entityRegistry.getDisplayName(EntityType.Container, container)} + + + )} + {entityCount && entityCount > 0 ? ( + <> + + {entityCount.toLocaleString()} entities + + ) : null} {entityData?.name || ' '} - {hasExternalUrl && } + {hasExternalUrl && View in {platformName}}