Skip to content

Commit 1bcdda7

Browse files
authored
feat(data contracts): supporting structured properties on data contracts (#13176)
1 parent ed0cfe9 commit 1bcdda7

File tree

19 files changed

+265
-47
lines changed

19 files changed

+265
-47
lines changed

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/EntityDataContractResolver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public CompletableFuture<DataContract> get(DataFetchingEnvironment environment)
8383

8484
if (entityResponse != null) {
8585
// Step 4: Package and return result
86-
return DataContractMapper.mapContract(entityResponse);
86+
return DataContractMapper.mapContract(context, entityResponse);
8787
}
8888
}
8989
// No contract found

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/datacontract/UpsertDataContractResolver.java

+1-1
Original file line numberDiff line numberDiff line change
@@ -115,7 +115,7 @@ public CompletableFuture<DataContract> get(final DataFetchingEnvironment environ
115115
null);
116116

117117
// Package and return result
118-
return DataContractMapper.mapContract(entityResponse);
118+
return DataContractMapper.mapContract(context, entityResponse);
119119
} catch (Exception e) {
120120
throw new RuntimeException(
121121
String.format("Failed to perform update against input %s", input.toString()), e);

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapper.java

+16-1
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.linkedin.datahub.graphql.types.datacontract;
22

3+
import com.linkedin.datahub.graphql.QueryContext;
34
import com.linkedin.datahub.graphql.generated.Assertion;
45
import com.linkedin.datahub.graphql.generated.DataContract;
56
import com.linkedin.datahub.graphql.generated.DataContractProperties;
@@ -9,16 +10,20 @@
910
import com.linkedin.datahub.graphql.generated.EntityType;
1011
import com.linkedin.datahub.graphql.generated.FreshnessContract;
1112
import com.linkedin.datahub.graphql.generated.SchemaContract;
13+
import com.linkedin.datahub.graphql.types.structuredproperty.StructuredPropertiesMapper;
1214
import com.linkedin.entity.EntityResponse;
1315
import com.linkedin.entity.EnvelopedAspect;
1416
import com.linkedin.entity.EnvelopedAspectMap;
1517
import com.linkedin.metadata.Constants;
18+
import com.linkedin.structured.StructuredProperties;
1619
import java.util.stream.Collectors;
1720
import javax.annotation.Nonnull;
21+
import javax.annotation.Nullable;
1822

1923
public class DataContractMapper {
2024

21-
public static DataContract mapContract(@Nonnull final EntityResponse entityResponse) {
25+
public static DataContract mapContract(
26+
@Nullable final QueryContext context, @Nonnull final EntityResponse entityResponse) {
2227
final DataContract result = new DataContract();
2328
final EnvelopedAspectMap aspects = entityResponse.getAspects();
2429

@@ -37,6 +42,16 @@ public static DataContract mapContract(@Nonnull final EntityResponse entityRespo
3742
String.format("Data Contract does not exist!. urn: %s", entityResponse.getUrn()));
3843
}
3944

45+
final EnvelopedAspect dataContractStructuredProperties =
46+
aspects.get(Constants.STRUCTURED_PROPERTIES_ASPECT_NAME);
47+
if (dataContractStructuredProperties != null) {
48+
result.setStructuredProperties(
49+
StructuredPropertiesMapper.map(
50+
context,
51+
new StructuredProperties(dataContractStructuredProperties.getValue().data()),
52+
entityResponse.getUrn()));
53+
}
54+
4055
final EnvelopedAspect dataContractStatus =
4156
aspects.get(Constants.DATA_CONTRACT_STATUS_ASPECT_NAME);
4257
if (dataContractStatus != null) {

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/datacontract/DataContractType.java

+2-1
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ public class DataContractType
2828
ImmutableSet.of(
2929
Constants.DATA_CONTRACT_KEY_ASPECT_NAME,
3030
Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME,
31+
Constants.STRUCTURED_PROPERTIES_ASPECT_NAME,
3132
Constants.DATA_CONTRACT_STATUS_ASPECT_NAME);
3233
private final EntityClient _entityClient;
3334

@@ -74,7 +75,7 @@ public List<DataFetcherResult<DataContract>> batchLoad(
7475
gmsResult == null
7576
? null
7677
: DataFetcherResult.<DataContract>newResult()
77-
.data(DataContractMapper.mapContract(gmsResult))
78+
.data(DataContractMapper.mapContract(context, gmsResult))
7879
.build())
7980
.collect(Collectors.toList());
8081
} catch (Exception e) {

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/types/entitytype/EntityTypeUrnMapper.java

+1
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ public class EntityTypeUrnMapper {
3131
.put(Constants.DASHBOARD_ENTITY_NAME, "urn:li:entityType:datahub.dashboard")
3232
.put(Constants.NOTEBOOK_ENTITY_NAME, "urn:li:entityType:datahub.notebook")
3333
.put(Constants.CHART_ENTITY_NAME, "urn:li:entityType:datahub.chart")
34+
.put(Constants.DATA_CONTRACT_ENTITY_NAME, "urn:li:entityType:datahub.dataContract")
3435
.put(Constants.DATA_FLOW_ENTITY_NAME, "urn:li:entityType:datahub.dataFlow")
3536
.put(Constants.DATA_JOB_ENTITY_NAME, "urn:li:entityType:datahub.dataJob")
3637
.put(Constants.TAG_ENTITY_NAME, "urn:li:entityType:datahub.tag")

datahub-graphql-core/src/main/resources/contract.graphql

+5
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,11 @@ type DataContract implements Entity {
4040
"""
4141
status: DataContractStatus
4242

43+
"""
44+
Structured properties about this Data Contract
45+
"""
46+
structuredProperties: StructuredProperties
47+
4348
"""
4449
List of relationships between the source Entity and some destination entities with a given types
4550
"""

datahub-graphql-core/src/test/java/com/linkedin/datahub/graphql/types/datacontract/DataContractMapperTest.java

+3-3
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ public void testMapAllFields() throws Exception {
7979
Constants.DATA_CONTRACT_STATUS_ASPECT_NAME,
8080
envelopedDataContractStatus)));
8181

82-
DataContract dataContract = DataContractMapper.mapContract(entityResponse);
82+
DataContract dataContract = DataContractMapper.mapContract(null, entityResponse);
8383
assertNotNull(dataContract);
8484
assertEquals(dataContract.getUrn(), urn.toString());
8585
assertEquals(dataContract.getType(), EntityType.DATA_CONTRACT);
@@ -136,7 +136,7 @@ public void testMapRequiredFields() throws Exception {
136136
Constants.DATA_CONTRACT_STATUS_ASPECT_NAME,
137137
envelopedDataContractStatus)));
138138

139-
DataContract dataContract = DataContractMapper.mapContract(entityResponse);
139+
DataContract dataContract = DataContractMapper.mapContract(null, entityResponse);
140140
assertNotNull(dataContract);
141141
assertEquals(dataContract.getUrn(), urn.toString());
142142
assertEquals(dataContract.getType(), EntityType.DATA_CONTRACT);
@@ -167,7 +167,7 @@ public void testMapNoStatus() throws Exception {
167167
ImmutableMap.of(
168168
Constants.DATA_CONTRACT_PROPERTIES_ASPECT_NAME, envelopedDataContractProperties)));
169169

170-
DataContract dataContract = DataContractMapper.mapContract(entityResponse);
170+
DataContract dataContract = DataContractMapper.mapContract(null, entityResponse);
171171
assertNotNull(dataContract);
172172
assertEquals(dataContract.getUrn(), urn.toString());
173173
assertEquals(dataContract.getType(), EntityType.DATA_CONTRACT);

datahub-web-react/src/app/buildEntityRegistryV2.ts

+17-15
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,36 @@
1+
import { RoleEntity } from './entityV2/Access/RoleEntity';
12
import EntityRegistry from './entityV2/EntityRegistry';
2-
import { DashboardEntity } from './entityV2/dashboard/DashboardEntity';
3+
import { BusinessAttributeEntity } from './entityV2/businessAttribute/BusinessAttributeEntity';
34
import { ChartEntity } from './entityV2/chart/ChartEntity';
4-
import { UserEntity } from './entityV2/user/User';
5-
import { GroupEntity } from './entityV2/group/Group';
6-
import { DatasetEntity } from './entityV2/dataset/DatasetEntity';
5+
import { ContainerEntity } from './entityV2/container/ContainerEntity';
6+
import { DashboardEntity } from './entityV2/dashboard/DashboardEntity';
7+
import { DataContractEntity } from './entityV2/dataContract/DataContractEntity';
78
import { DataFlowEntity } from './entityV2/dataFlow/DataFlowEntity';
89
import { DataJobEntity } from './entityV2/dataJob/DataJobEntity';
9-
import { TagEntity } from './entityV2/tag/Tag';
10+
import { DataPlatformEntity } from './entityV2/dataPlatform/DataPlatformEntity';
11+
import { DataPlatformInstanceEntity } from './entityV2/dataPlatformInstance/DataPlatformInstanceEntity';
12+
import { DataProcessInstanceEntity } from './entityV2/dataProcessInstance/DataProcessInstanceEntity';
13+
import { DataProductEntity } from './entityV2/dataProduct/DataProductEntity';
14+
import { DatasetEntity } from './entityV2/dataset/DatasetEntity';
15+
import { DomainEntity } from './entityV2/domain/DomainEntity';
16+
import GlossaryNodeEntity from './entityV2/glossaryNode/GlossaryNodeEntity';
1017
import { GlossaryTermEntity } from './entityV2/glossaryTerm/GlossaryTermEntity';
18+
import { GroupEntity } from './entityV2/group/Group';
1119
import { MLFeatureEntity } from './entityV2/mlFeature/MLFeatureEntity';
12-
import { MLPrimaryKeyEntity } from './entityV2/mlPrimaryKey/MLPrimaryKeyEntity';
1320
import { MLFeatureTableEntity } from './entityV2/mlFeatureTable/MLFeatureTableEntity';
1421
import { MLModelEntity } from './entityV2/mlModel/MLModelEntity';
1522
import { MLModelGroupEntity } from './entityV2/mlModelGroup/MLModelGroupEntity';
16-
import { DomainEntity } from './entityV2/domain/DomainEntity';
17-
import { ContainerEntity } from './entityV2/container/ContainerEntity';
18-
import GlossaryNodeEntity from './entityV2/glossaryNode/GlossaryNodeEntity';
19-
import { DataPlatformEntity } from './entityV2/dataPlatform/DataPlatformEntity';
20-
import { DataProductEntity } from './entityV2/dataProduct/DataProductEntity';
21-
import { DataPlatformInstanceEntity } from './entityV2/dataPlatformInstance/DataPlatformInstanceEntity';
22-
import { RoleEntity } from './entityV2/Access/RoleEntity';
23+
import { MLPrimaryKeyEntity } from './entityV2/mlPrimaryKey/MLPrimaryKeyEntity';
2324
import { QueryEntity } from './entityV2/query/QueryEntity';
2425
import { SchemaFieldEntity } from './entityV2/schemaField/SchemaFieldEntity';
2526
import { StructuredPropertyEntity } from './entityV2/structuredProperty/StructuredPropertyEntity';
26-
import { DataProcessInstanceEntity } from './entityV2/dataProcessInstance/DataProcessInstanceEntity';
27-
import { BusinessAttributeEntity } from './entityV2/businessAttribute/BusinessAttributeEntity';
27+
import { TagEntity } from './entityV2/tag/Tag';
28+
import { UserEntity } from './entityV2/user/User';
2829

2930
export default function buildEntityRegistryV2() {
3031
const registry = new EntityRegistry();
3132
registry.register(new DatasetEntity());
33+
registry.register(new DataContractEntity());
3234
registry.register(new DashboardEntity());
3335
registry.register(new ChartEntity());
3436
registry.register(new UserEntity());
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
import { FileOutlined } from '@ant-design/icons';
2+
import { TYPE_ICON_CLASS_NAME } from '@src/app/shared/constants';
3+
import { DataContract, EntityType } from '@src/types.generated';
4+
import * as React from 'react';
5+
import { Entity, IconStyleType } from '../Entity';
6+
import { getDataForEntityType } from '../shared/containers/profile/utils';
7+
8+
/**
9+
* Definition of the DataHub DataFlow entity.
10+
*/
11+
export class DataContractEntity implements Entity<DataContract> {
12+
type: EntityType = EntityType.DataContract;
13+
14+
icon = (fontSize?: number, styleType?: IconStyleType, color?: string) => {
15+
if (styleType === IconStyleType.TAB_VIEW) {
16+
return <FileOutlined className={TYPE_ICON_CLASS_NAME} style={{ fontSize, color }} />;
17+
}
18+
19+
if (styleType === IconStyleType.HIGHLIGHT) {
20+
return <FileOutlined className={TYPE_ICON_CLASS_NAME} style={{ fontSize, color: color || '#d6246c' }} />;
21+
}
22+
23+
return (
24+
<FileOutlined
25+
className={TYPE_ICON_CLASS_NAME}
26+
style={{
27+
fontSize,
28+
color: color || '#BFBFBF',
29+
}}
30+
/>
31+
);
32+
};
33+
34+
isSearchEnabled = () => true;
35+
36+
isBrowseEnabled = () => true;
37+
38+
isLineageEnabled = () => false;
39+
40+
getAutoCompleteFieldName = () => 'name';
41+
42+
getGraphName = () => 'dataContract';
43+
44+
getPathName = () => 'dataContracts';
45+
46+
getEntityName = () => 'Data Contract';
47+
48+
getCollectionName = () => 'Data Contracts';
49+
50+
renderProfile = () => <span>Not Implemented</span>;
51+
52+
getSidebarSections = () => [];
53+
54+
getSidebarTabs = () => [];
55+
56+
getOverridePropertiesFromEntity = () => {};
57+
58+
renderPreview = () => {
59+
return <span>Not Implemented</span>;
60+
};
61+
62+
renderSearch = () => {
63+
return <span>Not Implemented</span>;
64+
};
65+
66+
displayName = () => {
67+
return 'Data Contract';
68+
};
69+
70+
getGenericEntityProperties = (data: DataContract) => {
71+
return getDataForEntityType({
72+
data,
73+
entityType: this.type,
74+
});
75+
};
76+
77+
supportedCapabilities = () => {
78+
return new Set([]);
79+
};
80+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
import { EntityContext } from '@src/app/entity/shared/EntityContext';
2+
import { useEntityRegistry } from '@src/app/useEntityRegistry';
3+
import React from 'react';
4+
import styled from 'styled-components';
5+
import { DataContract, EntityType } from '../../../../../../../types.generated';
6+
import { ANTD_GRAY } from '../../../../constants';
7+
import { PropertiesTab } from '../../../Properties/PropertiesTab';
8+
9+
const TitleText = styled.div`
10+
color: ${ANTD_GRAY[7]};
11+
margin-bottom: 20px;
12+
letter-spacing: 1px;
13+
`;
14+
const Container = styled.div`
15+
padding: 28px;
16+
`;
17+
18+
const PropertiesContainer = styled.div`
19+
width: 100%;
20+
border-radius: 8px;
21+
box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.1);
22+
`;
23+
24+
type Props = {
25+
refetch: () => Promise<any>;
26+
contract: DataContract;
27+
};
28+
29+
export const ContractStructuredPropertiesSummary = ({ contract, refetch }: Props) => {
30+
// turn `contract` into a `GenericEntityProperties` object
31+
const entityRegistry = useEntityRegistry();
32+
const entityData = entityRegistry.getGenericEntityProperties(EntityType.DataContract, contract);
33+
return (
34+
<Container>
35+
<TitleText>PROPERTIES</TitleText>
36+
<EntityContext.Provider
37+
value={{
38+
urn: contract.urn,
39+
entityType: EntityType.DataContract,
40+
entityData,
41+
loading: false,
42+
baseEntity: contract,
43+
dataNotCombinedWithSiblings: contract,
44+
routeToTab: () => {},
45+
lineage: undefined,
46+
refetch,
47+
}}
48+
>
49+
<PropertiesContainer>
50+
<PropertiesTab
51+
properties={{
52+
refetch,
53+
disableEdit: true,
54+
disableSearch: true,
55+
}}
56+
/>
57+
</PropertiesContainer>
58+
</EntityContext.Provider>
59+
</Container>
60+
);
61+
};

datahub-web-react/src/app/entityV2/shared/tabs/Dataset/Validations/contract/DataContractTab.tsx

+13-4
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,16 @@
11
import React, { useState } from 'react';
22
import styled from 'styled-components';
33
import { useGetDatasetContractQuery } from '../../../../../../../graphql/contract.generated';
4-
import { DataContractState } from '../../../../../../../types.generated';
4+
import { DataContract, DataContractState } from '../../../../../../../types.generated';
55
import { useEntityData } from '../../../../../../entity/shared/EntityContext';
6+
import { DataContractEmptyState } from '../../../../../../entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState';
7+
import { getAssertionsSummary } from '../acrylUtils';
8+
import { ContractStructuredPropertiesSummary } from './ContractStructuredPropertiesSummary';
69
import { DataContractSummary } from './DataContractSummary';
710
import { DataQualityContractSummary } from './DataQualityContractSummary';
8-
import { SchemaContractSummary } from './SchemaContractSummary';
911
import { FreshnessContractSummary } from './FreshnessContractSummary';
12+
import { SchemaContractSummary } from './SchemaContractSummary';
1013
import { DataContractBuilderModal } from './builder/DataContractBuilderModal';
11-
import { DataContractEmptyState } from '../../../../../../entity/shared/tabs/Dataset/Validations/contract/DataContractEmptyState';
12-
import { getAssertionsSummary } from '../acrylUtils';
1314

1415
const Container = styled.div`
1516
display: flex;
@@ -52,6 +53,8 @@ export const DataContractTab = () => {
5253
const hasFreshnessContract = freshnessContracts && freshnessContracts?.length;
5354
const hasSchemaContract = schemaContracts && schemaContracts?.length;
5455
const hasDataQualityContract = dataQualityContracts && dataQualityContracts?.length;
56+
const hasStructuredProperties =
57+
contract?.structuredProperties && (contract?.structuredProperties?.properties?.length || 0) > 0;
5558
const showLeftColumn = hasFreshnessContract || hasSchemaContract || undefined;
5659

5760
const onContractUpdate = () => {
@@ -91,6 +94,12 @@ export const DataContractTab = () => {
9194
</LeftColumn>
9295
)}
9396
<RightColumn>
97+
{hasStructuredProperties && contract && (
98+
<ContractStructuredPropertiesSummary
99+
contract={contract as DataContract}
100+
refetch={refetch}
101+
/>
102+
)}
94103
{hasDataQualityContract ? (
95104
<DataQualityContractSummary
96105
contracts={dataQualityContracts as any}

0 commit comments

Comments
 (0)