diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts index 5c10ab50c75..9a91d671cb9 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/Table.spec.ts @@ -21,6 +21,7 @@ import { removeTagsFromChildren, } from '../../utils/entity'; import { sidebarClick } from '../../utils/sidebar'; +import { columnPaginationTable } from '../../utils/table'; import { test } from '../fixtures/pages'; const table1 = new TableClass(); @@ -262,46 +263,7 @@ test.describe('Table & Data Model columns table pagination', () => { '2000' ); - // 50 Row + 1 Header row - expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(51); - - expect(page.getByTestId('page-indicator')).toHaveText(`Page 1 of 40`); - - await page.getByTestId('next').click(); - - await page.waitForSelector('[data-testid="loader"]', { - state: 'detached', - }); - - expect(page.getByTestId('page-indicator')).toHaveText(`Page 2 of 40`); - - expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(51); - - await page.getByTestId('previous').click(); - - expect(page.getByTestId('page-indicator')).toHaveText(`Page 1 of 40`); - - // Change page size to 15 - await page.getByTestId('page-size-selection-dropdown').click(); - await page.getByRole('menuitem', { name: '15 / Page' }).click(); - - await page.waitForSelector('[data-testid="loader"]', { - state: 'detached', - }); - - // 15 Row + 1 Header row - expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(16); - - // Change page size to 25 - await page.getByTestId('page-size-selection-dropdown').click(); - await page.getByRole('menuitem', { name: '25 / Page' }).click(); - - await page.waitForSelector('[data-testid="loader"]', { - state: 'detached', - }); - - // 25 Row + 1 Header row - expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(26); + await columnPaginationTable(page); }); test('pagination for dashboard data model columns should work', async ({ diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TableVersionPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TableVersionPage.spec.ts new file mode 100644 index 00000000000..0c4ca73b4f1 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/TableVersionPage.spec.ts @@ -0,0 +1,54 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, test } from '@playwright/test'; +import { redirectToHomePage } from '../../utils/common'; +import { columnPaginationTable } from '../../utils/table'; + +// use the admin user to login +test.use({ storageState: 'playwright/.auth/admin.json' }); + +test.describe('Table Version Page', () => { + test('Pagination and Search should works for columns', async ({ page }) => { + await redirectToHomePage(page); + await page.goto( + '/table/sample_data.ecommerce_db.shopify.performance_test_table/versions/0.1' + ); + + await page.waitForLoadState('networkidle'); + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + await test.step('Pagination Should Work', async () => { + await columnPaginationTable(page); + }); + + await test.step('Search Should Work', async () => { + const searchResponse = page.waitForResponse( + '/api/v1/tables/name/sample_data.ecommerce_db.shopify.performance_test_table/columns/search?q=test_col_0250*' + ); + await page.getByTestId('searchbar').fill('test_col_0250'); + await searchResponse; + + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(2); + + expect( + page.getByTestId('entity-table').getByText('test_col_0250') + ).toBeVisible(); + }); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/table.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/table.ts new file mode 100644 index 00000000000..7983928266b --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/table.ts @@ -0,0 +1,57 @@ +/* + * Copyright 2025 Collate. + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * http://www.apache.org/licenses/LICENSE-2.0 + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { expect, Page } from '@playwright/test'; + +// Pagination is performed for "performance_test_table" Table Entity +export const columnPaginationTable = async (page: Page) => { + // 50 Row + 1 Header row + expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(51); + + expect(page.getByTestId('page-indicator')).toHaveText(`Page 1 of 40`); + + await page.getByTestId('next').click(); + + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + expect(page.getByTestId('page-indicator')).toHaveText(`Page 2 of 40`); + + expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(51); + + await page.getByTestId('previous').click(); + + expect(page.getByTestId('page-indicator')).toHaveText(`Page 1 of 40`); + + // Change page size to 15 + await page.getByTestId('page-size-selection-dropdown').click(); + await page.getByRole('menuitem', { name: '15 / Page' }).click(); + + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + // 15 Row + 1 Header row + expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(16); + + // Change page size to 25 + await page.getByTestId('page-size-selection-dropdown').click(); + await page.getByRole('menuitem', { name: '25 / Page' }).click(); + + await page.waitForSelector('[data-testid="loader"]', { + state: 'detached', + }); + + // 25 Row + 1 Header row + expect(page.getByTestId('entity-table').getByRole('row')).toHaveCount(26); +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx index ef726da516b..bc139ec7d34 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.component.tsx @@ -14,10 +14,11 @@ import { Col, Row, Space, Tabs, TabsProps } from 'antd'; import classNames from 'classnames'; import { cloneDeep, toString } from 'lodash'; -import { useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { useNavigate } from 'react-router-dom'; import { FQN_SEPARATOR_CHAR } from '../../../constants/char.constants'; +import { PAGE_SIZE_LARGE } from '../../../constants/constants'; import { EntityField } from '../../../constants/Feeds.constants'; import { EntityTabs, EntityType, FqnPart } from '../../../enums/entity.enum'; import { @@ -26,6 +27,12 @@ import { ColumnJoins, } from '../../../generated/entity/data/table'; import { TagSource } from '../../../generated/type/tagLabel'; +import { usePaging } from '../../../hooks/paging/usePaging'; +import { useFqn } from '../../../hooks/useFqn'; +import { + getTableColumnsByFQN, + searchTableColumnsByFQN, +} from '../../../rest/tableAPI'; import { getPartialNameFromTableFQN } from '../../../utils/CommonUtils'; import { getColumnsDataWithVersionChanges, @@ -35,10 +42,12 @@ import { getEntityVersionTags, } from '../../../utils/EntityVersionUtils'; import { getVersionPath } from '../../../utils/RouterUtils'; +import { pruneEmptyChildren } from '../../../utils/TableUtils'; import { useRequiredParams } from '../../../utils/useRequiredParams'; import { CustomPropertyTable } from '../../common/CustomPropertyTable/CustomPropertyTable'; import DescriptionV1 from '../../common/EntityDescription/DescriptionV1'; import Loader from '../../common/Loader/Loader'; +import { PagingHandlerParams } from '../../common/NextPrevious/NextPrevious.interface'; import TabsLabel from '../../common/TabsLabel/TabsLabel.component'; import { GenericProvider } from '../../Customization/GenericProvider/GenericProvider'; import DataAssetsVersionHeader from '../../DataAssets/DataAssetsVersionHeader/DataAssetsVersionHeader'; @@ -66,10 +75,101 @@ const TableVersion: React.FC = ({ const { t } = useTranslation(); const navigate = useNavigate(); const { tab } = useRequiredParams<{ tab: EntityTabs }>(); + const { + currentPage, + pageSize, + handlePageChange, + handlePageSizeChange, + showPagination, + paging, + handlePagingChange, + } = usePaging(PAGE_SIZE_LARGE); + const { fqn: tableFqn } = useFqn(); + const [searchText, setSearchText] = useState(''); + // Pagination state for columns + const [tableColumns, setTableColumns] = useState([]); + const [columnsLoading, setColumnsLoading] = useState(true); // Start with loading state const [changeDescription, setChangeDescription] = useState( currentVersionData.changeDescription as ChangeDescription ); + // Function to fetch paginated columns or search results + const fetchPaginatedColumns = useCallback( + async (page = 1, searchQuery?: string) => { + if (!tableFqn) { + return; + } + + setColumnsLoading(true); + try { + const offset = (page - 1) * pageSize; + + // Use search API if there's a search query, otherwise use regular pagination + const response = searchQuery + ? await searchTableColumnsByFQN(tableFqn, { + q: searchQuery, + limit: pageSize, + offset: offset, + fields: 'tags', + }) + : await getTableColumnsByFQN(tableFqn, { + limit: pageSize, + offset: offset, + fields: 'tags', + }); + + setTableColumns(pruneEmptyChildren(response.data) || []); + handlePagingChange(response.paging); + } catch { + // Set empty state if API fails + setTableColumns([]); + handlePagingChange({ + offset: 1, + limit: pageSize, + total: 0, + }); + } finally { + setColumnsLoading(false); + } + }, + [tableFqn, pageSize] + ); + + const handleSearchAction = useCallback((searchValue: string) => { + setSearchText(searchValue); + }, []); + + const handleColumnsPageChange = useCallback( + ({ currentPage }: PagingHandlerParams) => { + fetchPaginatedColumns(currentPage, searchText); + handlePageChange(currentPage); + }, + [paging, fetchPaginatedColumns, searchText] + ); + + const paginationProps = useMemo( + () => ({ + currentPage, + showPagination, + isLoading: columnsLoading, + isNumberBased: Boolean(searchText), + pageSize, + paging, + pagingHandler: handleColumnsPageChange, + onShowSizeChange: handlePageSizeChange, + }), + [ + currentPage, + showPagination, + columnsLoading, + searchText, + pageSize, + paging, + handleColumnsPageChange, + handlePageSizeChange, + ] + ); + const entityFqn = useMemo( () => currentVersionData.fullyQualifiedName ?? '', [currentVersionData.fullyQualifiedName] @@ -88,10 +188,10 @@ const TableVersion: React.FC = ({ ); const columns = useMemo(() => { - const colList = cloneDeep(currentVersionData.columns); + const colList = cloneDeep(tableColumns); return getColumnsDataWithVersionChanges(changeDescription, colList); - }, [currentVersionData, changeDescription]); + }, [tableColumns, changeDescription]); const handleTabChange = (activeKey: string) => { navigate( @@ -170,7 +270,10 @@ const TableVersion: React.FC = ({ columns={columns} deletedColumnConstraintDiffs={deletedColumnConstraintDiffs} deletedTableConstraintDiffs={deletedTableConstraintDiffs} + handelSearchCallback={handleSearchAction} + isLoading={columnsLoading} joins={currentVersionData.joins as ColumnJoins[]} + paginationProps={paginationProps} tableConstraints={currentVersionData.tableConstraints} /> @@ -221,6 +324,9 @@ const TableVersion: React.FC = ({ }, ], [ + columnsLoading, + handleSearchAction, + paginationProps, description, entityFqn, columns, @@ -233,6 +339,14 @@ const TableVersion: React.FC = ({ ] ); + // Fetch columns when search changes + useEffect(() => { + if (tableFqn && !isVersionLoading) { + // Reset to first page when search changes + fetchPaginatedColumns(1, searchText || undefined); + } + }, [isVersionLoading, tableFqn, searchText, fetchPaginatedColumns, pageSize]); + return ( <> {isVersionLoading ? ( diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.test.tsx index 01395c33fd6..09effb5d960 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Database/TableVersion/TableVersion.test.tsx @@ -56,6 +56,7 @@ jest.mock('react-router-dom', () => ({ useParams: jest.fn().mockReturnValue({ tab: 'tables', }), + useLocation: jest.fn().mockImplementation(() => ({ pathname: 'pathname' })), })); describe('TableVersion tests', () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.component.tsx index 12d48eefe6e..03768e5d53b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.component.tsx @@ -14,7 +14,7 @@ import { Tooltip } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { isEmpty, isUndefined } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { Key, useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { NO_DATA_PLACEHOLDER } from '../../../constants/constants'; import { TABLE_SCROLL_VALUE } from '../../../constants/Table.constants'; @@ -27,6 +27,7 @@ import { } from '../../../utils/EntityUtils'; import { getFilterTags } from '../../../utils/TableTags/TableTags.utils'; import { + getAllRowKeysByKeyName, getTableExpandableConfig, makeData, prepareConstraintIcon, @@ -41,19 +42,30 @@ import { VersionTableProps } from './VersionTable.interfaces'; function VersionTable({ columnName, columns, + isLoading, + paginationProps, joins, tableConstraints, addedColumnConstraintDiffs, deletedColumnConstraintDiffs, addedTableConstraintDiffs, deletedTableConstraintDiffs, + handelSearchCallback, }: Readonly>) { - const [searchedColumns, setSearchedColumns] = useState>([]); const { t } = useTranslation(); const [searchText, setSearchText] = useState(''); + const [expandedRowKeys, setExpandedRowKeys] = useState([]); - const data = useMemo(() => makeData(searchedColumns), [searchedColumns]); + const data = useMemo(() => { + if (!searchText) { + return makeData(columns); + } else { + const searchCols = searchInColumns(columns, searchText); + + return makeData(searchCols); + } + }, [searchText, columns]); const renderColumnName = useCallback( (name: T['name'], record: T) => { @@ -129,7 +141,6 @@ function VersionTable({ ); }, [ - columns, tableConstraints, addedColumnConstraintDiffs, deletedColumnConstraintDiffs, @@ -218,11 +229,12 @@ function VersionTable({ ), }, ], - [columnName, joins, data, renderColumnName] + [columnName, joins, renderColumnName] ); const handleSearchAction = (searchValue: string) => { setSearchText(searchValue); + handelSearchCallback?.(searchValue); }; const searchProps = useMemo( @@ -235,26 +247,28 @@ function VersionTable({ [searchText, handleSearchAction] ); + const handleExpandedRowsChange = useCallback((keys: readonly Key[]) => { + setExpandedRowKeys(keys as string[]); + }, []); + useEffect(() => { - if (!searchText) { - setSearchedColumns(columns); - } else { - const searchCols = searchInColumns(columns, searchText); - setSearchedColumns(searchCols); - } - }, [searchText, columns]); + setExpandedRowKeys(getAllRowKeysByKeyName(columns ?? [], 'name')); + }, [columns]); return ( (), - defaultExpandAllRows: true, + rowExpandable: (record) => !isEmpty(record.children), + onExpandedRowsChange: handleExpandedRowsChange, + expandedRowKeys: expandedRowKeys, }} - key={`${String(data)}`} // Necessary for working of the default auto expand all rows functionality. + loading={isLoading} locale={{ emptyText: , }} diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.interfaces.ts b/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.interfaces.ts index a4d2fd95683..aac512cef67 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.interfaces.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/Entity/VersionTable/VersionTable.interfaces.ts @@ -18,6 +18,8 @@ import { FieldChange, TableConstraint, } from '../../../generated/entity/data/table'; +import { Paging } from '../../../generated/type/paging'; +import { PagingHandlerParams } from '../../common/NextPrevious/NextPrevious.interface'; export interface VersionTableProps { columnName: string; @@ -29,4 +31,16 @@ export interface VersionTableProps { deletedTableConstraintDiffs?: FieldChange[]; constraintUpdatedColumns?: string[]; tableConstraints?: Array; + isLoading?: boolean; + paginationProps?: { + currentPage: number; + showPagination: boolean; + isLoading: boolean; + isNumberBased: boolean; + pageSize: number; + paging: Paging; + pagingHandler: ({ currentPage }: PagingHandlerParams) => void; + onShowSizeChange: (page: number) => void; + }; + handelSearchCallback?: (searchValue: string) => void; } diff --git a/openmetadata-ui/src/main/resources/ui/src/styles/components/toggle-switch.less b/openmetadata-ui/src/main/resources/ui/src/styles/components/toggle-switch.less index 0b1ca235cf8..6fa63b1a672 100644 --- a/openmetadata-ui/src/main/resources/ui/src/styles/components/toggle-switch.less +++ b/openmetadata-ui/src/main/resources/ui/src/styles/components/toggle-switch.less @@ -12,7 +12,7 @@ */ @import '../variables.less'; -@switch-bg-color-primary: rgb(107 114 128 / 15%); +@switch-bg-color-primary: rgba(107, 114, 128, 0.15); @switch-border-color: rgba(107, 114, 128, 1); .ant-switch {