#23987: fix contract old schema column not visible on schema form while edit (#24027)

* fix contract old schema column not visible on schem form while edit

* fix the unit test failing

* show column status, represent which column is being failed and passed

* fix the dropdown scrolling with screen and fix sonar issue as well
This commit is contained in:
Ashish Gupta 2025-10-28 17:07:33 +05:30 committed by GitHub
parent 3fb800cabc
commit c903f3b485
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
27 changed files with 291 additions and 22 deletions

View File

@ -1418,6 +1418,144 @@ test.describe('Data Contracts', () => {
}
});
test('Operation on Old Schema Columns Contract', async ({ page }) => {
test.slow(true);
const { apiContext } = await getApiContext(page);
const table = new TableClass();
await table.create(apiContext);
await redirectToHomePage(page);
await table.visitEntityPage(page);
const entityFQN = table.entityResponseData.fullyQualifiedName;
await page.click('[data-testid="contract"]');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await page.getByTestId('add-contract-button').click();
await expect(page.getByTestId('add-contract-card')).toBeVisible();
await page.getByTestId('contract-name').fill(DATA_CONTRACT_DETAILS.name);
await page.getByRole('tab', { name: 'Schema' }).click();
await page
.locator('input[type="checkbox"][aria-label="Select all"]')
.check();
await expect(
page.getByRole('checkbox', { name: 'Select all' })
).toBeChecked();
// save and trigger contract validation
await saveAndTriggerDataContractValidation(page, true);
await expect(
page.getByTestId('contract-status-card-item-schema-status')
).toContainText('Passed');
// Modify the first 2 columns with PATCH API
await table.patch({
apiContext,
patchData: [
{
op: 'replace',
path: '/columns/0/name',
value: 'new_column_0',
},
{
op: 'replace',
path: '/columns/0/fullyQualifiedName',
value: `${table.entityResponseData.fullyQualifiedName}.new_column_0`,
},
{
op: 'replace',
path: '/columns/1/name',
value: 'new_column_1',
},
{
op: 'replace',
path: '/columns/1/fullyQualifiedName',
value: `${table.entityResponseData.fullyQualifiedName}.new_column_1`,
},
],
});
// Run Contract After Schema Change should Fail
await page.getByTestId('manage-contract-actions').click();
await page.waitForSelector('.contract-action-dropdown', {
state: 'visible',
});
await page.getByTestId('contract-run-now-button').click();
await page.reload();
await page.waitForLoadState('networkidle');
await page.waitForSelector('[data-testid="loader"]', {
state: 'detached',
});
await expect(
page.getByTestId('contract-status-card-item-schema-status')
).toContainText('Failed');
await expect(
page.getByTestId('data-contract-latest-result-btn')
).toContainText('Contract Failed');
await expect(
page.getByTestId(`schema-column-${table.entityLinkColumnsName[0]}-failed`)
).toBeVisible();
await expect(
page.getByTestId(`schema-column-${table.entityLinkColumnsName[1]}-failed`)
).toBeVisible();
// Check the Columns Present in Contract Schema Form Component
await page.getByTestId('manage-contract-actions').click();
await page.waitForSelector('.contract-action-dropdown', {
state: 'visible',
});
await page.getByTestId('contract-edit-button').click();
await page.getByRole('tab', { name: 'Schema' }).click();
// Old column should be visible and we should un-check them
await page
.locator(
`[data-row-key="${entityFQN}.${table.entityLinkColumnsName[0]}"] .ant-checkbox-input`
)
.click();
await page
.locator(
`[data-row-key="${entityFQN}.${table.entityLinkColumnsName[1]}"] .ant-checkbox-input`
)
.click();
// Select newly added column
await page
.locator(`[data-row-key="${entityFQN}.new_column_0"] .ant-checkbox-input`)
.click();
await page
.locator(`[data-row-key="${entityFQN}.new_column_1"] .ant-checkbox-input`)
.click();
// save and trigger contract validation
await saveAndTriggerDataContractValidation(page, true);
await expect(
page.getByTestId('contract-status-card-item-schema-status')
).toContainText('Passed');
});
test('should allow adding a semantic with multiple rules', async ({
page,
}) => {

View File

@ -55,6 +55,7 @@ import {
} from '../../../utils/DataContract/DataContractUtils';
import { customFormatDateTime } from '../../../utils/date-time/DateTimeUtils';
import { getEntityName } from '../../../utils/EntityUtils';
import { getPopupContainer } from '../../../utils/formUtils';
import { pruneEmptyChildren } from '../../../utils/TableUtils';
import { showErrorToast, showSuccessToast } from '../../../utils/ToastUtils';
import AlertBar from '../../AlertBar/AlertBar';
@ -254,6 +255,7 @@ const ContractDetail: React.FC<{
<Dropdown
destroyPopupOnHide
getPopupContainer={getPopupContainer}
menu={{
items: contractActionsItems,
onClick: handleContractAction,
@ -481,6 +483,9 @@ const ContractDetail: React.FC<{
<ContractSchemaTable
contractStatus={constraintStatus['schema']}
latestSchemaValidationResult={
latestContractResults?.schemaValidation
}
schemaDetail={schemaDetail}
/>
</Col>

View File

@ -16,6 +16,10 @@
.ant-card-head {
border-bottom: none;
.ant-card-head-title {
overflow: initial;
}
.contract-header-container {
width: 100%;
display: flex;

View File

@ -26,7 +26,7 @@ import {
import { TABLE_COLUMNS_KEYS } from '../../../constants/TableKeys.constants';
import { EntityType, FqnPart } from '../../../enums/entity.enum';
import { DataContract } from '../../../generated/entity/data/dataContract';
import { Column } from '../../../generated/entity/data/table';
import { Column, Table } from '../../../generated/entity/data/table';
import { TagSource } from '../../../generated/tests/testCase';
import { TagLabel } from '../../../generated/type/tagLabel';
import { usePaging } from '../../../hooks/paging/usePaging';
@ -40,7 +40,8 @@ import {
import Fqn from '../../../utils/Fqn';
import { pruneEmptyChildren } from '../../../utils/TableUtils';
import { PagingHandlerParams } from '../../common/NextPrevious/NextPrevious.interface';
import Table from '../../common/Table/Table';
import AntTable from '../../common/Table/Table';
import { useGenericContext } from '../../Customization/GenericProvider/GenericProvider';
import { TableCellRendered } from '../../Database/SchemaTable/SchemaTable.interface';
import TableTags from '../../Database/TableTags/TableTags.component';
@ -54,7 +55,8 @@ export const ContractSchemaFormTab: React.FC<{
}> = ({ selectedSchema, onNext, onChange, onPrev, nextLabel, prevLabel }) => {
const { t } = useTranslation();
const { fqn } = useFqn();
const [allColumnsData, setAllColumnData] = useState<Column[]>([]);
const { data: tableData } = useGenericContext();
const [allColumnsData, setAllColumnsData] = useState<Column[]>([]);
const [columnsData, setColumnsData] = useState<Column[]>([]);
const [selectedKeys, setSelectedKeys] = useState<string[]>();
const [isLoading, setIsLoading] = useState(false);
@ -95,6 +97,17 @@ export const ContractSchemaFormTab: React.FC<{
[allColumnsData, onChange]
);
// Old Columns which are available in Contract but being Modified/Removed at Table Schema Level
const oldRemovedColumns = useMemo(() => {
const columnsDataFQN = new Set(
(tableData as Table).columns.map((col) => col.fullyQualifiedName)
);
return selectedSchema.filter(
(col) => !columnsDataFQN.has(col.fullyQualifiedName)
);
}, [selectedSchema, tableData]);
const fetchTableColumns = useCallback(
async (page = 1) => {
if (!tableFqn) {
@ -112,9 +125,18 @@ export const ContractSchemaFormTab: React.FC<{
});
const prunedColumns = pruneEmptyChildren(response.data);
setColumnsData(prunedColumns);
setAllColumnData((prev) => {
const combined = [...prev, ...selectedSchema, ...prunedColumns];
const oldPrunedColumns = pruneEmptyChildren(oldRemovedColumns);
// should render the oldPrunedColumns only on the first page, if there is pagination
setColumnsData(
offset === 0 ? [...oldPrunedColumns, ...prunedColumns] : prunedColumns
);
setAllColumnsData((prev) => {
const combined = [
...prev,
...selectedSchema,
...oldPrunedColumns,
...prunedColumns,
];
return uniqBy(combined, 'fullyQualifiedName');
});
@ -131,7 +153,7 @@ export const ContractSchemaFormTab: React.FC<{
}
setIsLoading(false);
},
[tableFqn, pageSize, selectedSchema, setAllColumnData]
[tableFqn, pageSize, selectedSchema, oldRemovedColumns, setAllColumnsData]
);
const handleColumnsPageChange = useCallback(
@ -295,7 +317,7 @@ export const ContractSchemaFormTab: React.FC<{
useEffect(() => {
setSelectedKeys(
selectedSchema.map((item) => (item as Column).fullyQualifiedName ?? '')
selectedSchema.map((item) => item.fullyQualifiedName ?? '')
);
}, [selectedSchema]);
@ -314,7 +336,7 @@ export const ContractSchemaFormTab: React.FC<{
{t('message.data-contract-schema-description')}
</Typography.Paragraph>
</div>
<Table
<AntTable
columns={columns}
customPaginationProps={paginationProps}
dataSource={columnsData}

View File

@ -21,6 +21,7 @@ import {
} from '@testing-library/react';
import { Column } from '../../../generated/entity/data/table';
import { useFqn } from '../../../hooks/useFqn';
import { mockTableData } from '../../../mocks/TableVersion.mock';
import { getTableColumnsByFQN } from '../../../rest/tableAPI';
import { ContractSchemaFormTab } from './ContractScehmaFormTab';
@ -50,6 +51,12 @@ jest.mock('../../../utils/TableUtils', () => ({
pruneEmptyChildren: jest.fn().mockImplementation((columns) => columns),
}));
jest.mock('../../Customization/GenericProvider/GenericProvider', () => ({
useGenericContext: jest.fn().mockImplementation(() => ({
data: mockTableData,
})),
}));
jest.mock('../../common/Table/Table', () => {
return function MockTable({
columns,

View File

@ -12,11 +12,16 @@
*/
import Icon from '@ant-design/icons';
import { Col, Row, Tag, Typography } from 'antd';
import { ColumnsType, ColumnType, TablePaginationConfig } from 'antd/lib/table';
import classNames from 'classnames';
import { useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import { ReactComponent as ArrowIcon } from '../../../assets/svg/arrow-right-full.svg';
import { ReactComponent as FailedIcon } from '../../../assets/svg/fail-badge.svg';
import { ReactComponent as CompletedIcon } from '../../../assets/svg/ic-check-circle-colored.svg';
import { LIST_SIZE, NO_DATA_PLACEHOLDER } from '../../../constants/constants';
import { Column } from '../../../generated/entity/data/table';
import { SchemaValidation } from '../../../generated/entity/datacontract/dataContractResult';
import { getContractStatusType } from '../../../utils/DataContract/DataContractUtils';
import StatusBadgeV2 from '../../common/StatusBadge/StatusBadgeV2.component';
import Table from '../../common/Table/Table';
@ -25,10 +30,23 @@ import './contract-schema.less';
const ContractSchemaTable: React.FC<{
schemaDetail: Column[];
contractStatus?: string;
}> = ({ schemaDetail, contractStatus }) => {
latestSchemaValidationResult?: SchemaValidation;
}> = ({ schemaDetail, contractStatus, latestSchemaValidationResult }) => {
const { t } = useTranslation();
const schemaColumns = useMemo(
const tablePaginationProps: TablePaginationConfig = useMemo(
() => ({
size: 'default',
hideOnSinglePage: true,
pageSize: LIST_SIZE,
prevIcon: <Icon component={ArrowIcon} />,
nextIcon: <Icon component={ArrowIcon} />,
className: 'schema-custom-pagination',
}),
[]
);
const schemaColumns: ColumnsType<Column> = useMemo(
() => [
{
title: t('label.name'),
@ -66,8 +84,31 @@ const ContractSchemaTable: React.FC<{
</div>
),
},
...(latestSchemaValidationResult
? [
{
title: t('label.column-status'),
dataIndex: 'name',
key: 'columnStatus',
align: 'center',
render: (name: string) => {
const isColumnFailed =
latestSchemaValidationResult?.failedFields?.includes(name);
const iconClass = isColumnFailed ? 'failed' : 'success';
return (
<Icon
className={classNames('column-status-icon', iconClass)}
component={isColumnFailed ? FailedIcon : CompletedIcon}
data-testid={`schema-column-${name}-${iconClass}`}
/>
);
},
} as ColumnType<Column>,
]
: []),
],
[]
[latestSchemaValidationResult]
);
return (
@ -76,14 +117,7 @@ const ContractSchemaTable: React.FC<{
<Table
columns={schemaColumns}
dataSource={schemaDetail}
pagination={{
size: 'default',
hideOnSinglePage: true,
pageSize: LIST_SIZE,
prevIcon: <Icon component={ArrowIcon} />,
nextIcon: <Icon component={ArrowIcon} />,
className: 'schema-custom-pagination',
}}
pagination={tablePaginationProps}
rowKey="name"
size="small"
/>

View File

@ -55,10 +55,42 @@ describe('ContractSchemaTable', () => {
expect(screen.getByText('label.name')).toBeInTheDocument();
expect(screen.getByText('label.type')).toBeInTheDocument();
expect(screen.getByText('label.constraint-plural')).toBeInTheDocument();
expect(screen.queryByText('label.column-status')).not.toBeInTheDocument();
expect(screen.queryByText('StatusBadgeV2')).not.toBeInTheDocument();
});
it('should render ColumnStatus column when latestSchemaValidationResult present', () => {
render(
<ContractSchemaTable
latestSchemaValidationResult={{
failed: 1,
failedFields: ['name'],
passed: 5,
total: 6,
}}
schemaDetail={mockSchemaDetail}
/>
);
expect(screen.getByText('label.column-status')).toBeInTheDocument();
expect(screen.getByTestId('schema-column-name-failed')).toBeInTheDocument();
expect(screen.getByTestId('schema-column-id-success')).toBeInTheDocument();
expect(
screen.getByTestId('schema-column-email-success')
).toBeInTheDocument();
expect(
screen.getByTestId('schema-column-contract-success')
).toBeInTheDocument();
expect(
screen.getByTestId('schema-column-property-success')
).toBeInTheDocument();
// Since being on second page
expect(
screen.queryByTestId('schema-column-business-success')
).not.toBeInTheDocument();
});
it('should render schema table with pagination', () => {
render(<ContractSchemaTable schemaDetail={mockSchemaDetail} />);

View File

@ -13,6 +13,15 @@
@import (reference) '../../../styles/variables.less';
.contract-schema-component-container {
.column-status-icon {
font-size: 16px;
&.success {
color: transparent;
}
}
}
.ant-pagination.schema-custom-pagination {
align-items: center;
&.ant-table-pagination-right {

View File

@ -54,7 +54,7 @@ export const ContractTab = () => {
}
};
const handleDelete = async () => {
const handleDelete = () => {
if (contract?.id) {
setIsDeleteModalVisible(true);
}

View File

@ -262,6 +262,7 @@
"column-name": "Spaltenname",
"column-plural": "Spalten",
"column-profile": "Spaltenprofil",
"column-status": "Spaltenstatus",
"comment": "Kommentar",
"comment-lowercase": "kommentar",
"comment-plural": "Kommentare",

View File

@ -262,6 +262,7 @@
"column-name": "Column Name",
"column-plural": "Columns",
"column-profile": "Column Profile",
"column-status": "Column Status",
"comment": "Comment",
"comment-lowercase": "comment",
"comment-plural": "Comments",

View File

@ -262,6 +262,7 @@
"column-name": "Nombre de columna",
"column-plural": "Columnas",
"column-profile": "Perfilado de columnas",
"column-status": "Estado de la columna",
"comment": "Comentar",
"comment-lowercase": "comentario",
"comment-plural": "Comentarios",

View File

@ -262,6 +262,7 @@
"column-name": "Nom de colonne",
"column-plural": "Colonnes",
"column-profile": "Profil de Colonne",
"column-status": "Statut de la colonne",
"comment": "Commentaire",
"comment-lowercase": "commentaire",
"comment-plural": "Commentaires",

View File

@ -262,6 +262,7 @@
"column-name": "Nome da columna",
"column-plural": "Columnas",
"column-profile": "Perfil da columna",
"column-status": "Estado da columna",
"comment": "Comentario",
"comment-lowercase": "comentario",
"comment-plural": "Comentarios",

View File

@ -262,6 +262,7 @@
"column-name": "שם עמודה",
"column-plural": "עמודות",
"column-profile": "פרופיל עמודה",
"column-status": "סטטוס עמודה",
"comment": "תגובה",
"comment-lowercase": "תגובה",
"comment-plural": "תגובות",

View File

@ -262,6 +262,7 @@
"column-name": "カラム名",
"column-plural": "カラム",
"column-profile": "カラムプロファイル",
"column-status": "カラムステータス",
"comment": "コメント",
"comment-lowercase": "コメント",
"comment-plural": "コメント",

View File

@ -262,6 +262,7 @@
"column-name": "열 이름",
"column-plural": "열들",
"column-profile": "열 프로필",
"column-status": "열 상태",
"comment": "댓글",
"comment-lowercase": "댓글",
"comment-plural": "댓글들",

View File

@ -262,6 +262,7 @@
"column-name": "स्तंभ नाव",
"column-plural": "स्तंभ",
"column-profile": "स्तंभ प्रोफाइल",
"column-status": "स्तंभ स्थिती",
"comment": "टिप्पणी",
"comment-lowercase": "टिप्पणी",
"comment-plural": "टिप्पण्या",

View File

@ -262,6 +262,7 @@
"column-name": "Kolomnaam",
"column-plural": "Kolommen",
"column-profile": "Kolomprofiel",
"column-status": "Kolomstatus",
"comment": "Opmerking",
"comment-lowercase": "opmerking",
"comment-plural": "Opmerkingen",

View File

@ -262,6 +262,7 @@
"column-name": "نام ستون",
"column-plural": "ستون‌ها",
"column-profile": "پروفایل ستون",
"column-status": "وضعیت ستون",
"comment": "نظر",
"comment-lowercase": "نظر",
"comment-plural": "Comentarios",

View File

@ -262,6 +262,7 @@
"column-name": "Nome da coluna",
"column-plural": "Colunas",
"column-profile": "Perfil da Coluna",
"column-status": "Status da coluna",
"comment": "Comentário",
"comment-lowercase": "comentário",
"comment-plural": "Comentários",

View File

@ -262,6 +262,7 @@
"column-name": "Nome da coluna",
"column-plural": "Colunas",
"column-profile": "Perfil da Coluna",
"column-status": "Estado da coluna",
"comment": "Comentário",
"comment-lowercase": "comentário",
"comment-plural": "Comentários",

View File

@ -262,6 +262,7 @@
"column-name": "Имя столбца",
"column-plural": "Столбцы",
"column-profile": "Профиль столбца",
"column-status": "Статус столбца",
"comment": "Комментарий",
"comment-lowercase": "комментарий",
"comment-plural": "Комментарии",

View File

@ -262,6 +262,7 @@
"column-name": "ชื่อคอลัมน์",
"column-plural": "คอลัมน์หลายรายการ",
"column-profile": "โปรไฟล์คอลัมน์",
"column-status": "สถานะคอลัมน์",
"comment": "ความคิดเห็น",
"comment-lowercase": "ความคิดเห็น",
"comment-plural": "ความคิดเห็น",

View File

@ -262,6 +262,7 @@
"column-name": "Sütun adı",
"column-plural": "Sütunlar",
"column-profile": "Sütun Profili",
"column-status": "Sütun Durumu",
"comment": "Yorum",
"comment-lowercase": "yorum",
"comment-plural": "Yorumlar",

View File

@ -262,6 +262,7 @@
"column-name": "列名",
"column-plural": "列",
"column-profile": "列分析",
"column-status": "列状态",
"comment": "评论",
"comment-lowercase": "评论",
"comment-plural": "评论",

View File

@ -262,6 +262,7 @@
"column-name": "欄位名稱",
"column-plural": "欄位",
"column-profile": "欄位分析",
"column-status": "欄位狀態",
"comment": "留言",
"comment-lowercase": "留言",
"comment-plural": "留言",