Skip to content

Commit

Permalink
feat(ui/data-contract): Data contract UI under Validation Tab (datahu…
Browse files Browse the repository at this point in the history
…b-project#10625)

Co-authored-by: jayacryl <159848059+jayacryl@users.noreply.github.com>
  • Loading branch information
amit-apptware and jayacryl authored Jun 19, 2024
1 parent 154591b commit 1b56035
Show file tree
Hide file tree
Showing 36 changed files with 2,272 additions and 49 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,4 +20,5 @@ public class FeatureFlags {
private boolean nestedDomainsEnabled = false;
private boolean schemaFieldEntityFetchEnabled = false;
private boolean businessAttributeEntityEnabled = false;
private boolean dataContractsEnabled = false;
}
Original file line number Diff line number Diff line change
Expand Up @@ -185,6 +185,7 @@ public CompletableFuture<AppConfig> get(final DataFetchingEnvironment environmen
.setShowAccessManagement(_featureFlags.isShowAccessManagement())
.setNestedDomainsEnabled(_featureFlags.isNestedDomainsEnabled())
.setPlatformBrowseV2(_featureFlags.isPlatformBrowseV2())
.setDataContractsEnabled(_featureFlags.isDataContractsEnabled())
.build();

appConfig.setFeatureFlags(featureFlagsConfig);
Expand Down
5 changes: 5 additions & 0 deletions datahub-graphql-core/src/main/resources/app.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -492,6 +492,11 @@ type FeatureFlagsConfig {
Whether business attribute entity should be shown
"""
businessAttributeEntityEnabled: Boolean!

"""
Whether data contracts should be enabled
"""
dataContractsEnabled: Boolean!
}

"""
Expand Down
15 changes: 15 additions & 0 deletions datahub-graphql-core/src/main/resources/entity.graphql
Original file line number Diff line number Diff line change
Expand Up @@ -12586,3 +12586,18 @@ type ListBusinessAttributesResult {
"""
businessAttributes: [BusinessAttribute!]!
}

"""
A cron schedule
"""
type CronSchedule {
"""
A cron-formatted execution interval, as a cron string, e.g. 1 * * * *
"""
cron: String!

"""
Timezone in which the cron interval applies, e.g. America/Los_Angeles
"""
timezone: String!
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { DatasetAssertionsList } from './DatasetAssertionsList';
import { DatasetAssertionsSummary } from './DatasetAssertionsSummary';
import { sortAssertions } from './assertionUtils';
import { combineEntityDataWithSiblings, useIsSeparateSiblingsMode } from '../../../siblingUtils';
import { useGetDatasetContractQuery } from '../../../../../../graphql/contract.generated';

/**
* Returns a status summary for the assertions associated with a Dataset.
Expand All @@ -15,6 +16,7 @@ const getAssertionsStatusSummary = (assertions: Array<Assertion>) => {
failedRuns: 0,
succeededRuns: 0,
totalRuns: 0,
erroredRuns: 0,
totalAssertions: assertions.length,
};
assertions.forEach((assertion) => {
Expand All @@ -27,7 +29,12 @@ const getAssertionsStatusSummary = (assertions: Array<Assertion>) => {
if (AssertionResultType.Failure === resultType) {
summary.failedRuns++;
}
summary.totalRuns++; // only count assertions for which there is one completed run event!
if (AssertionResultType.Error === resultType) {
summary.erroredRuns++;
}
if (AssertionResultType.Init !== resultType) {
summary.totalRuns++; // only count assertions for which there is one completed run event, ignoring INIT statuses!
}
}
});
return summary;
Expand All @@ -46,6 +53,11 @@ export const Assertions = () => {
const combinedData = isHideSiblingMode ? data : combineEntityDataWithSiblings(data);
const [removedUrns, setRemovedUrns] = useState<string[]>([]);

const { data: contractData } = useGetDatasetContractQuery({
variables: { urn },
fetchPolicy: 'cache-first',
});
const contract = contractData?.dataset?.contract as any;
const assertions =
(combinedData && combinedData.dataset?.assertions?.assertions?.map((assertion) => assertion as Assertion)) ||
[];
Expand All @@ -67,6 +79,7 @@ export const Assertions = () => {
setRemovedUrns([...removedUrns, assertionUrn]);
setTimeout(() => refetch(), 3000);
}}
contract={contract}
/>
)}
</>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
import { Button, Dropdown, Empty, Image, message, Modal, Tag, Tooltip, Typography } from 'antd';
import { Button, Dropdown, Empty, Image, message, Modal, Tag, Tooltip, Typography, Checkbox } from 'antd';
import React from 'react';
import styled from 'styled-components';
import { DeleteOutlined, DownOutlined, MoreOutlined, RightOutlined, StopOutlined } from '@ant-design/icons';
import {
DeleteOutlined,
DownOutlined,
MoreOutlined,
RightOutlined,
StopOutlined,
AuditOutlined,
} from '@ant-design/icons';
import { Link } from 'react-router-dom';
import { DatasetAssertionDescription } from './DatasetAssertionDescription';
import { StyledTable } from '../../../components/styled/StyledTable';
import { DatasetAssertionDetails } from './DatasetAssertionDetails';
import { Assertion, AssertionRunStatus } from '../../../../../../types.generated';
import { Assertion, AssertionRunStatus, DataContract, EntityType } from '../../../../../../types.generated';
import { getResultColor, getResultIcon, getResultText } from './assertionUtils';
import { useDeleteAssertionMutation } from '../../../../../../graphql/assertion.generated';
import { capitalizeFirstLetterOnly } from '../../../../../shared/textUtil';
import AssertionMenu from './AssertionMenu';
import { REDESIGN_COLORS } from '../../../constants';
import { useEntityRegistry } from '../../../../../useEntityRegistry';
import { isAssertionPartOfContract } from './contract/utils';
import { useEntityData } from '../../../EntityContext';

const ResultContainer = styled.div`
display: flex;
Expand All @@ -35,9 +47,26 @@ const StyledMoreOutlined = styled(MoreOutlined)`
font-size: 18px;
`;

const AssertionSelectCheckbox = styled(Checkbox)`
margin-right: 12px;
`;

const DataContractLogo = styled(AuditOutlined)`
margin-left: 8px;
font-size: 16px;
color: ${REDESIGN_COLORS.BLUE};
`;

type Props = {
assertions: Array<Assertion>;
onDelete?: (urn: string) => void;
contract?: DataContract;
// required for enabling menu/actions
showMenu?: boolean;
onSelect?: (assertionUrn: string) => void;
// required for enabling selection logic
showSelect?: boolean;
selectedUrns?: string[];
};

/**
Expand All @@ -46,8 +75,18 @@ type Props = {
*
* Currently this component supports rendering Dataset Assertions only.
*/
export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
export const DatasetAssertionsList = ({
assertions,
onDelete,
showMenu = true,
showSelect,
onSelect,
selectedUrns,
contract,
}: Props) => {
const entityData = useEntityData();
const [deleteAssertionMutation] = useDeleteAssertionMutation();
const entityRegistry = useEntityRegistry();

const deleteAssertion = async (urn: string) => {
try {
Expand Down Expand Up @@ -102,9 +141,19 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
const resultColor = (record.lastExecResult && getResultColor(record.lastExecResult)) || 'default';
const resultText = (record.lastExecResult && getResultText(record.lastExecResult)) || 'No Evaluations';
const resultIcon = (record.lastExecResult && getResultIcon(record.lastExecResult)) || <StopOutlined />;
const selected = selectedUrns?.some((selectedUrn) => selectedUrn === record.urn);
const isPartOfContract = contract && isAssertionPartOfContract(record, contract);

const { description } = record;
return (
<ResultContainer>
{showSelect ? (
<AssertionSelectCheckbox
checked={selected}
onClick={(e) => e.stopPropagation()}
onChange={() => onSelect?.(record.urn as string)}
/>
) : undefined}
<div>
<Tooltip title={(localTime && `Last evaluated on ${localTime}`) || 'No Evaluations'}>
<Tag style={{ borderColor: resultColor }}>
Expand All @@ -117,6 +166,34 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
description={description}
assertionInfo={record.datasetAssertionInfo}
/>
{(isPartOfContract && entityData?.urn && (
<Tooltip
title={
<>
Part of Data Contract{' '}
<Link
to={`${entityRegistry.getEntityUrl(
EntityType.Dataset,
entityData.urn,
)}/Validation/Data Contract`}
style={{ color: REDESIGN_COLORS.BLUE }}
>
view
</Link>
</>
}
>
<Link
to={`${entityRegistry.getEntityUrl(
EntityType.Dataset,
entityData.urn,
)}/Validation/Data Contract`}
>
<DataContractLogo />
</Link>
</Tooltip>
)) ||
undefined}
</ResultContainer>
);
},
Expand All @@ -126,35 +203,40 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
dataIndex: '',
key: '',
render: (_, record: any) => (
<ActionButtonContainer>
<Tooltip
title={
record.platform.properties?.displayName || capitalizeFirstLetterOnly(record.platform.name)
}
>
<PlatformContainer>
{(record.platform.properties?.logoUrl && (
<Image
preview={false}
height={20}
width={20}
src={record.platform.properties?.logoUrl}
/>
)) || (
<Typography.Text>
{record.platform.properties?.displayName ||
capitalizeFirstLetterOnly(record.platform.name)}
</Typography.Text>
)}
</PlatformContainer>
</Tooltip>
<Button onClick={() => onDeleteAssertion(record.urn)} type="text" shape="circle" danger>
<DeleteOutlined />
</Button>
<Dropdown overlay={<AssertionMenu urn={record.urn} />} trigger={['click']}>
<StyledMoreOutlined />
</Dropdown>
</ActionButtonContainer>
<>
{showMenu && (
<ActionButtonContainer>
<Tooltip
title={
record.platform.properties?.displayName ||
capitalizeFirstLetterOnly(record.platform.name)
}
>
<PlatformContainer>
{(record.platform.properties?.logoUrl && (
<Image
preview={false}
height={20}
width={20}
src={record.platform.properties?.logoUrl}
/>
)) || (
<Typography.Text>
{record.platform.properties?.displayName ||
capitalizeFirstLetterOnly(record.platform.name)}
</Typography.Text>
)}
</PlatformContainer>
</Tooltip>
<Button onClick={() => onDeleteAssertion(record.urn)} type="text" shape="circle" danger>
<DeleteOutlined />
</Button>
<Dropdown overlay={<AssertionMenu urn={record.urn} />} trigger={['click']}>
<StyledMoreOutlined />
</Dropdown>
</ActionButtonContainer>
)}
</>
),
},
];
Expand All @@ -168,18 +250,36 @@ export const DatasetAssertionsList = ({ assertions, onDelete }: Props) => {
locale={{
emptyText: <Empty description="No Assertions Found :(" image={Empty.PRESENTED_IMAGE_SIMPLE} />,
}}
expandable={{
defaultExpandAllRows: false,
expandRowByClick: true,
expandedRowRender: (record) => {
return <DatasetAssertionDetails urn={record.urn} lastEvaluatedAtMillis={record.lastExecTime} />;
},
expandIcon: ({ expanded, onExpand, record }: any) =>
expanded ? (
<DownOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
) : (
<RightOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
),
expandable={
showSelect
? {}
: {
defaultExpandAllRows: false,
expandRowByClick: true,
expandedRowRender: (record) => {
return (
<DatasetAssertionDetails
urn={record.urn}
lastEvaluatedAtMillis={record.lastExecTime}
/>
);
},
expandIcon: ({ expanded, onExpand, record }: any) =>
expanded ? (
<DownOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
) : (
<RightOutlined style={{ fontSize: 8 }} onClick={(e) => onExpand(record, e)} />
),
}
}
onRow={(record) => {
return {
onClick: (_) => {
if (showSelect) {
onSelect?.(record.urn as string);
}
},
};
}}
showHeader={false}
pagination={false}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import React from 'react';
import { Typography } from 'antd';
import { FieldAssertionInfo } from '../../../../../../types.generated';
import {
getFieldDescription,
getFieldOperatorDescription,
getFieldParametersDescription,
getFieldTransformDescription,
} from './fieldDescriptionUtils';

type Props = {
assertionInfo: FieldAssertionInfo;
};

/**
* A human-readable description of a Field Assertion.
*/
export const FieldAssertionDescription = ({ assertionInfo }: Props) => {
const field = getFieldDescription(assertionInfo);
const operator = getFieldOperatorDescription(assertionInfo);
const transform = getFieldTransformDescription(assertionInfo);
const parameters = getFieldParametersDescription(assertionInfo);

return (
<Typography.Text>
{transform}
{transform ? ' of ' : ''}
<Typography.Text code>{field}</Typography.Text> {operator} {parameters}
</Typography.Text>
);
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from 'react';
import { Typography } from 'antd';
import { AssertionInfo } from '../../../../../../types.generated';

type Props = {
assertionInfo: AssertionInfo;
};

/**
* A human-readable description of a SQL Assertion.
*/
export const SqlAssertionDescription = ({ assertionInfo }: Props) => {
const { description } = assertionInfo;

return <Typography.Text>{description}</Typography.Text>;
};
Loading

0 comments on commit 1b56035

Please sign in to comment.