From 4566607135cd8a45a2d250c0925929ad7164377b Mon Sep 17 00:00:00 2001 From: Chris Collins Date: Wed, 11 Dec 2024 20:12:24 -0500 Subject: [PATCH] feat(ui) Add full support for structured properties on assets (#12100) --- .../src/app/entity/EntityRegistry.tsx | 19 +- .../src/app/entity/chart/ChartEntity.tsx | 4 + .../app/entity/container/ContainerEntity.tsx | 4 + .../app/entity/dashboard/DashboardEntity.tsx | 4 + .../app/entity/dataFlow/DataFlowEntity.tsx | 4 + .../src/app/entity/dataJob/DataJobEntity.tsx | 4 + .../entity/dataProduct/DataProductEntity.tsx | 4 + .../src/app/entity/dataset/DatasetEntity.tsx | 7 +- .../components/StructuredPropValues.tsx | 69 ++++++ .../src/app/entity/domain/DomainEntity.tsx | 4 + .../glossaryNode/GlossaryNodeEntity.tsx | 4 + .../glossaryTerm/GlossaryTermEntity.tsx | 4 + .../app/entity/mlFeature/MLFeatureEntity.tsx | 4 + .../mlFeatureTable/MLFeatureTableEntity.tsx | 4 + .../src/app/entity/mlModel/MLModelEntity.tsx | 4 + .../mlModelGroup/MLModelGroupEntity.tsx | 4 + .../mlPrimaryKey/MLPrimaryKeyEntity.tsx | 4 + .../src/app/entity/shared/PreviewContext.tsx | 9 + .../profile/header/EntityHeader.tsx | 2 + .../header/StructuredPropertyBadge.tsx | 92 +++++++ .../shared/containers/profile/header/utils.ts | 29 +++ .../DataProduct/DataProductSection.tsx | 4 +- .../profile/sidebar/SidebarHeader.tsx | 5 +- .../SidebarStructuredPropsSection.tsx | 146 +++++++++++ .../tabs/Dataset/Schema/SchemaTable.tsx | 9 + .../Schema/components/PropertiesColumn.tsx | 4 +- .../SchemaFieldDrawer/FieldProperties.tsx | 67 +++-- .../SchemaFieldDrawer/SchemaFieldDrawer.tsx | 11 +- .../useGetSchemaColumnProperties.ts | 39 +++ .../Schema/useGetStructuredPropColumns.tsx | 22 ++ .../Schema/useGetTableColumnProperties.ts | 39 +++ .../components/CompactMarkdownViewer.tsx | 161 +++++++++++++ .../tabs/Properties/AddPropertyButton.tsx | 228 ++++++++++++++++++ .../tabs/Properties/Edit/EditColumn.tsx | 121 +++++++++- .../Edit/EditStructuredPropertyModal.tsx | 50 ++-- .../shared/tabs/Properties/PropertiesTab.tsx | 6 +- .../Properties/StructuredPropertyValue.tsx | 86 ++++--- .../shared/tabs/Properties/TabHeader.tsx | 10 +- .../entity/shared/tabs/Properties/types.ts | 1 + .../Properties/useStructuredProperties.tsx | 1 + .../src/app/entity/shared/types.ts | 1 + .../src/app/lineage/LineageEntityNode.tsx | 40 +++ datahub-web-react/src/app/lineage/types.ts | 4 + .../app/lineage/utils/constructFetchedNode.ts | 1 + .../src/app/lineage/utils/constructTree.ts | 4 + .../src/app/preview/DefaultPreviewCard.tsx | 4 + .../app/shared/sidebar/EmptySectionText.tsx | 20 ++ .../utils/test-utils/TestPageContainer.tsx | 2 + .../models/StructuredPropertyUtils.java | 3 +- 49 files changed, 1290 insertions(+), 82 deletions(-) create mode 100644 datahub-web-react/src/app/entity/dataset/profile/schema/components/StructuredPropValues.tsx create mode 100644 datahub-web-react/src/app/entity/shared/PreviewContext.tsx create mode 100644 datahub-web-react/src/app/entity/shared/containers/profile/header/StructuredPropertyBadge.tsx create mode 100644 datahub-web-react/src/app/entity/shared/containers/profile/header/utils.ts create mode 100644 datahub-web-react/src/app/entity/shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/useGetSchemaColumnProperties.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetStructuredPropColumns.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetTableColumnProperties.ts create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Documentation/components/CompactMarkdownViewer.tsx create mode 100644 datahub-web-react/src/app/entity/shared/tabs/Properties/AddPropertyButton.tsx create mode 100644 datahub-web-react/src/app/shared/sidebar/EmptySectionText.tsx diff --git a/datahub-web-react/src/app/entity/EntityRegistry.tsx b/datahub-web-react/src/app/entity/EntityRegistry.tsx index 0f65390f959df..827f0e6692442 100644 --- a/datahub-web-react/src/app/entity/EntityRegistry.tsx +++ b/datahub-web-react/src/app/entity/EntityRegistry.tsx @@ -7,6 +7,7 @@ import { Entity, EntityCapabilityType, IconStyleType, PreviewType } from './Enti import { GLOSSARY_ENTITY_TYPES } from './shared/constants'; import { EntitySidebarSection, GenericEntityProperties } from './shared/types'; import { dictToQueryStringParams, getFineGrainedLineageWithSiblings, urlEncodeUrn } from './shared/utils'; +import PreviewContext from './shared/PreviewContext'; function validatedGet(key: K, map: Map): V { if (map.has(key)) { @@ -142,13 +143,24 @@ export default class EntityRegistry { renderPreview(entityType: EntityType, type: PreviewType, data: T): JSX.Element { const entity = validatedGet(entityType, this.entityTypeToEntity); - return entity.renderPreview(type, data); + const genericEntityData = entity.getGenericEntityProperties(data); + return ( + + {entity.renderPreview(type, data)} + + ); } renderSearchResult(type: EntityType, searchResult: SearchResult): JSX.Element { const entity = validatedGet(type, this.entityTypeToEntity); + const genericEntityData = entity.getGenericEntityProperties(searchResult.entity); + return ( - {entity.renderSearch(searchResult)} + + + {entity.renderSearch(searchResult)} + + ); } @@ -205,6 +217,7 @@ export default class EntityRegistry { schemaMetadata: genericEntityProperties?.schemaMetadata, inputFields: genericEntityProperties?.inputFields, canEditLineage: genericEntityProperties?.privileges?.canEditLineage, + structuredProperties: genericEntityProperties?.structuredProperties, } as FetchedEntity) || undefined ); } @@ -239,7 +252,7 @@ export default class EntityRegistry { getCustomCardUrlPath(type: EntityType): string | undefined { const entity = validatedGet(type, this.entityTypeToEntity); - return entity.getCustomCardUrlPath?.(); + return entity.getCustomCardUrlPath?.() as string | undefined; } getGraphNameFromType(type: EntityType): string { diff --git a/datahub-web-react/src/app/entity/chart/ChartEntity.tsx b/datahub-web-react/src/app/entity/chart/ChartEntity.tsx index 8a62a9018661e..70fe8a5e7c7c2 100644 --- a/datahub-web-react/src/app/entity/chart/ChartEntity.tsx +++ b/datahub-web-react/src/app/entity/chart/ChartEntity.tsx @@ -29,6 +29,7 @@ import { MatchedFieldList } from '../../search/matches/MatchedFieldList'; import { matchedInputFieldRenderer } from '../../search/matches/matchedInputFieldRenderer'; import { IncidentTab } from '../shared/tabs/Incident/IncidentTab'; import { ChartQueryTab } from './ChartQueryTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub Chart entity. @@ -99,6 +100,9 @@ export class ChartEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderProfile = (urn: string) => ( diff --git a/datahub-web-react/src/app/entity/container/ContainerEntity.tsx b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx index 941e7fc3f552d..557f52146e77a 100644 --- a/datahub-web-react/src/app/entity/container/ContainerEntity.tsx +++ b/datahub-web-react/src/app/entity/container/ContainerEntity.tsx @@ -19,6 +19,7 @@ import { getDataProduct } from '../shared/utils'; import EmbeddedProfile from '../shared/embed/EmbeddedProfile'; import AccessManagement from '../shared/tabs/Dataset/AccessManagement/AccessManagement'; import { useAppConfig } from '../../useAppConfig'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub Container entity. @@ -133,6 +134,9 @@ export class ContainerEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, // TODO: Add back once entity-level recommendations are complete. // { // component: SidebarRecommendationsSection, diff --git a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx index 95d4431d59179..7d0275f60435a 100644 --- a/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx +++ b/datahub-web-react/src/app/entity/dashboard/DashboardEntity.tsx @@ -32,6 +32,7 @@ import { LOOKER_URN } from '../../ingest/source/builder/constants'; import { MatchedFieldList } from '../../search/matches/MatchedFieldList'; import { matchedInputFieldRenderer } from '../../search/matches/matchedInputFieldRenderer'; import { IncidentTab } from '../shared/tabs/Incident/IncidentTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub Dashboard entity. @@ -103,6 +104,9 @@ export class DashboardEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderProfile = (urn: string) => ( diff --git a/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx b/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx index dd4ae833e76f1..42555a0dd3f37 100644 --- a/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx +++ b/datahub-web-react/src/app/entity/dataFlow/DataFlowEntity.tsx @@ -19,6 +19,7 @@ import { capitalizeFirstLetterOnly } from '../../shared/textUtil'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { getDataProduct } from '../shared/utils'; import { IncidentTab } from '../shared/tabs/Incident/IncidentTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub DataFlow entity. @@ -123,6 +124,9 @@ export class DataFlowEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; getOverridePropertiesFromEntity = (dataFlow?: DataFlow | null): GenericEntityProperties => { diff --git a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx index 6bf9548226919..503acf7652dfa 100644 --- a/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx +++ b/datahub-web-react/src/app/entity/dataJob/DataJobEntity.tsx @@ -22,6 +22,7 @@ import { capitalizeFirstLetterOnly } from '../../shared/textUtil'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { getDataProduct } from '../shared/utils'; import { IncidentTab } from '../shared/tabs/Incident/IncidentTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; const getDataJobPlatformName = (data?: DataJob): string => { return ( @@ -143,6 +144,9 @@ export class DataJobEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; getOverridePropertiesFromEntity = (dataJob?: DataJob | null): GenericEntityProperties => { diff --git a/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx b/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx index 90c1127d9a5fc..b7912268eb2e3 100644 --- a/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx +++ b/datahub-web-react/src/app/entity/dataProduct/DataProductEntity.tsx @@ -17,6 +17,7 @@ import { DataProductEntitiesTab } from './DataProductEntitiesTab'; import { EntityActionItem } from '../shared/entity/EntityActions'; import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub Data Product entity. @@ -123,6 +124,9 @@ export class DataProductEntity implements Entity { updateOnly: true, }, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderPreview = (_: PreviewType, data: DataProduct) => { diff --git a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx index 07ab27a38f889..35ed3ffcc4c53 100644 --- a/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx +++ b/datahub-web-react/src/app/entity/dataset/DatasetEntity.tsx @@ -37,6 +37,7 @@ import { matchedFieldPathsRenderer } from '../../search/matches/matchedFieldPath import { getLastUpdatedMs } from './shared/utils'; import { IncidentTab } from '../shared/tabs/Incident/IncidentTab'; import { GovernanceTab } from '../shared/tabs/Dataset/Governance/GovernanceTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; const SUBTYPES = { VIEW: 'view', @@ -260,7 +261,11 @@ export class DatasetEntity implements Entity { }, { component: DataProductSection, - }, // TODO: Add back once entity-level recommendations are complete. + }, + { + component: SidebarStructuredPropsSection, + }, + // TODO: Add back once entity-level recommendations are complete. // { // component: SidebarRecommendationsSection, // }, diff --git a/datahub-web-react/src/app/entity/dataset/profile/schema/components/StructuredPropValues.tsx b/datahub-web-react/src/app/entity/dataset/profile/schema/components/StructuredPropValues.tsx new file mode 100644 index 0000000000000..4cba36b9375db --- /dev/null +++ b/datahub-web-react/src/app/entity/dataset/profile/schema/components/StructuredPropValues.tsx @@ -0,0 +1,69 @@ +import StructuredPropertyValue from '@src/app/entity/shared/tabs/Properties/StructuredPropertyValue'; +import { mapStructuredPropertyToPropertyRow } from '@src/app/entity/shared/tabs/Properties/useStructuredProperties'; +import { useEntityRegistry } from '@src/app/useEntityRegistry'; +import { SchemaFieldEntity, SearchResult, StdDataType } from '@src/types.generated'; +import { Tooltip } from 'antd'; +import React from 'react'; +import styled from 'styled-components'; + +const ValuesContainer = styled.span` + max-width: 120px; + display: flex; +`; + +const MoreIndicator = styled.span` + float: right; +`; + +interface Props { + schemaFieldEntity: SchemaFieldEntity; + propColumn: SearchResult | undefined; +} + +const StructuredPropValues = ({ schemaFieldEntity, propColumn }: Props) => { + const entityRegistry = useEntityRegistry(); + + const property = schemaFieldEntity.structuredProperties?.properties?.find( + (prop) => prop.structuredProperty.urn === propColumn?.entity.urn, + ); + const propRow = property ? mapStructuredPropertyToPropertyRow(property) : undefined; + const values = propRow?.values; + const isRichText = propRow?.dataType?.info.type === StdDataType.RichText; + + const hasMoreValues = values && values.length > 2; + const displayedValues = hasMoreValues ? values.slice(0, 1) : values; + const tooltipContent = values?.map((value) => { + const title = value.entity + ? entityRegistry.getDisplayName(value.entity.type, value.entity) + : value.value?.toString(); + return
{title}
; + }); + + return ( + <> + {values && ( + <> + {displayedValues?.map((val) => { + return ( + + + + ); + })} + {hasMoreValues && ( + + ... + + )} + + )} + + ); +}; + +export default StructuredPropValues; diff --git a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx index 81d245c230843..0f25f3ce565f0 100644 --- a/datahub-web-react/src/app/entity/domain/DomainEntity.tsx +++ b/datahub-web-react/src/app/entity/domain/DomainEntity.tsx @@ -15,6 +15,7 @@ import DataProductsTab from './DataProductsTab/DataProductsTab'; import { EntityProfileTab } from '../shared/constants'; import DomainIcon from '../../domain/DomainIcon'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub Domain entity. @@ -112,6 +113,9 @@ export class DomainEntity implements Entity { { component: SidebarOwnerSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderPreview = (_: PreviewType, data: Domain) => { diff --git a/datahub-web-react/src/app/entity/glossaryNode/GlossaryNodeEntity.tsx b/datahub-web-react/src/app/entity/glossaryNode/GlossaryNodeEntity.tsx index b214412dd29af..861e96811b340 100644 --- a/datahub-web-react/src/app/entity/glossaryNode/GlossaryNodeEntity.tsx +++ b/datahub-web-react/src/app/entity/glossaryNode/GlossaryNodeEntity.tsx @@ -12,6 +12,7 @@ import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab' import ChildrenTab from './ChildrenTab'; import { Preview } from './preview/Preview'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; class GlossaryNodeEntity implements Entity { type: EntityType = EntityType.GlossaryNode; @@ -100,6 +101,9 @@ class GlossaryNodeEntity implements Entity { { component: SidebarOwnerSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; displayName = (data: GlossaryNode) => { diff --git a/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx b/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx index 73c5a8e12122d..439cba2ea6923 100644 --- a/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx +++ b/datahub-web-react/src/app/entity/glossaryTerm/GlossaryTermEntity.tsx @@ -18,6 +18,7 @@ import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import { EntityActionItem } from '../shared/entity/EntityActions'; import { SidebarDomainSection } from '../shared/containers/profile/sidebar/Domain/SidebarDomainSection'; import { PageRoutes } from '../../../conf/Global'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub Dataset entity. @@ -129,6 +130,9 @@ export class GlossaryTermEntity implements Entity { hideOwnerType: true, }, }, + { + component: SidebarStructuredPropsSection, + }, ]; getOverridePropertiesFromEntity = (glossaryTerm?: GlossaryTerm | null): GenericEntityProperties => { diff --git a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx index eecffdb2f3843..51b66c8c2a41d 100644 --- a/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx +++ b/datahub-web-react/src/app/entity/mlFeature/MLFeatureEntity.tsx @@ -18,6 +18,7 @@ import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { getDataProduct } from '../shared/utils'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub MLFeature entity. @@ -122,6 +123,9 @@ export class MLFeatureEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderPreview = (_: PreviewType, data: MlFeature) => { diff --git a/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx b/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx index 8aa0c056b716f..56d4622311fb1 100644 --- a/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx +++ b/datahub-web-react/src/app/entity/mlFeatureTable/MLFeatureTableEntity.tsx @@ -19,6 +19,7 @@ import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import { capitalizeFirstLetterOnly } from '../../shared/textUtil'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { getDataProduct } from '../shared/utils'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub MLFeatureTable entity. @@ -90,6 +91,9 @@ export class MLFeatureTableEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderProfile = (urn: string) => ( diff --git a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx index 92f03aaef7a17..b77f6a19436a5 100644 --- a/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx +++ b/datahub-web-react/src/app/entity/mlModel/MLModelEntity.tsx @@ -18,6 +18,7 @@ import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab' import MlModelFeaturesTab from './profile/MlModelFeaturesTab'; import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub MlModel entity. @@ -91,6 +92,9 @@ export class MLModelEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderProfile = (urn: string) => ( diff --git a/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx b/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx index b5d32275f97bf..5c820007fd1e2 100644 --- a/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx +++ b/datahub-web-react/src/app/entity/mlModelGroup/MLModelGroupEntity.tsx @@ -16,6 +16,7 @@ import { DocumentationTab } from '../shared/tabs/Documentation/DocumentationTab' import { EntityMenuItems } from '../shared/EntityDropdown/EntityDropdown'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub MlModelGroup entity. @@ -87,6 +88,9 @@ export class MLModelGroupEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderProfile = (urn: string) => ( diff --git a/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx b/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx index 119a566b04f13..d72fabc17ecf6 100644 --- a/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx +++ b/datahub-web-react/src/app/entity/mlPrimaryKey/MLPrimaryKeyEntity.tsx @@ -17,6 +17,7 @@ import { LineageTab } from '../shared/tabs/Lineage/LineageTab'; import DataProductSection from '../shared/containers/profile/sidebar/DataProduct/DataProductSection'; import { getDataProduct } from '../shared/utils'; import { PropertiesTab } from '../shared/tabs/Properties/PropertiesTab'; +import SidebarStructuredPropsSection from '../shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; /** * Definition of the DataHub MLPrimaryKey entity. @@ -120,6 +121,9 @@ export class MLPrimaryKeyEntity implements Entity { { component: DataProductSection, }, + { + component: SidebarStructuredPropsSection, + }, ]; renderPreview = (_: PreviewType, data: MlPrimaryKey) => { diff --git a/datahub-web-react/src/app/entity/shared/PreviewContext.tsx b/datahub-web-react/src/app/entity/shared/PreviewContext.tsx new file mode 100644 index 0000000000000..889a6726f3c04 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/PreviewContext.tsx @@ -0,0 +1,9 @@ +import React from 'react'; +import { GenericEntityProperties } from './types'; + +const PreviewContext = React.createContext(null); +export default PreviewContext; + +export function usePreviewData() { + return React.useContext(PreviewContext); +} diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx index 12fa9131f33c7..9e8dc83c32302 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/header/EntityHeader.tsx @@ -18,6 +18,7 @@ import { useUserContext } from '../../../../../context/useUserContext'; import { useEntityRegistry } from '../../../../../useEntityRegistry'; import EntityHeaderLoadingSection from './EntityHeaderLoadingSection'; import { useIsEditableDatasetNameEnabled } from '../../../../../useAppConfig'; +import StructuredPropertyBadge from './StructuredPropertyBadge'; const TitleWrapper = styled.div` display: flex; @@ -132,6 +133,7 @@ export const EntityHeader = ({ headerDropdownItems, headerActionItems, isNameEdi baseUrl={entityRegistry.getEntityUrl(entityType, urn)} /> )} + { + const badgeStructuredProperty = structuredProperties?.properties?.find(filterForAssetBadge); + + const propRow = badgeStructuredProperty ? mapStructuredPropertyToPropertyRow(badgeStructuredProperty) : undefined; + + if (!badgeStructuredProperty) return null; + + const propertyValue = propRow?.values[0].value; + const relatedDescription = propRow?.structuredProperty.definition.allowedValues?.find( + (v) => getStructuredPropertyValue(v.value) === propertyValue, + )?.description; + + const BadgeTooltip = () => { + return ( + + + {getDisplayName(badgeStructuredProperty.structuredProperty)} + + + + Value + + {propRow?.values[0].value} + + {relatedDescription && ( + + + Description + + {relatedDescription} + + )} + + ); + }; + + return ( + } + color={colors.white} + overlayInnerStyle={{ width: 250, padding: 16 }} + > + + + + + ); +}; + +export default StructuredPropertyBadge; diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/header/utils.ts b/datahub-web-react/src/app/entity/shared/containers/profile/header/utils.ts new file mode 100644 index 0000000000000..6ab469725b51a --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/containers/profile/header/utils.ts @@ -0,0 +1,29 @@ +import EntityRegistry from '@src/app/entity/EntityRegistry'; +import { EntityType, StructuredPropertiesEntry } from '../../../../../../types.generated'; +import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil'; +import { GenericEntityProperties } from '../../../types'; + +export function getDisplayedEntityType( + entityData: GenericEntityProperties | null, + entityRegistry: EntityRegistry, + entityType: EntityType, +) { + return ( + entityData?.entityTypeOverride || + capitalizeFirstLetterOnly(entityData?.subTypes?.typeNames?.[0]) || + entityRegistry.getEntityName(entityType) || + '' + ); +} + +export function getEntityPlatforms(entityType: EntityType | null, entityData: GenericEntityProperties | null) { + const platform = entityType === EntityType.SchemaField ? entityData?.parent?.platform : entityData?.platform; + const platforms = + entityType === EntityType.SchemaField ? entityData?.parent?.siblingPlatforms : entityData?.siblingPlatforms; + + return { platform, platforms }; +} + +export function filterForAssetBadge(prop: StructuredPropertiesEntry) { + return prop.structuredProperty.settings?.showAsAssetBadge && !prop.structuredProperty.settings?.isHidden; +} diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/DataProduct/DataProductSection.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/DataProduct/DataProductSection.tsx index 1d489e88b5050..ec65b31968d83 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/DataProduct/DataProductSection.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/DataProduct/DataProductSection.tsx @@ -67,7 +67,7 @@ export default function DataProductSection({ readOnly }: Props) { }; return ( - <> +
{dataProduct && ( )} - +
); } diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarHeader.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarHeader.tsx index 0ee3fcb90e575..a5c7cce93a42a 100644 --- a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarHeader.tsx +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/SidebarHeader.tsx @@ -15,14 +15,15 @@ const HeaderContainer = styled.div` type Props = { title: string; + titleComponent?: React.ReactNode; actions?: React.ReactNode; children?: React.ReactNode; }; -export const SidebarHeader = ({ title, actions, children }: Props) => { +export const SidebarHeader = ({ title, titleComponent, actions, children }: Props) => { return ( - {title} + {titleComponent || {title}} {actions &&
{actions}
} {children}
diff --git a/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection.tsx b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection.tsx new file mode 100644 index 0000000000000..ea257ca2ade31 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection.tsx @@ -0,0 +1,146 @@ +import { useUserContext } from '@src/app/context/useUserContext'; +import { useEntityData } from '@src/app/entity/shared/EntityContext'; +import { StyledList } from '@src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties'; +import { EditColumn } from '@src/app/entity/shared/tabs/Properties/Edit/EditColumn'; +import StructuredPropertyValue from '@src/app/entity/shared/tabs/Properties/StructuredPropertyValue'; +import { PropertyRow } from '@src/app/entity/shared/tabs/Properties/types'; +import EmptySectionText from '@src/app/shared/sidebar/EmptySectionText'; +import { + getDisplayName, + getEntityTypesPropertyFilter, + getNotHiddenPropertyFilter, + getPropertyRowFromSearchResult, +} from '@src/app/govern/structuredProperties/utils'; +import { useEntityRegistry } from '@src/app/useEntityRegistry'; +import { useGetSearchResultsForMultipleQuery } from '@src/graphql/search.generated'; +import { EntityType, SchemaField, SearchResult, StdDataType, StructuredPropertyEntity } from '@src/types.generated'; +import { + SHOW_IN_ASSET_SUMMARY_PROPERTY_FILTER_NAME, + SHOW_IN_COLUMNS_TABLE_PROPERTY_FILTER_NAME, +} from '@src/app/search/utils/constants'; +import { + SectionHeader, + StyledDivider, +} from '@src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/components'; +import { useGetEntityWithSchema } from '@src/app/entity/shared/tabs/Dataset/Schema/useGetEntitySchema'; +import React from 'react'; +import { SidebarHeader } from '../SidebarHeader'; + +interface Props { + properties?: { + schemaField?: SchemaField; + schemaColumnProperties?: SearchResult[]; + }; +} + +const SidebarStructuredPropsSection = ({ properties }: Props) => { + const schemaField = properties?.schemaField; + const schemaColumnProperties = properties?.schemaColumnProperties; + const { entityData, entityType } = useEntityData(); + const me = useUserContext(); + const entityRegistry = useEntityRegistry(); + const { refetch: refetchSchema } = useGetEntityWithSchema(true); + + const currentProperties = schemaField + ? schemaField?.schemaFieldEntity?.structuredProperties + : entityData?.structuredProperties; + + const inputs = { + types: [EntityType.StructuredProperty], + query: '', + start: 0, + count: 50, + searchFlags: { skipCache: true }, + orFilters: [ + { + and: [ + getEntityTypesPropertyFilter(entityRegistry, !!schemaField, entityType), + getNotHiddenPropertyFilter(), + { + field: schemaField + ? SHOW_IN_COLUMNS_TABLE_PROPERTY_FILTER_NAME + : SHOW_IN_ASSET_SUMMARY_PROPERTY_FILTER_NAME, + values: ['true'], + }, + ], + }, + ], + }; + + // Execute search + const { data } = useGetSearchResultsForMultipleQuery({ + variables: { + input: inputs, + }, + skip: !!schemaColumnProperties, + fetchPolicy: 'cache-first', + }); + + const entityTypeProperties = schemaColumnProperties || data?.searchAcrossEntities?.searchResults; + + const canEditProperties = me.platformPrivileges?.manageStructuredProperties; + + return ( + <> + {entityTypeProperties?.map((property) => { + const propertyRow: PropertyRow | undefined = getPropertyRowFromSearchResult( + property, + currentProperties, + ); + const isRichText = propertyRow?.dataType?.info.type === StdDataType.RichText; + const values = propertyRow?.values; + const hasMultipleValues = values && values.length > 1; + const propertyName = getDisplayName(property.entity as StructuredPropertyEntity); + + return ( + <> +
+ {propertyName}} + actions={ + canEditProperties && ( + <> + v.value) || []} + isAddMode={!values} + associatedUrn={schemaField?.schemaFieldEntity?.urn} + refetch={schemaField ? refetchSchema : undefined} + /> + + ) + } + /> + + {values ? ( + <> + {hasMultipleValues ? ( + + {values.map((value) => ( +
  • + +
  • + ))} +
    + ) : ( + <> + {values?.map((value) => ( + + ))} + + )} + + ) : ( + + )} +
    + {schemaField && } + + ); + })} + + ); +}; + +export default SidebarStructuredPropsSection; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx index 0bfd5255f3065..83ebb8f6b7828 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/SchemaTable.tsx @@ -27,6 +27,8 @@ import PropertiesColumn from './components/PropertiesColumn'; import SchemaFieldDrawer from './components/SchemaFieldDrawer/SchemaFieldDrawer'; import useBusinessAttributeRenderer from './utils/useBusinessAttributeRenderer'; import { useBusinessAttributesFlag } from '../../../../../useAppConfig'; +import { useGetTableColumnProperties } from './useGetTableColumnProperties'; +import { useGetStructuredPropColumns } from './useGetStructuredPropColumns'; const TableContainer = styled.div` overflow: inherit; @@ -126,6 +128,9 @@ export default function SchemaTable({ const schemaTitleRenderer = useSchemaTitleRenderer(schemaMetadata, setSelectedFkFieldPath, filterText); const schemaBlameRenderer = useSchemaBlameRenderer(schemaFieldBlameList); + const tableColumnStructuredProps = useGetTableColumnProperties(); + const structuredPropColumns = useGetStructuredPropColumns(tableColumnStructuredProps); + const fieldColumn = { width: '22%', title: 'Field', @@ -221,6 +226,10 @@ export default function SchemaTable({ allColumns = [...allColumns, propertiesColumn]; } + if (structuredPropColumns) { + allColumns = [...allColumns, ...structuredPropColumns]; + } + if (hasUsageStats) { allColumns = [...allColumns, usageColumn]; } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx index b74de3e94e554..74d14cb0db753 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/PropertiesColumn.tsx @@ -17,7 +17,9 @@ interface Props { export default function PropertiesColumn({ field }: Props) { const { schemaFieldEntity } = field; - const numProperties = schemaFieldEntity?.structuredProperties?.properties?.length; + const numProperties = schemaFieldEntity?.structuredProperties?.properties?.filter( + (prop) => !prop.structuredProperty.settings?.isHidden, + )?.length; if (!schemaFieldEntity || !numProperties) return null; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx index 9a0da20f22dfd..d6f2a83748251 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/FieldProperties.tsx @@ -1,47 +1,76 @@ +import { useEntityData } from '@src/app/entity/shared/EntityContext'; import React from 'react'; import styled from 'styled-components'; -import { SchemaField, StdDataType } from '../../../../../../../../types.generated'; -import { SectionHeader, StyledDivider } from './components'; -import { mapStructuredPropertyValues } from '../../../../Properties/useStructuredProperties'; -import StructuredPropertyValue from '../../../../Properties/StructuredPropertyValue'; +import { SchemaField, SearchResult, StdDataType } from '../../../../../../../../types.generated'; +import AddPropertyButton from '../../../../Properties/AddPropertyButton'; import { EditColumn } from '../../../../Properties/Edit/EditColumn'; +import StructuredPropertyValue from '../../../../Properties/StructuredPropertyValue'; +import { mapStructuredPropertyValues } from '../../../../Properties/useStructuredProperties'; import { useGetEntityWithSchema } from '../../useGetEntitySchema'; +import { StyledDivider } from './components'; -const PropertyTitle = styled.div` +export const PropertyTitle = styled.div` font-size: 14px; font-weight: 700; margin-bottom: 4px; `; -const PropertyWrapper = styled.div` +export const PropertyWrapper = styled.div` margin-bottom: 12px; display: flex; justify-content: space-between; `; -const PropertiesWrapper = styled.div` +export const PropertiesWrapper = styled.div` padding-left: 16px; `; -const StyledList = styled.ul` +export const StyledList = styled.ul` padding-left: 24px; `; +export const Header = styled.div` + font-size: 16px; + font-weight: 600; + margin-bottom: 16px; + display: flex; + justify-content: space-between; + align-items: center; +`; + interface Props { expandedField: SchemaField; + schemaColumnProperties?: SearchResult[]; } -export default function FieldProperties({ expandedField }: Props) { +export default function FieldProperties({ expandedField, schemaColumnProperties }: Props) { const { schemaFieldEntity } = expandedField; const { refetch } = useGetEntityWithSchema(true); + const { entityData } = useEntityData(); const properties = - schemaFieldEntity?.structuredProperties?.properties?.filter((prop) => prop.structuredProperty.exists) || []; + schemaFieldEntity?.structuredProperties?.properties?.filter( + (prop) => + prop.structuredProperty.exists && + !prop.structuredProperty.settings?.isHidden && + !schemaColumnProperties?.find((p) => p.entity.urn === prop.structuredProperty.urn), + ) || []; + + const canEditProperties = + entityData?.parent?.privileges?.canEditProperties || entityData?.privileges?.canEditProperties; - if (!schemaFieldEntity || !properties.length) return null; + if (!schemaFieldEntity) return null; return ( <> - Properties +
    + Properties + +
    {properties.map((structuredProp) => { const isRichText = @@ -71,12 +100,14 @@ export default function FieldProperties({ expandedField }: Props) { )} - v.value) || []} - refetch={refetch} - /> + {canEditProperties && ( + v.value) || []} + refetch={refetch} + /> + )} ); })} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx index 0d9f7a98f207c..df0cd8b2dd762 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/SchemaFieldDrawer.tsx @@ -1,6 +1,7 @@ import { Drawer } from 'antd'; import React, { useMemo } from 'react'; import styled from 'styled-components'; +import SidebarStructuredPropsSection from '@src/app/entity/shared/containers/profile/sidebar/StructuredProperties/SidebarStructuredPropsSection'; import DrawerHeader from './DrawerHeader'; import FieldHeader from './FieldHeader'; import FieldDescription from './FieldDescription'; @@ -11,6 +12,7 @@ import FieldTags from './FieldTags'; import FieldTerms from './FieldTerms'; import FieldProperties from './FieldProperties'; import FieldAttribute from './FieldAttribute'; +import useGetSchemaColumnProperties from './useGetSchemaColumnProperties'; const StyledDrawer = styled(Drawer)` position: absolute; @@ -50,6 +52,7 @@ export default function SchemaFieldDrawer({ const editableFieldInfo = editableSchemaMetadata?.editableSchemaFieldInfo.find((candidateEditableFieldInfo) => pathMatchesNewPath(candidateEditableFieldInfo.fieldPath, expandedField?.fieldPath), ); + const schemaColumnProperties = useGetSchemaColumnProperties(); return ( - + + diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/useGetSchemaColumnProperties.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/useGetSchemaColumnProperties.ts new file mode 100644 index 0000000000000..ed5af588fa036 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/components/SchemaFieldDrawer/useGetSchemaColumnProperties.ts @@ -0,0 +1,39 @@ +import { getEntityTypesPropertyFilter, getNotHiddenPropertyFilter } from '@src/app/govern/structuredProperties/utils'; +import { SHOW_IN_COLUMNS_TABLE_PROPERTY_FILTER_NAME } from '@src/app/search/utils/constants'; +import { useEntityRegistry } from '@src/app/useEntityRegistry'; +import { useGetSearchResultsForMultipleQuery } from '@src/graphql/search.generated'; +import { EntityType, SearchResult } from '@src/types.generated'; + +export default function useGetSchemaColumnProperties() { + const entityRegistry = useEntityRegistry(); + + const inputs = { + types: [EntityType.StructuredProperty], + query: '', + start: 0, + count: 50, + searchFlags: { skipCache: true }, + orFilters: [ + { + and: [ + getEntityTypesPropertyFilter(entityRegistry, true, EntityType.SchemaField), + getNotHiddenPropertyFilter(), + { + field: SHOW_IN_COLUMNS_TABLE_PROPERTY_FILTER_NAME, + values: ['true'], + }, + ], + }, + ], + }; + + // Execute search + const { data } = useGetSearchResultsForMultipleQuery({ + variables: { + input: inputs, + }, + fetchPolicy: 'cache-first', + }); + + return data?.searchAcrossEntities?.searchResults || ([] as SearchResult[]); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetStructuredPropColumns.tsx b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetStructuredPropColumns.tsx new file mode 100644 index 0000000000000..eed3fd510724b --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetStructuredPropColumns.tsx @@ -0,0 +1,22 @@ +import StructuredPropValues from '@src/app/entity/dataset/profile/schema/components/StructuredPropValues'; +import { getDisplayName } from '@src/app/govern/structuredProperties/utils'; +import { SearchResult, StructuredPropertyEntity } from '@src/types.generated'; +import React, { useMemo } from 'react'; + +export const useGetStructuredPropColumns = (properties: SearchResult[] | undefined) => { + const columns = useMemo(() => { + return properties?.map((prop) => { + const name = getDisplayName(prop.entity as StructuredPropertyEntity); + return { + width: 120, + title: name, + dataIndex: 'schemaFieldEntity', + key: prop.entity.urn, + render: (record) => , + ellipsis: true, + }; + }); + }, [properties]); + + return columns; +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetTableColumnProperties.ts b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetTableColumnProperties.ts new file mode 100644 index 0000000000000..96ff90921b937 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Dataset/Schema/useGetTableColumnProperties.ts @@ -0,0 +1,39 @@ +import { + getEntityTypesPropertyFilter, + getNotHiddenPropertyFilter, + getShowInColumnsTablePropertyFilter, +} from '@src/app/govern/structuredProperties/utils'; +import { useEntityRegistry } from '@src/app/useEntityRegistry'; +import { useGetSearchResultsForMultipleQuery } from '@src/graphql/search.generated'; +import { EntityType } from '@src/types.generated'; + +export const useGetTableColumnProperties = () => { + const entityRegistry = useEntityRegistry(); + + const inputs = { + types: [EntityType.StructuredProperty], + query: '', + start: 0, + count: 50, + searchFlags: { skipCache: true }, + orFilters: [ + { + and: [ + getEntityTypesPropertyFilter(entityRegistry, true), + getNotHiddenPropertyFilter(), + getShowInColumnsTablePropertyFilter(), + ], + }, + ], + }; + + // Execute search + const { data } = useGetSearchResultsForMultipleQuery({ + variables: { + input: inputs, + }, + fetchPolicy: 'cache-first', + }); + + return data?.searchAcrossEntities?.searchResults; +}; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/CompactMarkdownViewer.tsx b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/CompactMarkdownViewer.tsx new file mode 100644 index 0000000000000..df93d51e721bb --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Documentation/components/CompactMarkdownViewer.tsx @@ -0,0 +1,161 @@ +import { Button } from 'antd'; +import React, { useCallback, useEffect, useState } from 'react'; +import styled from 'styled-components'; +import { Editor } from './editor/Editor'; + +const LINE_HEIGHT = 1.5; + +const ShowMoreWrapper = styled.div` + align-items: start; + display: flex; + flex-direction: column; +`; + +const MarkdownContainer = styled.div<{ lineLimit?: number | null }>` + max-width: 100%; + position: relative; + ${(props) => + props.lineLimit && + props.lineLimit <= 1 && + ` + display: flex; + align-items: center; + gap: 4px; + ${ShowMoreWrapper}{ + flex-direction: row; + align-items: center; + gap: 4px; + } + `} +`; + +const CustomButton = styled(Button)` + padding: 0; + color: #676b75; +`; + +const MarkdownViewContainer = styled.div<{ scrollableY: boolean }>` + display: block; + overflow-wrap: break-word; + word-wrap: break-word; + overflow-x: hidden; + overflow-y: ${(props) => (props.scrollableY ? 'auto' : 'hidden')}; +`; + +const CompactEditor = styled(Editor)<{ limit: number | null; customStyle?: React.CSSProperties }>` + .remirror-editor.ProseMirror { + ${({ limit }) => limit && `max-height: ${limit * LINE_HEIGHT}em;`} + h1 { + font-size: 1.4em; + } + + h2 { + font-size: 1.3em; + } + + h3 { + font-size: 1.2em; + } + + h4 { + font-size: 1.1em; + } + + h5, + h6 { + font-size: 1em; + } + + p { + ${(props) => props?.customStyle?.fontSize && `font-size: ${props?.customStyle?.fontSize}`}; + margin-bottom: 0; + } + + padding: 0; + } +`; + +const FixedLineHeightEditor = styled(CompactEditor)<{ customStyle?: React.CSSProperties }>` + .remirror-editor.ProseMirror { + * { + line-height: ${LINE_HEIGHT}; + font-size: 1em !important; + margin-top: 0; + margin-bottom: 0; + } + p { + font-size: ${(props) => (props?.customStyle?.fontSize ? props?.customStyle?.fontSize : '1em')} !important; + } + } +`; + +export type Props = { + content: string; + lineLimit?: number | null; + fixedLineHeight?: boolean; + isShowMoreEnabled?: boolean; + customStyle?: React.CSSProperties; + scrollableY?: boolean; // Whether the viewer is vertically scrollable. + handleShowMore?: () => void; + hideShowMore?: boolean; +}; + +export default function CompactMarkdownViewer({ + content, + lineLimit = 4, + fixedLineHeight = false, + isShowMoreEnabled = false, + customStyle = {}, + scrollableY = true, + handleShowMore, + hideShowMore, +}: Props) { + const [isShowingMore, setIsShowingMore] = useState(false); + const [isTruncated, setIsTruncated] = useState(false); + + useEffect(() => { + if (isShowMoreEnabled) { + setIsShowingMore(isShowMoreEnabled); + } + return () => { + setIsShowingMore(false); + }; + }, [isShowMoreEnabled]); + + const measuredRef = useCallback((node: HTMLDivElement | null) => { + if (node !== null) { + const resizeObserver = new ResizeObserver(() => { + setIsTruncated(node.scrollHeight > node.clientHeight + 1); + }); + resizeObserver.observe(node); + } + }, []); + + const StyledEditor = fixedLineHeight ? FixedLineHeightEditor : CompactEditor; + + return ( + + + + + {hideShowMore && <>...} + + {!hideShowMore && + (isShowingMore || isTruncated) && ( // "show more" when isTruncated, "show less" when isShowingMore + + (handleShowMore ? handleShowMore() : setIsShowingMore(!isShowingMore))} + > + {isShowingMore ? 'show less' : 'show more'} + + + )} + + ); +} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/AddPropertyButton.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/AddPropertyButton.tsx new file mode 100644 index 0000000000000..cac3e268c1df5 --- /dev/null +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/AddPropertyButton.tsx @@ -0,0 +1,228 @@ +import { LoadingOutlined } from '@ant-design/icons'; +import { colors, Icon, Input as InputComponent, Text } from '@src/alchemy-components'; +import { useUserContext } from '@src/app/context/useUserContext'; +import { getEntityTypesPropertyFilter, getNotHiddenPropertyFilter } from '@src/app/govern/structuredProperties/utils'; +import { useEntityRegistry } from '@src/app/useEntityRegistry'; +import { PageRoutes } from '@src/conf/Global'; +import { useGetSearchResultsForMultipleQuery } from '@src/graphql/search.generated'; +import { Dropdown } from 'antd'; +import { Tooltip } from '@components'; +import { EntityType, Maybe, StructuredProperties, StructuredPropertyEntity } from '@src/types.generated'; +import React, { useMemo, useState } from 'react'; +import { Link } from 'react-router-dom'; +import styled from 'styled-components'; +import { useEntityData } from '../../EntityContext'; +import EditStructuredPropertyModal from './Edit/EditStructuredPropertyModal'; + +const AddButton = styled.div<{ isV1Drawer?: boolean }>` + border-radius: 200px; + background-color: #5280e2; + width: ${(props) => (props.isV1Drawer ? '24px' : '32px')}; + height: ${(props) => (props.isV1Drawer ? '24px' : '32px')}; + display: flex; + align-items: center; + justify-content: center; + + :hover { + cursor: pointer; + } +`; + +const DropdownContainer = styled.div` + border-radius: 12px; + box-shadow: 0px 0px 14px 0px rgba(0, 0, 0, 0.15); + background-color: ${colors.white}; + padding-bottom: 8px; + width: 300px; +`; + +const SearchContainer = styled.div` + padding: 8px; +`; + +const OptionsContainer = styled.div` + max-height: 200px; + overflow-y: auto; + font-size: 14px; +`; + +const Option = styled.div``; + +const LoadingContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; + justify-content: center; + height: 100px; +`; + +const EmptyContainer = styled.div` + display: flex; + flex-direction: column; + gap: 8px; + align-items: center; + justify-content: center; + min-height: 50px; + padding: 16px; + text-align: center; +`; + +interface Props { + fieldUrn?: string; + refetch?: () => void; + fieldProperties?: Maybe; + isV1Drawer?: boolean; +} + +const AddPropertyButton = ({ fieldUrn, refetch, fieldProperties, isV1Drawer }: Props) => { + const [searchQuery, setSearchQuery] = useState(''); + const { entityData, entityType } = useEntityData(); + const me = useUserContext(); + const entityRegistry = useEntityRegistry(); + const [isEditModalVisible, setIsEditModalVisible] = useState(false); + + const inputs = { + types: [EntityType.StructuredProperty], + query: '', + start: 0, + count: 100, + searchFlags: { skipCache: true }, + orFilters: [ + { + and: [ + getEntityTypesPropertyFilter(entityRegistry, !!fieldUrn, entityType), + getNotHiddenPropertyFilter(), + ], + }, + ], + }; + + // Execute search + const { data, loading } = useGetSearchResultsForMultipleQuery({ + variables: { + input: inputs, + }, + fetchPolicy: 'cache-first', + }); + + const [selectedProperty, setSelectedProperty] = useState( + data?.searchAcrossEntities?.searchResults?.[0]?.entity as StructuredPropertyEntity | undefined, + ); + + const handleOptionClick = (property: StructuredPropertyEntity) => { + setSelectedProperty(property); + setIsEditModalVisible(true); + }; + + const entityPropertiesUrns = entityData?.structuredProperties?.properties?.map( + (prop) => prop.structuredProperty.urn, + ); + const fieldPropertiesUrns = fieldProperties?.properties?.map((prop) => prop.structuredProperty.urn); + + // filter out the existing properties when displaying in the list of add button + const properties = useMemo( + () => + data?.searchAcrossEntities?.searchResults + .filter((result) => + fieldUrn + ? !fieldPropertiesUrns?.includes(result.entity.urn) + : !entityPropertiesUrns?.includes(result.entity.urn), + ) + .map((prop) => { + const entity = prop.entity as StructuredPropertyEntity; + return { + label: ( + + ), + key: entity.urn, + name: entity.definition?.displayName || entity.urn, + }; + }), + [data, fieldUrn, fieldPropertiesUrns, entityPropertiesUrns], + ); + + const canEditProperties = + entityData?.parent?.privileges?.canEditProperties || entityData?.privileges?.canEditProperties; + + if (!canEditProperties) return null; + + // Filter items based on search query + const filteredItems = properties?.filter((prop) => prop.name?.toLowerCase().includes(searchQuery.toLowerCase())); + + const noDataText = + properties?.length === 0 ? ( + <> + It looks like there are no structured properties for this asset type. + {me.platformPrivileges?.manageStructuredProperties && ( + + {' '} + Manage custom properties + + )} + + ) : null; + + return ( + <> + ( + + + setSearchQuery(e.target.value)} + /> + + {loading ? ( + + + Loading... + + ) : ( + <> + {filteredItems?.length === 0 && ( + + + No results found + + + {noDataText} + + + )} + {menuNode} + + )} + + )} + > + + + + + + + {selectedProperty && ( + setIsEditModalVisible(false)} + structuredProperty={selectedProperty} + associatedUrn={fieldUrn} // pass in fieldUrn to use otherwise we will use mutation urn for siblings + refetch={refetch} + isAddMode + /> + )} + + ); +}; + +export default AddPropertyButton; diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditColumn.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditColumn.tsx index 6a0599c0cdb33..a2d5c44b391e3 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditColumn.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditColumn.tsx @@ -1,27 +1,128 @@ -import { Button } from 'antd'; +import { colors, Icon, Text } from '@src/alchemy-components'; +import analytics, { EventType } from '@src/app/analytics'; +import { MenuItem } from '@src/app/govern/structuredProperties/styledComponents'; +import { ConfirmationModal } from '@src/app/sharedV2/modals/ConfirmationModal'; +import { showToastMessage, ToastType } from '@src/app/sharedV2/toastMessageUtils'; +import { useRemoveStructuredPropertiesMutation } from '@src/graphql/structuredProperties.generated'; +import { EntityType, StructuredPropertyEntity } from '@src/types.generated'; +import { Dropdown } from 'antd'; import React, { useState } from 'react'; +import styled from 'styled-components'; +import { useEntityContext, useEntityData, useMutationUrn } from '../../../EntityContext'; import EditStructuredPropertyModal from './EditStructuredPropertyModal'; -import { StructuredPropertyEntity } from '../../../../../../types.generated'; + +export const MoreOptionsContainer = styled.div` + display: flex; + gap: 12px; + justify-content: end; + + div { + background-color: ${colors.gray[1500]}; + border-radius: 20px; + width: 24px; + height: 24px; + padding: 3px; + color: ${colors.gray[1800]}; + :hover { + cursor: pointer; + } + } +`; interface Props { structuredProperty?: StructuredPropertyEntity; associatedUrn?: string; values?: (string | number | null)[]; refetch?: () => void; + isAddMode?: boolean; } -export function EditColumn({ structuredProperty, associatedUrn, values, refetch }: Props) { +export function EditColumn({ structuredProperty, associatedUrn, values, refetch, isAddMode }: Props) { const [isEditModalVisible, setIsEditModalVisible] = useState(false); + const { refetch: entityRefetch } = useEntityContext(); + const { entityType } = useEntityData(); + + const [removeStructuredProperty] = useRemoveStructuredPropertiesMutation(); + + const [showConfirmRemove, setShowConfirmRemove] = useState(false); + const mutationUrn = useMutationUrn(); if (!structuredProperty || structuredProperty?.definition.immutable) { return null; } + const handleRemoveProperty = () => { + showToastMessage(ToastType.LOADING, 'Removing structured property', 1); + removeStructuredProperty({ + variables: { + input: { + assetUrn: associatedUrn || mutationUrn, + structuredPropertyUrns: [structuredProperty.urn], + }, + }, + }) + .then(() => { + analytics.event({ + type: EventType.RemoveStructuredPropertyEvent, + propertyUrn: structuredProperty.urn, + propertyType: structuredProperty.definition.valueType.urn, + assetUrn: associatedUrn || mutationUrn, + assetType: associatedUrn?.includes('urn:li:schemaField') ? EntityType.SchemaField : entityType, + }); + showToastMessage(ToastType.SUCCESS, 'Structured property removed successfully!', 3); + if (refetch) { + refetch(); + } else { + entityRefetch(); + } + }) + .catch(() => { + showToastMessage(ToastType.ERROR, 'Failed to remove structured property', 3); + }); + + setShowConfirmRemove(false); + }; + + const handleRemoveClose = () => { + setShowConfirmRemove(false); + }; + + const items = [ + { + key: '0', + label: ( + { + setIsEditModalVisible(true); + }} + > + {isAddMode ? 'Add' : 'Edit'} + + ), + }, + ]; + if (values && values?.length > 0) { + items.push({ + key: '1', + label: ( + { + setShowConfirmRemove(true); + }} + > + Remove + + ), + }); + } + return ( <> - + + + + + setIsEditModalVisible(false)} refetch={refetch} + isAddMode={isAddMode} + /> + handleRemoveProperty()} + modalTitle="Confirm Remove Structured Property" + modalText={`Are you sure you want to remove ${structuredProperty.definition.displayName} from this asset?`} /> ); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx index c8def8bef5e19..13aa0dfd42d1e 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/Edit/EditStructuredPropertyModal.tsx @@ -1,12 +1,13 @@ +import analytics, { EventType } from '@src/app/analytics'; import { Button, Modal, message } from 'antd'; import React, { useEffect, useMemo } from 'react'; import styled from 'styled-components'; -import StructuredPropertyInput from '../../../components/styled/StructuredProperty/StructuredPropertyInput'; -import { PropertyValueInput, StructuredPropertyEntity } from '../../../../../../types.generated'; import { useUpsertStructuredPropertiesMutation } from '../../../../../../graphql/structuredProperties.generated'; -import { useEditStructuredProperty } from '../../../components/styled/StructuredProperty/useEditStructuredProperty'; -import { useEntityContext, useMutationUrn } from '../../../EntityContext'; +import { EntityType, PropertyValueInput, StructuredPropertyEntity } from '../../../../../../types.generated'; import handleGraphQLError from '../../../../../shared/handleGraphQLError'; +import StructuredPropertyInput from '../../../components/styled/StructuredProperty/StructuredPropertyInput'; +import { useEditStructuredProperty } from '../../../components/styled/StructuredProperty/useEditStructuredProperty'; +import { useEntityContext, useEntityData, useMutationUrn } from '../../../EntityContext'; const Description = styled.div` font-size: 14px; @@ -21,6 +22,7 @@ interface Props { values?: (string | number | null)[]; closeModal: () => void; refetch?: () => void; + isAddMode?: boolean; } export default function EditStructuredPropertyModal({ @@ -30,9 +32,11 @@ export default function EditStructuredPropertyModal({ values, closeModal, refetch, + isAddMode, }: Props) { const { refetch: entityRefetch } = useEntityContext(); const mutationUrn = useMutationUrn(); + const { entityType } = useEntityData(); const urn = associatedUrn || mutationUrn; const initialValues = useMemo(() => values || [], [values]); const { selectedValues, selectSingleValue, toggleSelectedValue, updateSelectedValues, setSelectedValues } = @@ -44,7 +48,13 @@ export default function EditStructuredPropertyModal({ }, [isOpen, initialValues, setSelectedValues]); function upsertProperties() { - message.loading('Updating...'); + message.loading(isAddMode ? 'Adding...' : 'Updating...'); + const propValues = selectedValues.map((value) => { + if (typeof value === 'string') { + return { stringValue: value as string }; + } + return { numberValue: value as number }; + }) as PropertyValueInput[]; upsertStructuredProperties({ variables: { input: { @@ -52,25 +62,30 @@ export default function EditStructuredPropertyModal({ structuredPropertyInputParams: [ { structuredPropertyUrn: structuredProperty.urn, - values: selectedValues.map((value) => { - if (typeof value === 'string') { - return { stringValue: value as string }; - } - return { numberValue: value as number }; - }) as PropertyValueInput[], + values: propValues, }, ], }, }, }) .then(() => { + analytics.event({ + type: isAddMode + ? EventType.ApplyStructuredPropertyEvent + : EventType.UpdateStructuredPropertyOnAssetEvent, + propertyUrn: structuredProperty.urn, + propertyType: structuredProperty.definition.valueType.urn, + assetUrn: urn, + assetType: associatedUrn?.includes('urn:li:schemaField') ? EntityType.SchemaField : entityType, + values: propValues, + }); if (refetch) { refetch(); } else { entityRefetch(); } message.destroy(); - message.success('Successfully updated structured property!'); + message.success(`Successfully ${isAddMode ? 'added' : 'updated'} structured property!`); closeModal(); }) .catch((error) => { @@ -84,7 +99,7 @@ export default function EditStructuredPropertyModal({ return ( Cancel - } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx index eeff8fc2e2795..5fc209688c957 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/PropertiesTab.tsx @@ -44,6 +44,7 @@ export const PropertiesTab = () => { render: (propertyRow: PropertyRow) => ( v.value) || []} /> ), @@ -51,9 +52,12 @@ export const PropertiesTab = () => { } const { structuredPropertyRows, expandedRowsFromFilter } = useStructuredProperties(entityRegistry, filterText); + const filteredStructuredPropertyRows = structuredPropertyRows.filter( + (row) => !row.structuredProperty?.settings?.isHidden, + ); const customProperties = getFilteredCustomProperties(filterText, entityData) || []; const customPropertyRows = mapCustomPropertiesToPropertyRows(customProperties); - const dataSource: PropertyRow[] = structuredPropertyRows.concat(customPropertyRows); + const dataSource: PropertyRow[] = filteredStructuredPropertyRows.concat(customPropertyRows); const [expandedRows, setExpandedRows] = useState>(new Set()); diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx index b1a01f2b69fe1..2ed4ab79a41ee 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/StructuredPropertyValue.tsx @@ -1,24 +1,26 @@ import Icon from '@ant-design/icons/lib/components/Icon'; -import React, { useState } from 'react'; +import React from 'react'; import Highlight from 'react-highlighter'; -import { Button, Typography } from 'antd'; +import { Typography } from 'antd'; import styled from 'styled-components'; import { ValueColumnData } from './types'; import { ANTD_GRAY } from '../../constants'; import { useEntityRegistry } from '../../../../useEntityRegistry'; import ExternalLink from '../../../../../images/link-out.svg?react'; -import MarkdownViewer, { MarkdownView } from '../../components/legacy/MarkdownViewer'; +import CompactMarkdownViewer from '../Documentation/components/CompactMarkdownViewer'; import EntityIcon from '../../components/styled/EntityIcon'; const ValueText = styled(Typography.Text)` font-family: 'Manrope'; font-weight: 400; - font-size: 14px; + font-size: 12px; color: ${ANTD_GRAY[9]}; display: block; + width: 100%; + margin-bottom: 2px; - ${MarkdownView} { - font-size: 14px; + .remirror-editor.ProseMirror { + font-size: 12px; } `; @@ -28,38 +30,56 @@ const StyledIcon = styled(Icon)` const IconWrapper = styled.span` margin-right: 4px; + display: flex; `; -const StyledButton = styled(Button)` - margin-top: 2px; +const EntityWrapper = styled.div` + display: flex; + align-items: center; +`; + +const EntityName = styled(Typography.Text)` + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +`; + +const StyledHighlight = styled(Highlight)<{ truncateText?: boolean }>` + line-height: 1.5; + text-wrap: wrap; + + ${(props) => + props.truncateText && + ` + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + max-width: 100%; + display: block; + `} `; interface Props { value: ValueColumnData; isRichText?: boolean; filterText?: string; + truncateText?: boolean; + isFieldColumn?: boolean; } -const MAX_CHARACTERS = 200; - -export default function StructuredPropertyValue({ value, isRichText, filterText }: Props) { +export default function StructuredPropertyValue({ value, isRichText, filterText, truncateText, isFieldColumn }: Props) { const entityRegistry = useEntityRegistry(); - const [showMore, setShowMore] = useState(false); - - const toggleShowMore = () => { - setShowMore(!showMore); - }; - - const valueAsString = value?.value?.toString() ?? ''; return ( {value.entity ? ( - <> + - + - {entityRegistry.getDisplayName(value.entity.type, value.entity)} + + {entityRegistry.getDisplayName(value.entity.type, value.entity)} + - + ) : ( <> {isRichText ? ( - + ) : ( <> - - {showMore ? valueAsString : valueAsString?.substring(0, MAX_CHARACTERS)} - - {valueAsString?.length > MAX_CHARACTERS && ( - - {showMore ? 'Show less' : 'Show more'} - + {truncateText ? ( + + {value.value?.toString() ||
    } + + ) : ( + + {value.value?.toString() ||
    } + )} )} diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx index 9e0b4992d9c78..192b840b50040 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/TabHeader.tsx @@ -1,8 +1,10 @@ import { SearchOutlined } from '@ant-design/icons'; +import { Maybe, StructuredProperties } from '@src/types.generated'; import { Input } from 'antd'; import React from 'react'; import styled from 'styled-components'; import { ANTD_GRAY } from '../../constants'; +import AddPropertyButton from './AddPropertyButton'; const StyledInput = styled(Input)` border-radius: 70px; @@ -12,13 +14,18 @@ const StyledInput = styled(Input)` const TableHeader = styled.div` padding: 8px 16px; border-bottom: 1px solid ${ANTD_GRAY[4.5]}; + display: flex; + justify-content: space-between; `; interface Props { setFilterText: (text: string) => void; + fieldUrn?: string; + fieldProperties?: Maybe; + refetch?: () => void; } -export default function TabHeader({ setFilterText }: Props) { +export default function TabHeader({ setFilterText, fieldUrn, fieldProperties, refetch }: Props) { return ( } /> + ); } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts b/datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts index b93ba886d5a64..4adaafc3d98b6 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/types.ts @@ -22,4 +22,5 @@ export interface PropertyRow { dataType?: DataTypeEntity; isParentRow?: boolean; structuredProperty?: StructuredPropertyEntity; + associatedUrn?: string; } diff --git a/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx b/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx index 4635486c24d1d..18ee6bb18da3d 100644 --- a/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx +++ b/datahub-web-react/src/app/entity/shared/tabs/Properties/useStructuredProperties.tsx @@ -64,6 +64,7 @@ function getStructuredPropertyRows(entityData?: GenericEntityProperties | null) typeNameToType[structuredPropertiesEntry.values[0].__typename].nativeDataType, } : undefined, + associatedUrn: structuredPropertiesEntry.associatedUrn, }); }); diff --git a/datahub-web-react/src/app/entity/shared/types.ts b/datahub-web-react/src/app/entity/shared/types.ts index 00e501740c2ad..ceba5b4bf30eb 100644 --- a/datahub-web-react/src/app/entity/shared/types.ts +++ b/datahub-web-react/src/app/entity/shared/types.ts @@ -122,6 +122,7 @@ export type GenericEntityProperties = { browsePathV2?: Maybe; inputOutput?: Maybe; forms?: Maybe; + parent?: Maybe; }; export type GenericEntityUpdate = { diff --git a/datahub-web-react/src/app/lineage/LineageEntityNode.tsx b/datahub-web-react/src/app/lineage/LineageEntityNode.tsx index fcf2198e7e004..994090059754c 100644 --- a/datahub-web-react/src/app/lineage/LineageEntityNode.tsx +++ b/datahub-web-react/src/app/lineage/LineageEntityNode.tsx @@ -19,6 +19,10 @@ import ManageLineageMenu from './manage/ManageLineageMenu'; import { useGetLineageTimeParams } from './utils/useGetLineageTimeParams'; import { EntityHealth } from '../entity/shared/containers/profile/header/EntityHealth'; import { EntityType } from '../../types.generated'; +import StructuredPropertyBadge, { + MAX_PROP_BADGE_WIDTH, +} from '../entity/shared/containers/profile/header/StructuredPropertyBadge'; +import { filterForAssetBadge } from '../entity/shared/containers/profile/header/utils'; const CLICK_DELAY_THRESHOLD = 1000; const DRAG_DISTANCE_THRESHOLD = 20; @@ -38,6 +42,11 @@ const MultilineTitleText = styled.p` word-break: break-all; `; +const PropertyBadgeWrapper = styled.div` + display: flex; + justify-content: flex-end; +`; + export default function LineageEntityNode({ node, isSelected, @@ -150,6 +159,11 @@ export default function LineageEntityNode({ const baseUrl = node.data.type && node.data.urn && entityRegistry.getEntityUrl(node.data.type, node.data.urn); const hasHealth = (health && baseUrl) || false; + const entityStructuredProps = node.data.structuredProperties; + const hasAssetBadge = entityStructuredProps?.properties?.find(filterForAssetBadge); + const siblingStructuredProps = node.data.siblingStructuredProperties; + const siblingHasAssetBadge = siblingStructuredProps?.properties?.find(filterForAssetBadge); + return ( {unexploredHiddenChildren && (isHovered || isSelected) ? ( @@ -340,6 +354,32 @@ export default function LineageEntityNode({ /> )} + {hasAssetBadge && ( + e.stopPropagation()} + > + + + + + )} + {!hasAssetBadge && siblingHasAssetBadge && ( + e.stopPropagation()} + > + + + + + )} ; + structuredProperties?: Maybe; }; export type NodeData = { @@ -82,6 +84,8 @@ export type NodeData = { upstreamRelationships?: Array; downstreamRelationships?: Array; health?: Maybe; + structuredProperties?: Maybe; + siblingStructuredProperties?: Maybe; }; export type VizNode = { diff --git a/datahub-web-react/src/app/lineage/utils/constructFetchedNode.ts b/datahub-web-react/src/app/lineage/utils/constructFetchedNode.ts index 12d4cca352bb3..bb9f29522d0cb 100644 --- a/datahub-web-react/src/app/lineage/utils/constructFetchedNode.ts +++ b/datahub-web-react/src/app/lineage/utils/constructFetchedNode.ts @@ -68,6 +68,7 @@ export default function constructFetchedNode( upstreamRelationships: fetchedNode?.upstreamRelationships || [], downstreamRelationships: fetchedNode?.downstreamRelationships || [], health: fetchedNode?.health, + structuredProperties: fetchedNode?.structuredProperties, }; // eslint-disable-next-line no-param-reassign diff --git a/datahub-web-react/src/app/lineage/utils/constructTree.ts b/datahub-web-react/src/app/lineage/utils/constructTree.ts index 38a865ea9e093..4e94ad2813674 100644 --- a/datahub-web-react/src/app/lineage/utils/constructTree.ts +++ b/datahub-web-react/src/app/lineage/utils/constructTree.ts @@ -83,6 +83,8 @@ export default function constructTree( }); const fetchedEntity = entityRegistry.getLineageVizConfig(entityAndType.type, entityAndType.entity); + const sibling = fetchedEntity?.siblings?.siblings?.[0]; + const fetchedSiblingEntity = sibling ? entityRegistry.getLineageVizConfig(sibling.type, sibling) : null; const root: NodeData = { name: fetchedEntity?.name || '', @@ -100,6 +102,8 @@ export default function constructTree( upstreamRelationships: fetchedEntity?.upstreamRelationships || [], downstreamRelationships: fetchedEntity?.downstreamRelationships || [], health: fetchedEntity?.health, + structuredProperties: fetchedEntity?.structuredProperties, + siblingStructuredProperties: fetchedSiblingEntity?.structuredProperties, }; const lineageConfig = entityRegistry.getLineageVizConfig(entityAndType.type, entityAndType.entity); let updatedLineageConfig = { ...lineageConfig }; diff --git a/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx b/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx index 0d5cfaaf42b9a..4c8948a6664e0 100644 --- a/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx +++ b/datahub-web-react/src/app/preview/DefaultPreviewCard.tsx @@ -37,6 +37,8 @@ import { DataProductLink } from '../shared/tags/DataProductLink'; import { EntityHealth } from '../entity/shared/containers/profile/header/EntityHealth'; import SearchTextHighlighter from '../search/matches/SearchTextHighlighter'; import { getUniqueOwners } from './utils'; +import StructuredPropertyBadge from '../entity/shared/containers/profile/header/StructuredPropertyBadge'; +import { usePreviewData } from '../entity/shared/PreviewContext'; const PreviewContainer = styled.div` display: flex; @@ -245,6 +247,7 @@ export default function DefaultPreviewCard({ // sometimes these lists will be rendered inside an entity container (for example, in the case of impact analysis) // in those cases, we may want to enrich the preview w/ context about the container entity const { entityData } = useEntityData(); + const previewData = usePreviewData(); const insightViews: Array = [ ...(insights?.map((insight) => ( <> @@ -305,6 +308,7 @@ export default function DefaultPreviewCard({ )} {health && health.length > 0 ? : null} + {externalUrl && ( { + return {message}.; +}; + +export default EmptySectionText; diff --git a/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx b/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx index eaca969bb524e..f03fc3b43f320 100644 --- a/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx +++ b/datahub-web-react/src/utils/test-utils/TestPageContainer.tsx @@ -26,6 +26,7 @@ import { DataPlatformEntity } from '../../app/entity/dataPlatform/DataPlatformEn import { ContainerEntity } from '../../app/entity/container/ContainerEntity'; import AppConfigProvider from '../../AppConfigProvider'; import { BusinessAttributeEntity } from '../../app/entity/businessAttribute/BusinessAttributeEntity'; +import { SchemaFieldPropertiesEntity } from '../../app/entity/schemaField/SchemaFieldPropertiesEntity'; type Props = { children: React.ReactNode; @@ -49,6 +50,7 @@ export function getTestEntityRegistry() { entityRegistry.register(new DataPlatformEntity()); entityRegistry.register(new ContainerEntity()); entityRegistry.register(new BusinessAttributeEntity()); + entityRegistry.register(new SchemaFieldPropertiesEntity()); return entityRegistry; } diff --git a/entity-registry/src/main/java/com/linkedin/metadata/models/StructuredPropertyUtils.java b/entity-registry/src/main/java/com/linkedin/metadata/models/StructuredPropertyUtils.java index 1b12f540badfb..612f923d5d68b 100644 --- a/entity-registry/src/main/java/com/linkedin/metadata/models/StructuredPropertyUtils.java +++ b/entity-registry/src/main/java/com/linkedin/metadata/models/StructuredPropertyUtils.java @@ -395,7 +395,8 @@ public static boolean validatePropertySettings( if (settings.isIsHidden()) { if (settings.isShowInSearchFilters() || settings.isShowInAssetSummary() - || settings.isShowAsAssetBadge()) { + || settings.isShowAsAssetBadge() + || settings.isShowInColumnsTable()) { if (shouldThrow) { throw new IllegalArgumentException(INVALID_SETTINGS_MESSAGE); } else {