Skip to content

Commit 1b56035

Browse files
feat(ui/data-contract): Data contract UI under Validation Tab (datahub-project#10625)
Co-authored-by: jayacryl <[email protected]>
1 parent 154591b commit 1b56035

36 files changed

+2272
-49
lines changed

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/featureflags/FeatureFlags.java

+1
Original file line numberDiff line numberDiff line change
@@ -20,4 +20,5 @@ public class FeatureFlags {
2020
private boolean nestedDomainsEnabled = false;
2121
private boolean schemaFieldEntityFetchEnabled = false;
2222
private boolean businessAttributeEntityEnabled = false;
23+
private boolean dataContractsEnabled = false;
2324
}

datahub-graphql-core/src/main/java/com/linkedin/datahub/graphql/resolvers/config/AppConfigResolver.java

+1
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,7 @@ public CompletableFuture<AppConfig> get(final DataFetchingEnvironment environmen
185185
.setShowAccessManagement(_featureFlags.isShowAccessManagement())
186186
.setNestedDomainsEnabled(_featureFlags.isNestedDomainsEnabled())
187187
.setPlatformBrowseV2(_featureFlags.isPlatformBrowseV2())
188+
.setDataContractsEnabled(_featureFlags.isDataContractsEnabled())
188189
.build();
189190

190191
appConfig.setFeatureFlags(featureFlagsConfig);

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

+5
Original file line numberDiff line numberDiff line change
@@ -492,6 +492,11 @@ type FeatureFlagsConfig {
492492
Whether business attribute entity should be shown
493493
"""
494494
businessAttributeEntityEnabled: Boolean!
495+
496+
"""
497+
Whether data contracts should be enabled
498+
"""
499+
dataContractsEnabled: Boolean!
495500
}
496501

497502
"""

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

+15
Original file line numberDiff line numberDiff line change
@@ -12586,3 +12586,18 @@ type ListBusinessAttributesResult {
1258612586
"""
1258712587
businessAttributes: [BusinessAttribute!]!
1258812588
}
12589+
12590+
"""
12591+
A cron schedule
12592+
"""
12593+
type CronSchedule {
12594+
"""
12595+
A cron-formatted execution interval, as a cron string, e.g. 1 * * * *
12596+
"""
12597+
cron: String!
12598+
12599+
"""
12600+
Timezone in which the cron interval applies, e.g. America/Los_Angeles
12601+
"""
12602+
timezone: String!
12603+
}

datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/Assertions.tsx

+14-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import { DatasetAssertionsList } from './DatasetAssertionsList';
66
import { DatasetAssertionsSummary } from './DatasetAssertionsSummary';
77
import { sortAssertions } from './assertionUtils';
88
import { combineEntityDataWithSiblings, useIsSeparateSiblingsMode } from '../../../siblingUtils';
9+
import { useGetDatasetContractQuery } from '../../../../../../graphql/contract.generated';
910

1011
/**
1112
* Returns a status summary for the assertions associated with a Dataset.
@@ -15,6 +16,7 @@ const getAssertionsStatusSummary = (assertions: Array<Assertion>) => {
1516
failedRuns: 0,
1617
succeededRuns: 0,
1718
totalRuns: 0,
19+
erroredRuns: 0,
1820
totalAssertions: assertions.length,
1921
};
2022
assertions.forEach((assertion) => {
@@ -27,7 +29,12 @@ const getAssertionsStatusSummary = (assertions: Array<Assertion>) => {
2729
if (AssertionResultType.Failure === resultType) {
2830
summary.failedRuns++;
2931
}
30-
summary.totalRuns++; // only count assertions for which there is one completed run event!
32+
if (AssertionResultType.Error === resultType) {
33+
summary.erroredRuns++;
34+
}
35+
if (AssertionResultType.Init !== resultType) {
36+
summary.totalRuns++; // only count assertions for which there is one completed run event, ignoring INIT statuses!
37+
}
3138
}
3239
});
3340
return summary;
@@ -46,6 +53,11 @@ export const Assertions = () => {
4653
const combinedData = isHideSiblingMode ? data : combineEntityDataWithSiblings(data);
4754
const [removedUrns, setRemovedUrns] = useState<string[]>([]);
4855

56+
const { data: contractData } = useGetDatasetContractQuery({
57+
variables: { urn },
58+
fetchPolicy: 'cache-first',
59+
});
60+
const contract = contractData?.dataset?.contract as any;
4961
const assertions =
5062
(combinedData && combinedData.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) ||
5163
[];
@@ -67,6 +79,7 @@ export const Assertions = () => {
6779
setRemovedUrns([...removedUrns, assertionUrn]);
6880
setTimeout(() => refetch(), 3000);
6981
}}
82+
contract={contract}
7083
/>
7184
)}
7285
</>

datahub-web-react/src/app/entity/shared/tabs/Dataset/Validations/DatasetAssertionsList.tsx

+145-45
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,27 @@
1-
import { Button, Dropdown, Empty, Image, message, Modal, Tag, Tooltip, Typography } from 'antd';
1+
import { Button, Dropdown, Empty, Image, message, Modal, Tag, Tooltip, Typography, Checkbox } from 'antd';
22
import React from 'react';
33
import styled from 'styled-components';
4-
import { DeleteOutlined, DownOutlined, MoreOutlined, RightOutlined, StopOutlined } from '@ant-design/icons';
4+
import {
5+
DeleteOutlined,
6+
DownOutlined,
7+
MoreOutlined,
8+
RightOutlined,
9+
StopOutlined,
10+
AuditOutlined,
11+
} from '@ant-design/icons';
12+
import { Link } from 'react-router-dom';
513
import { DatasetAssertionDescription } from './DatasetAssertionDescription';
614
import { StyledTable } from '../../../components/styled/StyledTable';
715
import { DatasetAssertionDetails } from './DatasetAssertionDetails';
8-
import { Assertion, AssertionRunStatus } from '../../../../../../types.generated';
16+
import { Assertion, AssertionRunStatus, DataContract, EntityType } from '../../../../../../types.generated';
917
import { getResultColor, getResultIcon, getResultText } from './assertionUtils';
1018
import { useDeleteAssertionMutation } from '../../../../../../graphql/assertion.generated';
1119
import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil';
1220
import AssertionMenu from './AssertionMenu';
21+
import { REDESIGN_COLORS } from '../../../constants';
22+
import { useEntityRegistry } from '../../../../../useEntityRegistry';
23+
import { isAssertionPartOfContract } from './contract/utils';
24+
import { useEntityData } from '../../../EntityContext';
1325

1426
const ResultContainer = styled.div`
1527
display: flex;
@@ -35,9 +47,26 @@ const StyledMoreOutlined = styled(MoreOutlined)`
3547
font-size: 18px;
3648
`;
3749

50+
const AssertionSelectCheckbox = styled(Checkbox)`
51+
margin-right: 12px;
52+
`;
53+
54+
const DataContractLogo = styled(AuditOutlined)`
55+
margin-left: 8px;
56+
font-size: 16px;
57+
color: ${REDESIGN_COLORS.BLUE};
58+
`;
59+
3860
type Props = {
3961
assertions: Array<Assertion>;
4062
onDelete?: (urn: string) => void;
63+
contract?: DataContract;
64+
// required for enabling menu/actions
65+
showMenu?: boolean;
66+
onSelect?: (assertionUrn: string) => void;
67+
// required for enabling selection logic
68+
showSelect?: boolean;
69+
selectedUrns?: string[];
4170
};
4271

4372
/**
@@ -46,8 +75,18 @@ type Props = {
4675
*
4776
* Currently this component supports rendering Dataset Assertions only.
4877
*/
49-
export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
78+
export const DatasetAssertionsList = ({
79+
assertions,
80+
onDelete,
81+
showMenu = true,
82+
showSelect,
83+
onSelect,
84+
selectedUrns,
85+
contract,
86+
}: Props) => {
87+
const entityData = useEntityData();
5088
const [deleteAssertionMutation] = useDeleteAssertionMutation();
89+
const entityRegistry = useEntityRegistry();
5190

5291
const deleteAssertion = async (urn: string) => {
5392
try {
@@ -102,9 +141,19 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
102141
const resultColor = (record.lastExecResult && getResultColor(record.lastExecResult)) || 'default';
103142
const resultText = (record.lastExecResult && getResultText(record.lastExecResult)) || 'No Evaluations';
104143
const resultIcon = (record.lastExecResult && getResultIcon(record.lastExecResult)) || <StopOutlined />;
144+
const selected = selectedUrns?.some((selectedUrn) => selectedUrn === record.urn);
145+
const isPartOfContract = contract && isAssertionPartOfContract(record, contract);
146+
105147
const { description } = record;
106148
return (
107149
<ResultContainer>
150+
{showSelect ? (
151+
<AssertionSelectCheckbox
152+
checked={selected}
153+
onClick={(e) => e.stopPropagation()}
154+
onChange={() => onSelect?.(record.urn as string)}
155+
/>
156+
) : undefined}
108157
<div>
109158
<Tooltip title={(localTime && `Last evaluated on ${localTime}`) || 'No Evaluations'}>
110159
<Tag style={{ borderColor: resultColor }}>
@@ -117,6 +166,34 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
117166
description={description}
118167
assertionInfo={record.datasetAssertionInfo}
119168
/>
169+
{(isPartOfContract && entityData?.urn && (
170+
<Tooltip
171+
title={
172+
<>
173+
Part of Data Contract{' '}
174+
<Link
175+
to={`${entityRegistry.getEntityUrl(
176+
EntityType.Dataset,
177+
entityData.urn,
178+
)}/Validation/Data Contract`}
179+
style={{ color: REDESIGN_COLORS.BLUE }}
180+
>
181+
view
182+
</Link>
183+
</>
184+
}
185+
>
186+
<Link
187+
to={`${entityRegistry.getEntityUrl(
188+
EntityType.Dataset,
189+
entityData.urn,
190+
)}/Validation/Data Contract`}
191+
>
192+
<DataContractLogo />
193+
</Link>
194+
</Tooltip>
195+
)) ||
196+
undefined}
120197
</ResultContainer>
121198
);
122199
},
@@ -126,35 +203,40 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
126203
dataIndex: '',
127204
key: '',
128205
render: (_, record: any) => (
129-
<ActionButtonContainer>
130-
<Tooltip
131-
title={
132-
record.platform.properties?.displayName || capitalizeFirstLetterOnly(record.platform.name)
133-
}
134-
>
135-
<PlatformContainer>
136-
{(record.platform.properties?.logoUrl && (
137-
<Image
138-
preview={false}
139-
height={20}
140-
width={20}
141-
src={record.platform.properties?.logoUrl}
142-
/>
143-
)) || (
144-
<Typography.Text>
145-
{record.platform.properties?.displayName ||
146-
capitalizeFirstLetterOnly(record.platform.name)}
147-
</Typography.Text>
148-
)}
149-
</PlatformContainer>
150-
</Tooltip>
151-
<Button onClick={() => onDeleteAssertion(record.urn)} type="text" shape="circle" danger>
152-
<DeleteOutlined />
153-
</Button>
154-
<Dropdown overlay={<AssertionMenu urn={record.urn} />} trigger={['click']}>
155-
<StyledMoreOutlined />
156-
</Dropdown>
157-
</ActionButtonContainer>
206+
<>
207+
{showMenu && (
208+
<ActionButtonContainer>
209+
<Tooltip
210+
title={
211+
record.platform.properties?.displayName ||
212+
capitalizeFirstLetterOnly(record.platform.name)
213+
}
214+
>
215+
<PlatformContainer>
216+
{(record.platform.properties?.logoUrl && (
217+
<Image
218+
preview={false}
219+
height={20}
220+
width={20}
221+
src={record.platform.properties?.logoUrl}
222+
/>
223+
)) || (
224+
<Typography.Text>
225+
{record.platform.properties?.displayName ||
226+
capitalizeFirstLetterOnly(record.platform.name)}
227+
</Typography.Text>
228+
)}
229+
</PlatformContainer>
230+
</Tooltip>
231+
<Button onClick={() => onDeleteAssertion(record.urn)} type="text" shape="circle" danger>
232+
<DeleteOutlined />
233+
</Button>
234+
<Dropdown overlay={<AssertionMenu urn={record.urn} />} trigger={['click']}>
235+
<StyledMoreOutlined />
236+
</Dropdown>
237+
</ActionButtonContainer>
238+
)}
239+
</>
158240
),
159241
},
160242
];
@@ -168,18 +250,36 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
168250
locale={{
169251
emptyText: <Empty description="No Assertions Found :(" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
170252
}}
171-
expandable={{
172-
defaultExpandAllRows: false,
173-
expandRowByClick: true,
174-
expandedRowRender: (record) => {
175-
return <DatasetAssertionDetails urn={record.urn} lastEvaluatedAtMillis={record.lastExecTime} />;
176-
},
177-
expandIcon: ({ expanded, onExpand, record }: any) =>
178-
expanded ? (
179-
<DownOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
180-
) : (
181-
<RightOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
182-
),
253+
expandable={
254+
showSelect
255+
? {}
256+
: {
257+
defaultExpandAllRows: false,
258+
expandRowByClick: true,
259+
expandedRowRender: (record) => {
260+
return (
261+
<DatasetAssertionDetails
262+
urn={record.urn}
263+
lastEvaluatedAtMillis={record.lastExecTime}
264+
/>
265+
);
266+
},
267+
expandIcon: ({ expanded, onExpand, record }: any) =>
268+
expanded ? (
269+
<DownOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
270+
) : (
271+
<RightOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
272+
),
273+
}
274+
}
275+
onRow={(record) => {
276+
return {
277+
onClick: (_) => {
278+
if (showSelect) {
279+
onSelect?.(record.urn as string);
280+
}
281+
},
282+
};
183283
}}
184284
showHeader={false}
185285
pagination={false}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import React from 'react';
2+
import { Typography } from 'antd';
3+
import { FieldAssertionInfo } from '../../../../../../types.generated';
4+
import {
5+
getFieldDescription,
6+
getFieldOperatorDescription,
7+
getFieldParametersDescription,
8+
getFieldTransformDescription,
9+
} from './fieldDescriptionUtils';
10+
11+
type Props = {
12+
assertionInfo: FieldAssertionInfo;
13+
};
14+
15+
/**
16+
* A human-readable description of a Field Assertion.
17+
*/
18+
export const FieldAssertionDescription = ({ assertionInfo }: Props) => {
19+
const field = getFieldDescription(assertionInfo);
20+
const operator = getFieldOperatorDescription(assertionInfo);
21+
const transform = getFieldTransformDescription(assertionInfo);
22+
const parameters = getFieldParametersDescription(assertionInfo);
23+
24+
return (
25+
<Typography.Text>
26+
{transform}
27+
{transform ? ' of ' : ''}
28+
<Typography.Text code>{field}</Typography.Text> {operator} {parameters}
29+
</Typography.Text>
30+
);
31+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from 'react';
2+
import { Typography } from 'antd';
3+
import { AssertionInfo } from '../../../../../../types.generated';
4+
5+
type Props = {
6+
assertionInfo: AssertionInfo;
7+
};
8+
9+
/**
10+
* A human-readable description of a SQL Assertion.
11+
*/
12+
export const SqlAssertionDescription = ({ assertionInfo }: Props) => {
13+
const { description } = assertionInfo;
14+
15+
return <Typography.Text>{description}</Typography.Text>;
16+
};

0 commit comments

Comments
 (0)