Data Contract UI Improvement (#22705)

* Data Contract UI Improvement

* fix the semantic card not visibel on expand and switch not working on outside

* fix the add new semantic not being disbaled on first edit

* added the status badge for latestRun in DataAssetHeader
This commit is contained in:
Ashish Gupta 2025-08-02 23:05:30 +05:30 committed by GitHub
parent 287c1b6138
commit 34cd7178e2
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
29 changed files with 367 additions and 109 deletions

View File

@ -0,0 +1,16 @@
<svg viewBox="0 0 28 28" fill="none" xmlns="http://www.w3.org/2000/svg">
<g opacity="0.3">
<path d="M13.999 5.19995C18.8591 5.19995 22.7987 9.13974 22.7988 13.9998C22.7988 18.8599 18.8591 22.7996 13.999 22.7996C9.13901 22.7995 5.19922 18.8598 5.19922 13.9998C5.19932 9.13981 9.13907 5.20006 13.999 5.19995Z" stroke="#D92D20" stroke-width="2"/>
</g>
<g opacity="0.1">
<path d="M13.999 1.69995C20.7921 1.69995 26.2987 7.20675 26.2988 13.9998C26.2988 20.7929 20.7921 26.2996 13.999 26.2996C7.20601 26.2995 1.69922 20.7928 1.69922 13.9998C1.69932 7.20681 7.20608 1.70006 13.999 1.69995Z" stroke="#D92D20" stroke-width="2"/>
</g>
<g clip-path="url(#clip0_542_17892)">
<path d="M15.7513 12.2501L12.2513 15.7501M12.2513 12.2501L15.7513 15.7501M19.8346 14.0001C19.8346 17.2217 17.223 19.8334 14.0013 19.8334C10.7796 19.8334 8.16797 17.2217 8.16797 14.0001C8.16797 10.7784 10.7796 8.16675 14.0013 8.16675C17.223 8.16675 19.8346 10.7784 19.8346 14.0001Z" stroke="#D92D20" stroke-width="1.66667" stroke-linecap="round" stroke-linejoin="round"/>
</g>
<defs>
<clipPath id="clip0_542_17892">
<rect width="14" height="14" fill="white" transform="translate(7 7)"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -11,7 +11,16 @@
* limitations under the License. * limitations under the License.
*/ */
import Icon from '@ant-design/icons'; import Icon from '@ant-design/icons';
import { Button, Col, Divider, Row, Space, Tooltip, Typography } from 'antd'; import {
Button,
Col,
Divider,
Row,
Space,
Tag,
Tooltip,
Typography,
} from 'antd';
import ButtonGroup from 'antd/lib/button/button-group'; import ButtonGroup from 'antd/lib/button/button-group';
import { AxiosError } from 'axios'; import { AxiosError } from 'axios';
import classNames from 'classnames'; import classNames from 'classnames';
@ -46,6 +55,7 @@ import {
import { ServiceCategory } from '../../../enums/service.enum'; import { ServiceCategory } from '../../../enums/service.enum';
import { LineageLayer } from '../../../generated/configuration/lineageSettings'; import { LineageLayer } from '../../../generated/configuration/lineageSettings';
import { Container } from '../../../generated/entity/data/container'; import { Container } from '../../../generated/entity/data/container';
import { ContractExecutionStatus } from '../../../generated/entity/data/dataContract';
import { Table } from '../../../generated/entity/data/table'; import { Table } from '../../../generated/entity/data/table';
import { Thread } from '../../../generated/entity/feed/thread'; import { Thread } from '../../../generated/entity/feed/thread';
import { useApplicationStore } from '../../../hooks/useApplicationStore'; import { useApplicationStore } from '../../../hooks/useApplicationStore';
@ -62,6 +72,7 @@ import {
import EntityLink from '../../../utils/EntityLink'; import EntityLink from '../../../utils/EntityLink';
import entityUtilClassBase from '../../../utils/EntityUtilClassBase'; import entityUtilClassBase from '../../../utils/EntityUtilClassBase';
import { import {
getDataContractStatusIcon,
getEntityFeedLink, getEntityFeedLink,
getEntityName, getEntityName,
getEntityVoteStatus, getEntityVoteStatus,
@ -100,6 +111,7 @@ export const DataAssetsHeader = ({
showDomain = true, showDomain = true,
afterDeleteAction, afterDeleteAction,
dataAsset, dataAsset,
dataContract,
onUpdateVote, onUpdateVote,
onOwnerUpdate, onOwnerUpdate,
onTierUpdate, onTierUpdate,
@ -421,6 +433,25 @@ export const DataAssetsHeader = ({
selectedUserSuggestions, selectedUserSuggestions,
]); ]);
const dataContractLatestResultButton = useMemo(() => {
if (dataContract?.latestResult?.status === ContractExecutionStatus.Failed) {
return (
<Tag
className={classNames(
`data-contract-latest-result-button
${dataContract?.latestResult?.status}`
)}>
{getDataContractStatusIcon(dataContract?.latestResult?.status)}
{t('label.entity-failed', {
entity: t('label.contract'),
})}
</Tag>
);
}
return null;
}, [dataContract]);
const triggerTheAutoPilotApplication = useCallback(async () => { const triggerTheAutoPilotApplication = useCallback(async () => {
try { try {
setIsAutoPilotTriggering(true); setIsAutoPilotTriggering(true);
@ -517,6 +548,8 @@ export const DataAssetsHeader = ({
data-testid="asset-header-btn-group" data-testid="asset-header-btn-group"
size="small"> size="small">
{triggerAutoPilotApplicationButton} {triggerAutoPilotApplicationButton}
{dataContractLatestResultButton}
{onUpdateVote && ( {onUpdateVote && (
<Voting <Voting
disabled={deleted} disabled={deleted}

View File

@ -22,6 +22,7 @@ import { Dashboard } from '../../../generated/entity/data/dashboard';
import { DashboardDataModel } from '../../../generated/entity/data/dashboardDataModel'; import { DashboardDataModel } from '../../../generated/entity/data/dashboardDataModel';
import { Database } from '../../../generated/entity/data/database'; import { Database } from '../../../generated/entity/data/database';
import { DatabaseSchema } from '../../../generated/entity/data/databaseSchema'; import { DatabaseSchema } from '../../../generated/entity/data/databaseSchema';
import { DataContract } from '../../../generated/entity/data/dataContract';
import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm'; import { GlossaryTerm } from '../../../generated/entity/data/glossaryTerm';
import { Metric } from '../../../generated/entity/data/metric'; import { Metric } from '../../../generated/entity/data/metric';
import { Mlmodel } from '../../../generated/entity/data/mlmodel'; import { Mlmodel } from '../../../generated/entity/data/mlmodel';
@ -115,6 +116,7 @@ export type DataAssetWithDomains =
| GlossaryTerm; | GlossaryTerm;
export type DataAssetsHeaderProps = { export type DataAssetsHeaderProps = {
dataContract?: DataContract;
permissions: OperationPermission; permissions: OperationPermission;
openTaskCount?: number; openTaskCount?: number;
allowSoftDelete?: boolean; allowSoftDelete?: boolean;

View File

@ -128,4 +128,26 @@
fill: @de-active-color; fill: @de-active-color;
} }
} }
.data-contract-latest-result-button {
font-size: 14px;
&.Failed {
color: @red-14;
font-weight: 600;
border: 1px solid @red-19;
background-color: @red-9;
border-radius: 12px;
padding: 6px 12px;
display: flex;
align-items: center;
gap: 4px;
box-shadow: 0px 2px 2px -1px @grey-35, 0px 4px 6px -2px @grey-35,
0px 12px 16px -4px @grey-35;
svg {
font-size: 26px;
fill: transparent;
}
}
}
} }

View File

@ -18,8 +18,10 @@ import React, { useEffect, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg'; import { ReactComponent as EditIcon } from '../../../assets/svg/edit-new.svg';
import { ReactComponent as EmptyContractIcon } from '../../../assets/svg/empty-contract.svg'; import { ReactComponent as EmptyContractIcon } from '../../../assets/svg/empty-contract.svg';
import { ReactComponent as FailIcon } from '../../../assets/svg/fail-badge.svg';
import { ReactComponent as FlagIcon } from '../../../assets/svg/flag.svg'; import { ReactComponent as FlagIcon } from '../../../assets/svg/flag.svg';
import { ReactComponent as CheckIcon } from '../../../assets/svg/ic-check-circle.svg'; import { ReactComponent as CheckIcon } from '../../../assets/svg/ic-check-circle.svg';
import { ReactComponent as DefaultIcon } from '../../../assets/svg/ic-task.svg';
import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg'; import { ReactComponent as DeleteIcon } from '../../../assets/svg/ic-trash.svg';
import { isEmpty } from 'lodash'; import { isEmpty } from 'lodash';
@ -52,6 +54,7 @@ import { getRelativeTime } from '../../../utils/date-time/DateTimeUtils';
import { getEntityName } from '../../../utils/EntityUtils'; import { getEntityName } from '../../../utils/EntityUtils';
import { pruneEmptyChildren } from '../../../utils/TableUtils'; import { pruneEmptyChildren } from '../../../utils/TableUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils'; import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import AlertBar from '../../AlertBar/AlertBar';
import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1';
import ErrorPlaceHolderNew from '../../common/ErrorWithPlaceholder/ErrorPlaceHolderNew'; import ErrorPlaceHolderNew from '../../common/ErrorWithPlaceholder/ErrorPlaceHolderNew';
import ExpandableCard from '../../common/ExpandableCard/ExpandableCard'; import ExpandableCard from '../../common/ExpandableCard/ExpandableCard';
@ -171,6 +174,22 @@ const ContractDetail: React.FC<{
return getTestCaseSummaryChartItems(testCaseSummary); return getTestCaseSummaryChartItems(testCaseSummary);
}, [testCaseSummary]); }, [testCaseSummary]);
const getSemanticIconPerLastExecution = (semanticName: string) => {
if (!latestContractResults) {
return DefaultIcon;
}
const isRuleFailed =
latestContractResults?.semanticsValidation?.failedRules?.find(
(rule) => rule.ruleName === semanticName
);
if (isRuleFailed) {
return FailIcon;
}
return CheckIcon;
};
const getTestCaseStatusIcon = (record: TestCase) => ( const getTestCaseStatusIcon = (record: TestCase) => (
<Icon <Icon
className="test-status-icon" className="test-status-icon"
@ -389,38 +408,49 @@ const ContractDetail: React.FC<{
{isLoading ? ( {isLoading ? (
<Loading /> <Loading />
) : ( ) : (
constraintStatus.map((item) => ( <>
<div {latestContractResults?.result && (
className="contract-status-card-item d-flex justify-between items-center" <AlertBar
key={item.label}> defafultExpand
<div className="d-flex items-center"> className="h-full m-b-md"
<Icon message={latestContractResults.result}
className="contract-status-card-icon" type="error"
component={item.icon} />
data-testid={`${item.label}-icon`} )}
/>
<div className="d-flex flex-column m-l-md"> {constraintStatus.map((item) => (
<Typography.Text className="contract-status-card-label"> <div
{item.label} className="contract-status-card-item d-flex justify-between items-center"
</Typography.Text> key={item.label}>
<div> <div className="d-flex items-center">
<Typography.Text className="contract-status-card-desc"> <Icon
{item.desc} className="contract-status-card-icon"
</Typography.Text> component={item.icon}
<Typography.Text className="contract-status-card-time"> data-testid={`${item.label}-icon`}
{item.time} />
<div className="d-flex flex-column m-l-md">
<Typography.Text className="contract-status-card-label">
{item.label}
</Typography.Text> </Typography.Text>
<div>
<Typography.Text className="contract-status-card-desc">
{item.desc}
</Typography.Text>
<Typography.Text className="contract-status-card-time">
{item.time}
</Typography.Text>
</div>
</div> </div>
</div> </div>
</div>
<StatusBadgeV2 <StatusBadgeV2
label={item.status} label={item.status}
status={getContractStatusType(item.status)} status={getContractStatusType(item.status)}
/> />
</div> </div>
)) ))}
</>
)} )}
</ExpandableCard> </ExpandableCard>
</Col> </Col>
@ -450,7 +480,12 @@ const ContractDetail: React.FC<{
<div className="rule-item-container"> <div className="rule-item-container">
{(contract?.semantics ?? []).map((item) => ( {(contract?.semantics ?? []).map((item) => (
<div className="rule-item"> <div className="rule-item">
<Icon className="rule-icon" component={CheckIcon} /> <Icon
className="rule-icon"
component={getSemanticIconPerLastExecution(
item.name
)}
/>
<span className="rule-name">{item.name}</span>{' '} <span className="rule-name">{item.name}</span>{' '}
<span className="rule-description"> <span className="rule-description">
{item.description} {item.description}

View File

@ -12,12 +12,14 @@
*/ */
import Icon, { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons'; import Icon, { ArrowLeftOutlined, ArrowRightOutlined } from '@ant-design/icons';
import { Actions } from '@react-awesome-query-builder/antd';
import { FieldErrorProps } from '@rjsf/utils'; import { FieldErrorProps } from '@rjsf/utils';
import { Button, Col, Form, Input, Row, Switch, Typography } from 'antd'; import { Button, Col, Form, Input, Row, Switch, Typography } from 'antd';
import Card from 'antd/lib/card/Card'; import Card from 'antd/lib/card/Card';
import TextArea from 'antd/lib/input/TextArea'; import TextArea from 'antd/lib/input/TextArea';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect, useRef, useState } from 'react'; import { isNull } from 'lodash';
import { useCallback, useEffect, useRef, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { ReactComponent as PlusIcon } from '../../../assets/svg/x-colored.svg'; import { ReactComponent as PlusIcon } from '../../../assets/svg/x-colored.svg';
import { EntityType } from '../../../enums/entity.enum'; import { EntityType } from '../../../enums/entity.enum';
@ -40,25 +42,34 @@ export const ContractSemanticFormTab: React.FC<{
const [form] = Form.useForm(); const [form] = Form.useForm();
const semanticsData = Form.useWatch('semantics', form); const semanticsData = Form.useWatch('semantics', form);
const [editingKey, setEditingKey] = useState<number | null>(null); const [editingKey, setEditingKey] = useState<number | null>(null);
const [queryBuilderAddRule, setQueryBuilderAddRule] = useState<Actions>();
const addFunctionRef = useRef<((defaultValue?: any) => void) | null>(null); const addFunctionRef = useRef<((defaultValue?: any) => void) | null>(null);
const handleAddQueryBuilderRule = (actionFunctions: Actions) => {
setQueryBuilderAddRule(actionFunctions);
};
const handleAddSemantic = () => { const handleAddSemantic = () => {
addFunctionRef.current?.({ addFunctionRef.current?.({
name: '', name: '',
description: '', description: '',
rule: '', rule: '',
enabled: false, enabled: true,
}); });
setEditingKey(semanticsData.length); setEditingKey(semanticsData.length);
}; };
const handleAddNewRule = useCallback(() => {
queryBuilderAddRule?.addRule([]);
}, [queryBuilderAddRule]);
useEffect(() => { useEffect(() => {
form.setFieldsValue({ form.setFieldsValue({
semantics: [ semantics: [
{ {
name: '', name: '',
description: '', description: '',
enabled: false, enabled: true,
rule: '', rule: '',
}, },
], ],
@ -88,7 +99,7 @@ export const ContractSemanticFormTab: React.FC<{
<Button <Button
className="add-semantic-button" className="add-semantic-button"
disabled={!!editingKey || !addFunctionRef.current} disabled={!isNull(editingKey) || !addFunctionRef.current}
icon={<Icon className="anticon" component={PlusIcon} />} icon={<Icon className="anticon" component={PlusIcon} />}
type="link" type="link"
onClick={handleAddSemantic}> onClick={handleAddSemantic}>
@ -125,9 +136,13 @@ export const ContractSemanticFormTab: React.FC<{
{editingKey === field.key ? null : ( {editingKey === field.key ? null : (
<> <>
<div className="d-flex items-center gap-6"> <div className="d-flex items-center gap-6">
<Switch <Form.Item
checked={semanticsData[field.key].enabled} {...field}
/> name={[field.name, 'enabled']}
valuePropName="checked">
<Switch />
</Form.Item>
<div className="d-flex flex-column"> <div className="d-flex flex-column">
<Typography.Text> <Typography.Text>
{semanticsData[field.key]?.name || {semanticsData[field.key]?.name ||
@ -151,74 +166,91 @@ export const ContractSemanticFormTab: React.FC<{
</div> </div>
), ),
}} }}
defaultExpanded={editingKey === field.key}
key={field.key}> key={field.key}>
{editingKey === field.key ? ( {editingKey === field.key ? (
<Row> <>
<Col span={24}> <Row className="semantic-form-item-content">
<Form.Item <Col span={24}>
{...field} <Form.Item
label={t('label.name')} {...field}
name={[field.name, 'name']}> label={t('label.name')}
<Input /> name={[field.name, 'name']}>
</Form.Item> <Input />
</Col> </Form.Item>
<Col span={24}> </Col>
<Form.Item <Col span={24}>
{...field} <Form.Item
label={t('label.description')} {...field}
name={[field.name, 'description']}> label={t('label.description')}
<TextArea /> name={[field.name, 'description']}>
</Form.Item> <TextArea />
</Col> </Form.Item>
<Col span={24}> </Col>
<Form.Item <Col span={24}>
{...field} <Form.Item
label={t('label.enabled')} {...field}
name={[field.name, 'enabled']}> label={t('label.enabled')}
<Switch /> name={[field.name, 'enabled']}
</Form.Item> valuePropName="checked">
</Col> <Switch />
<Col span={24}> </Form.Item>
<Form.Item </Col>
{...field} <Col span={24}>
label={t('label.add-entity', { <Form.Item
{...field}
label={t('label.add-entity', {
entity: t('label.rule-plural'),
})}
name={[field.name, 'rule']}>
{/* @ts-expect-error because Form.Item will provide value and onChange */}
<QueryBuilderWidget
formContext={{
entityType: EntityType.TABLE,
}}
getQueryActions={handleAddQueryBuilderRule}
id="rule"
name={`${field.name}.rule`}
options={{
addButtonText: t('label.add-semantic'),
removeButtonText: t(
'label.remove-semantic'
),
}}
registry={{} as FieldErrorProps['registry']}
schema={{
outputType: SearchOutputType.JSONLogic,
}}
/>
</Form.Item>
</Col>
</Row>
<div className="semantic-form-item-actions">
<Button
className="add-semantic-button"
disabled={!queryBuilderAddRule?.addRule}
icon={<Icon component={PlusIcon} />}
type="link"
onClick={handleAddNewRule}>
{t('label.add-new-entity', {
entity: t('label.rule'), entity: t('label.rule'),
})} })}
name={[field.name, 'rule']}> </Button>
{/* @ts-expect-error because Form.Item will provide value and onChange */}
<QueryBuilderWidget
formContext={{
entityType: EntityType.TABLE,
}}
id="rule"
label={t('label.rule')}
name={`${field.name}.rule`}
options={{
addButtonText: t('label.add-semantic'),
removeButtonText: t(
'label.remove-semantic'
),
}}
registry={{} as FieldErrorProps['registry']}
schema={{
outputType: SearchOutputType.JSONLogic,
}}
/>
</Form.Item>
</Col>
<Col className="d-flex justify-end" span={24}> <div className="d-flex items-center">
<Button onClick={() => setEditingKey(null)}> <Button onClick={() => setEditingKey(null)}>
{t('label.cancel')} {t('label.cancel')}
</Button> </Button>
<Button <Button
className="m-l-md" className="m-l-md"
type="primary" type="primary"
onClick={() => setEditingKey(null)}> onClick={() => setEditingKey(null)}>
{t('label.save')} {t('label.save')}
</Button> </Button>
</Col> </div>
</Row> </div>
</>
) : ( ) : (
<div className="semantic-rule-editor-view-only"> <div className="semantic-rule-editor-view-only">
{/* @ts-expect-error because Form.Item will provide value and onChange */} {/* @ts-expect-error because Form.Item will provide value and onChange */}

View File

@ -24,8 +24,17 @@
} }
.contract-semantic-form-container { .contract-semantic-form-container {
.expanded {
.ant-card-head {
border-bottom-left-radius: 0 !important;
border-bottom-right-radius: 0 !important;
}
}
.expandable-card { .expandable-card {
margin-top: 16px; margin-top: 16px;
border: 1px solid @border-color-7 !important;
box-shadow: 0 1px 2px 0 @grey-27;
.ant-card-head { .ant-card-head {
background: @white !important; background: @white !important;
@ -37,6 +46,48 @@
.ant-card-head { .ant-card-head {
display: none; display: none;
} }
.ant-card-body {
padding: 0;
.semantic-form-item-content {
padding: 20px;
}
}
}
.semantic-form-item-actions {
display: flex;
justify-content: space-between;
padding: 16px 24px;
border-top: 1px solid @grey-200;
}
.query-builder-form-field {
.ant-card {
border: none;
}
.ant-card-body {
padding: 0;
.ant-divider {
display: none;
}
.ant-btn-group {
.action--DELETE {
border: 1px solid @grey-34;
.anticon {
color: @grey-400;
}
}
.action--ADD-RULE {
display: none !important;
}
}
}
} }
} }

View File

@ -12,12 +12,13 @@
*/ */
import { Card, CardProps } from 'antd'; import { Card, CardProps } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import React, { useCallback, useState } from 'react'; import React, { useCallback, useEffect, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { CardExpandCollapseIconButton } from '../IconButtons/EditIconButton'; import { CardExpandCollapseIconButton } from '../IconButtons/EditIconButton';
interface ExpandableCardProps { interface ExpandableCardProps {
children: React.ReactNode; children: React.ReactNode;
defaultExpanded?: boolean;
onExpandStateChange?: (isExpanded: boolean) => void; onExpandStateChange?: (isExpanded: boolean) => void;
isExpandDisabled?: boolean; isExpandDisabled?: boolean;
cardProps: CardProps; cardProps: CardProps;
@ -30,9 +31,10 @@ const ExpandableCard = ({
onExpandStateChange, onExpandStateChange,
isExpandDisabled, isExpandDisabled,
dataTestId, dataTestId,
defaultExpanded = true,
}: ExpandableCardProps) => { }: ExpandableCardProps) => {
const { t } = useTranslation(); const { t } = useTranslation();
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(defaultExpanded);
const handleExpandClick = useCallback(() => { const handleExpandClick = useCallback(() => {
setIsExpanded((prev) => { setIsExpanded((prev) => {
@ -42,6 +44,10 @@ const ExpandableCard = ({
}); });
}, [onExpandStateChange]); }, [onExpandStateChange]);
useEffect(() => {
setIsExpanded(defaultExpanded);
}, [defaultExpanded]);
return ( return (
<Card <Card
bodyStyle={{ bodyStyle={{

View File

@ -23,8 +23,10 @@ import {
Typography, Typography,
} from 'antd'; } from 'antd';
import classNames from 'classnames'; import classNames from 'classnames';
import { useEffect } from 'react';
import { import {
Actions,
Builder, Builder,
Config, Config,
ImmutableTree, ImmutableTree,
@ -34,7 +36,7 @@ import {
import 'antd/dist/antd.css'; import 'antd/dist/antd.css';
import { debounce, isEmpty, isUndefined } from 'lodash'; import { debounce, isEmpty, isUndefined } from 'lodash';
import Qs from 'qs'; import Qs from 'qs';
import { FC, useCallback, useEffect, useMemo, useState } from 'react'; import { FC, useCallback, useMemo, useState } from 'react';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { import {
EntityFields, EntityFields,
@ -84,6 +86,7 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
const [initDone, setInitDone] = useState<boolean>(false); const [initDone, setInitDone] = useState<boolean>(false);
const { t } = useTranslation(); const { t } = useTranslation();
const [queryURL, setQueryURL] = useState<string>(''); const [queryURL, setQueryURL] = useState<string>('');
const [queryActions, setQueryActions] = useState<Actions>();
const fetchEntityCount = useCallback( const fetchEntityCount = useCallback(
async (queryFilter: Record<string, unknown>) => { async (queryFilter: Record<string, unknown>) => {
@ -224,6 +227,12 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
} }
}, [isSearchIndexUpdatedInContext, isUpdating]); }, [isSearchIndexUpdatedInContext, isUpdating]);
useEffect(() => {
if (props.getQueryActions) {
props.getQueryActions(queryActions);
}
}, [queryActions]);
if (!initDone) { if (!initDone) {
return <></>; return <></>;
} }
@ -249,11 +258,18 @@ const QueryBuilderWidget: FC<WidgetProps> = ({
)} )}
<Query <Query
{...config} {...config}
renderBuilder={(props) => ( renderBuilder={(props) => {
<div className="query-builder-container query-builder qb-lite"> // Store the actions for external access
<Builder {...props} /> if (!queryActions) {
</div> setQueryActions(props.actions);
)} }
return (
<div className="query-builder-container query-builder qb-lite">
<Builder {...props} />
</div>
);
}}
settings={{ settings={{
...config.settings, ...config.settings,
...(props.readonly ? READONLY_SETTINGS : {}), ...(props.readonly ? READONLY_SETTINGS : {}),

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Abdeckung", "entity-coverage": "{{entity}} Abdeckung",
"entity-detail-plural": "Details von {{entity}}", "entity-detail-plural": "Details von {{entity}}",
"entity-distribution": "{{entity}} Verteilung", "entity-distribution": "{{entity}} Verteilung",
"entity-failed": "{{entity}} fehlgeschlagen",
"entity-feed-plural": "Entitäts-Feeds", "entity-feed-plural": "Entitäts-Feeds",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Coverage", "entity-coverage": "{{entity}} Coverage",
"entity-detail-plural": "{{entity}} Details", "entity-detail-plural": "{{entity}} Details",
"entity-distribution": "{{entity}} Distribution", "entity-distribution": "{{entity}} Distribution",
"entity-failed": "{{entity}} Failed",
"entity-feed-plural": "Entity feeds", "entity-feed-plural": "Entity feeds",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Abdeckung", "entity-coverage": "{{entity}} Abdeckung",
"entity-detail-plural": "Detalles de {{entity}}", "entity-detail-plural": "Detalles de {{entity}}",
"entity-distribution": "Distribución de {{entity}}", "entity-distribution": "Distribución de {{entity}}",
"entity-failed": "{{entity}} Falló",
"entity-feed-plural": "Feeds de entidad", "entity-feed-plural": "Feeds de entidad",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Couverture", "entity-coverage": "{{entity}} Couverture",
"entity-detail-plural": "Détails de {{entity}}", "entity-detail-plural": "Détails de {{entity}}",
"entity-distribution": "Distribution de {{entity}}", "entity-distribution": "Distribution de {{entity}}",
"entity-failed": "{{entity}} Échoué",
"entity-feed-plural": "Flux de l'Entité", "entity-feed-plural": "Flux de l'Entité",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Cobertura", "entity-coverage": "{{entity}} Cobertura",
"entity-detail-plural": "Detalles de {{entity}}", "entity-detail-plural": "Detalles de {{entity}}",
"entity-distribution": "Distribución de {{entity}}", "entity-distribution": "Distribución de {{entity}}",
"entity-failed": "{{entity}} Fallado",
"entity-feed-plural": "Fontes de entidade", "entity-feed-plural": "Fontes de entidade",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "ID de {{entity}}", "entity-id": "ID de {{entity}}",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} כיסוי", "entity-coverage": "{{entity}} כיסוי",
"entity-detail-plural": "פרטי {{entity}}", "entity-detail-plural": "פרטי {{entity}}",
"entity-distribution": "הפצת {{entity}}", "entity-distribution": "הפצת {{entity}}",
"entity-failed": "{{entity}} נכשל",
"entity-feed-plural": "הזנות ישויות", "entity-feed-plural": "הזנות ישויות",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} カバレッジ", "entity-coverage": "{{entity}} カバレッジ",
"entity-detail-plural": "{{entity}}の詳細", "entity-detail-plural": "{{entity}}の詳細",
"entity-distribution": "{{entity}} の分布", "entity-distribution": "{{entity}} の分布",
"entity-failed": "{{entity}} 失敗",
"entity-feed-plural": "エンティティフィード", "entity-feed-plural": "エンティティフィード",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} ID", "entity-id": "{{entity}} ID",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} 커버리지", "entity-coverage": "{{entity}} 커버리지",
"entity-detail-plural": "{{entity}} 세부사항", "entity-detail-plural": "{{entity}} 세부사항",
"entity-distribution": "{{entity}} 분포", "entity-distribution": "{{entity}} 분포",
"entity-failed": "{{entity}} 실패",
"entity-feed-plural": "엔티티 피드", "entity-feed-plural": "엔티티 피드",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} ID", "entity-id": "{{entity}} ID",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} कवर", "entity-coverage": "{{entity}} कवर",
"entity-detail-plural": "{{entity}} तपशील", "entity-detail-plural": "{{entity}} तपशील",
"entity-distribution": "{{entity}} वितरण", "entity-distribution": "{{entity}} वितरण",
"entity-failed": "{{entity}} अयशस्वी",
"entity-feed-plural": "घटक फीड्स", "entity-feed-plural": "घटक फीड्स",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} आयडी", "entity-id": "{{entity}} आयडी",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Dekkingsg", "entity-coverage": "{{entity}} Dekkingsg",
"entity-detail-plural": "{{entity}}-details", "entity-detail-plural": "{{entity}}-details",
"entity-distribution": "{{entity}} Verdeling", "entity-distribution": "{{entity}} Verdeling",
"entity-failed": "{{entity}} Mislukt",
"entity-feed-plural": "Entiteitsfeeds", "entity-feed-plural": "Entiteitsfeeds",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} پوشش", "entity-coverage": "{{entity}} پوشش",
"entity-detail-plural": "جزئیات {{entity}}", "entity-detail-plural": "جزئیات {{entity}}",
"entity-distribution": "توزیع {{entity}}", "entity-distribution": "توزیع {{entity}}",
"entity-failed": "{{entity}} ناموفق",
"entity-feed-plural": "فیدهای نهاد", "entity-feed-plural": "فیدهای نهاد",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "شناسه {{entity}}", "entity-id": "شناسه {{entity}}",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Cobertura", "entity-coverage": "{{entity}} Cobertura",
"entity-detail-plural": "Detalhes de {{entity}}", "entity-detail-plural": "Detalhes de {{entity}}",
"entity-distribution": "Distribuição de {{entity}}", "entity-distribution": "Distribuição de {{entity}}",
"entity-failed": "{{entity}} Falhou",
"entity-feed-plural": "Feeds de Entidade", "entity-feed-plural": "Feeds de Entidade",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Cobertura", "entity-coverage": "{{entity}} Cobertura",
"entity-detail-plural": "Detalhes de {{entity}}", "entity-detail-plural": "Detalhes de {{entity}}",
"entity-distribution": "Distribuição de {{entity}}", "entity-distribution": "Distribuição de {{entity}}",
"entity-failed": "{{entity}} Falhou",
"entity-feed-plural": "Feeds de Entidade", "entity-feed-plural": "Feeds de Entidade",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Id", "entity-id": "{{entity}} Id",

View File

@ -562,6 +562,7 @@
"entity-coverage": "Покрытие {{entity}}", "entity-coverage": "Покрытие {{entity}}",
"entity-detail-plural": "Детали {{entity}}", "entity-detail-plural": "Детали {{entity}}",
"entity-distribution": "Распределение {{entity}}", "entity-distribution": "Распределение {{entity}}",
"entity-failed": "{{entity}} Неудачно",
"entity-feed-plural": "Фиды сущностец", "entity-feed-plural": "Фиды сущностец",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "Идентификатор {{entity}}", "entity-id": "Идентификатор {{entity}}",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} ความครอบคลุม", "entity-coverage": "{{entity}} ความครอบคลุม",
"entity-detail-plural": "รายละเอียด {{entity}}", "entity-detail-plural": "รายละเอียด {{entity}}",
"entity-distribution": "{{entity}} การแจกแจง", "entity-distribution": "{{entity}} การแจกแจง",
"entity-failed": "{{entity}} ล้มเหลว",
"entity-feed-plural": "ฟีดเอนทิตี", "entity-feed-plural": "ฟีดเอนทิตี",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "รหัส {{entity}}", "entity-id": "รหัส {{entity}}",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} Kapsamı", "entity-coverage": "{{entity}} Kapsamı",
"entity-detail-plural": "{{entity}} Detayları", "entity-detail-plural": "{{entity}} Detayları",
"entity-distribution": "{{entity}} Dağılımı", "entity-distribution": "{{entity}} Dağılımı",
"entity-failed": "{{entity}} Başarısız",
"entity-feed-plural": "Varlık akışları", "entity-feed-plural": "Varlık akışları",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} Kimliği", "entity-id": "{{entity}} Kimliği",

View File

@ -562,6 +562,7 @@
"entity-coverage": "{{entity}} 覆盖", "entity-coverage": "{{entity}} 覆盖",
"entity-detail-plural": "{{entity}}详情", "entity-detail-plural": "{{entity}}详情",
"entity-distribution": "{{entity}} 分布", "entity-distribution": "{{entity}} 分布",
"entity-failed": "{{entity}} 失败",
"entity-feed-plural": "实体信息流", "entity-feed-plural": "实体信息流",
"entity-hyphen-value": "{{entity}} - {{value}}", "entity-hyphen-value": "{{entity}} - {{value}}",
"entity-id": "{{entity}} ID", "entity-id": "{{entity}} ID",

View File

@ -51,6 +51,7 @@ import {
TabSpecificField, TabSpecificField,
} from '../../enums/entity.enum'; } from '../../enums/entity.enum';
import { Tag } from '../../generated/entity/classification/tag'; import { Tag } from '../../generated/entity/classification/tag';
import { DataContract } from '../../generated/entity/data/dataContract';
import { Table, TableType } from '../../generated/entity/data/table'; import { Table, TableType } from '../../generated/entity/data/table';
import { import {
Suggestion, Suggestion,
@ -66,6 +67,7 @@ import { useCustomPages } from '../../hooks/useCustomPages';
import { useFqn } from '../../hooks/useFqn'; import { useFqn } from '../../hooks/useFqn';
import { useSub } from '../../hooks/usePubSub'; import { useSub } from '../../hooks/usePubSub';
import { FeedCounts } from '../../interface/feed.interface'; import { FeedCounts } from '../../interface/feed.interface';
import { getContractByEntityId } from '../../rest/contractAPI';
import { getDataQualityLineage } from '../../rest/lineageAPI'; import { getDataQualityLineage } from '../../rest/lineageAPI';
import { getQueriesList } from '../../rest/queryAPI'; import { getQueriesList } from '../../rest/queryAPI';
import { import {
@ -135,6 +137,7 @@ const TableDetailsPageV1: React.FC = () => {
const [dqFailureCount, setDqFailureCount] = useState(0); const [dqFailureCount, setDqFailureCount] = useState(0);
const { customizedPage, isLoading } = useCustomPages(PageType.Table); const { customizedPage, isLoading } = useCustomPages(PageType.Table);
const [isTabExpanded, setIsTabExpanded] = useState(false); const [isTabExpanded, setIsTabExpanded] = useState(false);
const [dataContract, setDataContract] = useState<DataContract>();
const tableFqn = useMemo( const tableFqn = useMemo(
() => () =>
@ -212,7 +215,6 @@ const TableDetailsPageV1: React.FC = () => {
} }
const details = await getTableDetailsByFQN(tableFqn, { fields }); const details = await getTableDetailsByFQN(tableFqn, { fields });
setTableDetails(details); setTableDetails(details);
addToRecentViewed({ addToRecentViewed({
displayName: getEntityName(details), displayName: getEntityName(details),
@ -297,6 +299,15 @@ const TableDetailsPageV1: React.FC = () => {
} }
}; };
const fetchDataContract = async (tableId: string) => {
try {
const contract = await getContractByEntityId(tableId, EntityType.TABLE);
setDataContract(contract);
} catch {
// Do nothing
}
};
const { const {
tableTags, tableTags,
deleted, deleted,
@ -780,6 +791,12 @@ const TableDetailsPageV1: React.FC = () => {
} }
}, [tableDetails?.fullyQualifiedName]); }, [tableDetails?.fullyQualifiedName]);
useEffect(() => {
if (tableDetails) {
fetchDataContract(tableDetails.id);
}
}, [tableDetails?.id]);
useSub( useSub(
'updateDetails', 'updateDetails',
(suggestion: Suggestion) => { (suggestion: Suggestion) => {
@ -847,6 +864,7 @@ const TableDetailsPageV1: React.FC = () => {
afterDomainUpdateAction={updateTableDetailsState} afterDomainUpdateAction={updateTableDetailsState}
badge={alertBadge} badge={alertBadge}
dataAsset={tableDetails} dataAsset={tableDetails}
dataContract={dataContract}
entityType={EntityType.TABLE} entityType={EntityType.TABLE}
extraDropdownContent={extraDropdownContent} extraDropdownContent={extraDropdownContent}
openTaskCount={feedCount.openTaskCount} openTaskCount={feedCount.openTaskCount}

View File

@ -161,6 +161,8 @@
@grey-31: #f1f3fc; @grey-31: #f1f3fc;
@grey-32: #6b7f99; @grey-32: #6b7f99;
@grey-33: #4c526c; @grey-33: #4c526c;
@grey-34: #d3d3d3;
@grey-35: #0a0d120a;
@text-grey-muted: @grey-4; @text-grey-muted: @grey-4;
@de-active-color: #6b7280; @de-active-color: #6b7280;

View File

@ -11,6 +11,7 @@
* limitations under the License. * limitations under the License.
*/ */
import Icon from '@ant-design/icons';
import { Popover, Space, Typography } from 'antd'; import { Popover, Space, Typography } from 'antd';
import i18next, { t } from 'i18next'; import i18next, { t } from 'i18next';
import { import {
@ -26,6 +27,7 @@ import QueryString from 'qs';
import { Fragment } from 'react'; import { Fragment } from 'react';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import { Node } from 'reactflow'; import { Node } from 'reactflow';
import { ReactComponent as CancelOutlineIcon } from '../assets/svg/ic-cancel-outline.svg';
import { DomainLabel } from '../components/common/DomainLabel/DomainLabel.component'; import { DomainLabel } from '../components/common/DomainLabel/DomainLabel.component';
import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component'; import { OwnerLabel } from '../components/common/OwnerLabel/OwnerLabel.component';
import QueryCount from '../components/common/QueryCount/QueryCount.component'; import QueryCount from '../components/common/QueryCount/QueryCount.component';
@ -102,6 +104,7 @@ import {
EventSubscription, EventSubscription,
} from '../generated/events/eventSubscription'; } from '../generated/events/eventSubscription';
import { TestCase, TestSuite } from '../generated/tests/testCase'; import { TestCase, TestSuite } from '../generated/tests/testCase';
import { ContractExecutionStatus } from '../generated/type/contractExecutionStatus';
import { EntityReference } from '../generated/type/entityUsage'; import { EntityReference } from '../generated/type/entityUsage';
import { TagLabel } from '../generated/type/tagLabel'; import { TagLabel } from '../generated/type/tagLabel';
import { UsageDetails } from '../generated/type/usageDetails'; import { UsageDetails } from '../generated/type/usageDetails';
@ -2637,5 +2640,10 @@ export const EntityTypeName: Record<EntityType, string> = {
[EntityType.SERVICE]: t('label.service'), [EntityType.SERVICE]: t('label.service'),
[EntityType.DATA_CONTRACT]: t('label.data-contract'), [EntityType.DATA_CONTRACT]: t('label.data-contract'),
[EntityType.SECURITY_SERVICE]: t('label.security-service'), [EntityType.SECURITY_SERVICE]: t('label.security-service'),
[EntityType.DATA_CONTRACT]: t('label.data-contract'), };
export const getDataContractStatusIcon = (status: ContractExecutionStatus) => {
return status === ContractExecutionStatus.Failed ? (
<Icon component={CancelOutlineIcon} />
) : null;
}; };