diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts index a59117e0f96..455d2ad3b10 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/TestCases.spec.ts @@ -75,6 +75,13 @@ test('Table difference test case', async ({ page }) => { ); await page.getByTestId('tableDiff').click(); await tableListSearchResponse; + + const table2KeyColumnsInput = page.locator( + '#testCaseFormV1_params_table2\\.keyColumns_0_value' + ); + + await expect(table2KeyColumnsInput).toBeDisabled(); + await page.click('#testCaseFormV1_params_table2'); await page.waitForSelector(`[data-id="tableDiff"]`, { state: 'visible', @@ -107,6 +114,15 @@ test('Table difference test case', async ({ page }) => { table1.entity?.columns[0].name ); await page.getByTitle(table1.entity?.columns[0].name).click(); + + await page.fill( + '#testCaseFormV1_params_table2\\.keyColumns_0_value', + table2.entity?.columns[0].name + ); + await page.getByTitle(table2.entity?.columns[0].name).click(); + + await expect(table2KeyColumnsInput).not.toBeDisabled(); + await page.fill('#testCaseFormV1_params_threshold', testCase.threshold); await page.fill( '#testCaseFormV1_params_useColumns_0_value', @@ -160,6 +176,44 @@ test('Table difference test case', async ({ page }) => { `Edit ${testCase.name}` ); + // Wait for form to finish loading (isLoading becomes false) + await expect(page.getByTestId('edit-test-form')).toBeVisible(); + + // Verify Table 1's keyColumns is enabled and populated in edit mode + const table1KeyColumnsEditInput = page.locator( + '#tableTestForm_params_keyColumns_0_value' + ); + + // Wait for the input to be visible and enabled, and then check its value + await expect(table1KeyColumnsEditInput).toBeVisible(); + await expect(table1KeyColumnsEditInput).not.toBeDisabled(); + + // Wait for the value to be populated + // Use data-testid to find the select component + const columnName = table1.entity?.columns[0].name; + const table1Select = page.getByTestId('keyColumns-select'); + + // Wait for the select to be visible and verify the selected value is displayed + await expect(table1Select).toBeVisible(); + await expect(table1Select.getByText(columnName)).toBeVisible(); + + // Verify table2.keyColumns is enabled and populated in edit mode + const table2KeyColumnsEditInput = page.locator( + '#tableTestForm_params_table2\\.keyColumns_0_value' + ); + + // Wait for the input to be visible and enabled, and then check its value + await expect(table2KeyColumnsEditInput).toBeVisible(); + await expect(table2KeyColumnsEditInput).not.toBeDisabled(); + + // Wait for the value to be populated + const table2ColumnName = table2.entity?.columns[0].name; + const table2Select = page.getByTestId('table2.keyColumns-select'); + + // Wait for the select to be visible and verify the selected value is displayed + await expect(table2Select).toBeVisible(); + await expect(table2Select.getByText(table2ColumnName)).toBeVisible(); + await page .locator('label') .filter({ hasText: "Table 1's key columns" }) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.test.tsx index 631d065c116..a5e8b6def0d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.test.tsx @@ -18,6 +18,7 @@ import { TestDefinition } from '../../../../generated/tests/testDefinition'; import { MOCK_TABLE_COLUMN_NAME_TO_EXIST, MOCK_TABLE_CUSTOM_SQL_QUERY, + MOCK_TABLE_DIFF_DEFINITION, MOCK_TABLE_ROW_INSERTED_COUNT_TO_BE_BETWEEN, MOCK_TABLE_TEST_WITH_COLUMN, MOCK_TABLE_WITH_DATE_TIME_COLUMNS, @@ -72,6 +73,12 @@ jest.mock('../../../../rest/searchAPI', () => { }; }); +jest.mock('../../../../rest/tableAPI', () => { + return { + getTableDetailsByFQN: jest.fn(), + }; +}); + const renderWithForm = (component: React.ReactElement) => { return render(
{component}
); }; @@ -183,4 +190,58 @@ describe('ParameterForm component test', () => { expect(selectBox).toBeInTheDocument(); }); + + describe('Table Diff functionality', () => { + beforeEach(() => { + jest.clearAllMocks(); + }); + + it('Should render table2 parameter for table diff definition', async () => { + await act(async () => { + renderWithForm( + + ); + }); + + const table2Select = await screen.findByTestId('table2'); + + expect(table2Select).toBeInTheDocument(); + }); + + it('Should render all parameters when table has columns', async () => { + await act(async () => { + renderWithForm( + + ); + }); + + const parameters = await screen.findAllByTestId('parameter'); + + expect(parameters.length).toBeGreaterThan(0); + }); + + it('Should have table2.keyColumns disabled when table2 is not selected', async () => { + await act(async () => { + renderWithForm( + + ); + }); + + const keyColumnsInputs = screen.getAllByRole('combobox'); + const table2KeyColumnsInput = keyColumnsInputs.find((input) => + input.id.includes('table2.keyColumns') + ); + + expect(table2KeyColumnsInput).toBeDisabled(); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.tsx b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.tsx index cee9a0beb99..f3e7b6f08c4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/DataQuality/AddDataQualityTest/components/ParameterForm.tsx @@ -36,6 +36,7 @@ import { SUPPORTED_PARTITION_TYPE_FOR_DATE_TIME } from '../../../../constants/pr import { TABLE_DIFF } from '../../../../constants/TestSuite.constant'; import { CSMode } from '../../../../enums/codemirror.enum'; import { SearchIndex } from '../../../../enums/search.enum'; +import { Column } from '../../../../generated/entity/data/table'; import { Rule, TestCaseParameterDefinition, @@ -49,6 +50,7 @@ import { searchQuery } from '../../../../rest/searchAPI'; import { getEntityName } from '../../../../utils/EntityUtils'; import { getPopupContainer } from '../../../../utils/formUtils'; import { + getSelectedColumnsSet, validateEquals, validateGreaterThanOrEquals, validateLessThanOrEquals, @@ -320,24 +322,29 @@ const ParameterForm: React.FC = ({ definition, table }) => { }; const TableDiffForm = () => { + const form = Form.useFormInstance(); const [isOptionsLoading, setIsOptionsLoading] = useState(false); const [tableList, setTableList] = useState< SearchHitBody< SearchIndex.TABLE, - Pick + Pick< + TableSearchSource, + 'name' | 'displayName' | 'fullyQualifiedName' | 'columns' + > >[] >([]); + const [table2Columns, setTable2Columns] = useState(); + const tableOptions = useMemo( () => - tableList.map((hit) => { - return { - label: hit._source.fullyQualifiedName, - value: hit._source.fullyQualifiedName, - }; - }), + tableList.map((hit) => ({ + label: hit._source.fullyQualifiedName, + value: hit._source.fullyQualifiedName, + })), [tableList] ); - const fetchTableData = async (search = WILD_CARD_CHAR) => { + + const fetchTableData = useCallback(async (search = WILD_CARD_CHAR) => { setIsOptionsLoading(true); try { const response = await searchQuery({ @@ -346,76 +353,129 @@ const ParameterForm: React.FC = ({ definition, table }) => { pageSize: PAGE_SIZE_LARGE, searchIndex: SearchIndex.TABLE, fetchSource: true, - includeFields: ['name', 'fullyQualifiedName', 'displayName'], + includeFields: [ + 'name', + 'fullyQualifiedName', + 'displayName', + 'columns', + ], }); - setTableList(response.hits.hits); - } catch (error) { + } catch { setTableList([]); } finally { setIsOptionsLoading(false); } - }; + }, []); - const debounceFetchTableData = useCallback(debounce(fetchTableData, 1000), [ - fetchTableData, - ]); + const debounceFetchTableData = useMemo( + () => debounce(fetchTableData, 1000), + [fetchTableData] + ); + + useEffect(() => { + fetchTableData(); + }, [fetchTableData]); + + useEffect(() => { + const table2Value = form.getFieldValue(['params', 'table2']); + if (table2Value && !table2Columns && tableList.length > 0) { + const selectedTable = tableList.find( + (hit) => hit._source.fullyQualifiedName === table2Value + ); + if (selectedTable) { + setTable2Columns(selectedTable._source.columns); + } + } + }, [tableList, table2Columns, form]); const getFormData = (data: TestCaseParameterDefinition) => { switch (data.name) { case 'table2': - return prepareForm( - data, - { + // Clear key columns when table2 changes + setFieldsValue({ + params: { 'table2.keyColumns': [{ value: undefined }] }, + }); + + // Update columns or clear them + if (value) { + const selectedTable = tableList.find( + (hit) => hit._source.fullyQualifiedName === value + ); + setTable2Columns(selectedTable?._source.columns); + } else { + setTable2Columns(undefined); + } + }} + onSearch={debounceFetchTableData} + /> + ) + } + ); case 'keyColumns': + case 'table2.keyColumns': case 'useColumns': return ( - + {({ getFieldValue }) => { - // Convert selectedKeyColumn and selectedUseColumns to Sets for efficient lookup - const selectedKeyColumnSet = new Set( - getFieldValue(['params', 'keyColumns'])?.map( - (item: { value: string }) => item?.value - ) - ); - const selectedUseColumnsSet = new Set( - getFieldValue(['params', 'useColumns'])?.map( - (item: { value: string }) => item?.value - ) + const isTable2KeyColumns = data.name === 'table2.keyColumns'; + const table2Value = getFieldValue(['params', 'table2']); + + let sourceColumns = table?.columns; + if (isTable2KeyColumns) { + if (table2Value) { + const selectedTable = + tableList.find( + (hit) => hit._source.fullyQualifiedName === table2Value + ) ?? undefined; + sourceColumns = + selectedTable?._source.columns ?? table2Columns; + } else { + sourceColumns = undefined; + } + } + + // Disable when no table2 selected + const isDisabled = isTable2KeyColumns && !table2Value; + + const selectedColumnsSet = getSelectedColumnsSet( + data, + getFieldValue ); - // Combine both Sets for a single lookup operation - const selectedColumnsSet = new Set([ - ...selectedKeyColumnSet, - ...selectedUseColumnsSet, - ]); - - const columns = table?.columns.map((column) => ({ - label: getEntityName(column), - value: column.name, - // Check if column.name is in the combined Set to determine if it should be disabled - disabled: selectedColumnsSet.has(column.name), - })); + const columnOptions = + sourceColumns?.map((column) => ({ + label: getEntityName(column), + value: column.name, + disabled: selectedColumnsSet.has(column.name), + })) ?? []; return prepareForm( data,