diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts index 18e3e3694318..7393a60cc844 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/ColumnBulkOperations.spec.ts @@ -695,6 +695,59 @@ test.describe('Column Bulk Operations - Selection & Edit Drawer', () => { }); }); + test('should keep the tag select popup interactive inside the edit drawer', async ({ + page, + }) => { + await test.step('Search, select a column, and open the edit drawer', async () => { + await searchColumn(page, sharedColumnName); + const checkbox = getColumnRowCheckbox(page, sharedColumnName); + await expect(checkbox).toBeVisible(); + await checkbox.click(); + + const editButton = page.getByTestId('edit-button'); + await expect(editButton).toBeEnabled(); + await editButton.click(); + + await expect( + page.getByTestId('column-bulk-operations-form-drawer') + ).toBeVisible(); + }); + + await test.step('Open the tag select and verify its popup renders inside the drawer dialog', async () => { + const drawer = page.getByTestId('column-bulk-operations-form-drawer'); + + await drawer + .getByTestId('tags-field') + .locator('.ant-select-selector') + .click(); + + // Regression guard for the react-aria focus-scope bug: the Ant Select + // popup must render inside the drawer dialog (not document.body) so its + // options stay clickable and scrollable. + const dropdownOption = page + .locator( + '[role="dialog"] .async-select-list-dropdown .ant-select-item-option' + ) + .first(); + await expect(dropdownOption).toBeVisible(); + + await dropdownOption.click(); + + await expect( + page + .locator( + '[role="dialog"] .async-select-list-dropdown .ant-select-item-option-selected' + ) + .first() + ).toBeVisible(); + }); + + await test.step('Close the drawer', async () => { + await page.keyboard.press('Escape'); + await page.keyboard.press('Escape'); + }); + }); + test('should show column count for multiple column selection', async ({ page, }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts index bd615d97f861..acd02dfe7dba 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/DataProducts.spec.ts @@ -23,6 +23,7 @@ import { ClassificationClass } from '../../support/tag/ClassificationClass'; import { TagClass } from '../../support/tag/TagClass'; import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; +import { verifyDrawerAssetFilters } from '../../utils/assetSelection'; import { descriptionBox, redirectToHomePage } from '../../utils/common'; import { addAssetsToDataProduct, @@ -206,6 +207,55 @@ test.describe('Data Products', () => { }); }); + test('Data product add assets drawer quick filters are interactive', async ({ + page, + }) => { + const dataProduct = new DataProduct([domain]); + const table = new TableClass(); + + const { apiContext, afterAction } = await performAdminLogin( + page.context().browser()! + ); + await table.create(apiContext); + await table.patch({ + apiContext, + patchData: [ + { + op: 'add', + path: '/domains/0', + value: { + id: domain.responseData.id, + type: 'domain', + }, + }, + ], + }); + await dataProduct.create(apiContext); + + try { + await sidebarClick(page, SidebarItem.DATA_PRODUCT); + await waitForAllLoadersToDisappear(page); + await selectDataProduct(page, dataProduct.data); + + const assetRes = page.waitForResponse( + '/api/v1/search/query?q=&index=all&*' + ); + await page.getByTestId('data-product-details-add-button').click(); + await assetRes; + + await page + .getByTestId('asset-selection-modal') + .waitFor({ state: 'visible' }); + await waitForAllLoadersToDisappear(page); + + await verifyDrawerAssetFilters(page); + } finally { + await dataProduct.delete(apiContext); + await table.delete(apiContext); + await afterAction(); + } + }); + test('Search Data Products', async ({ page }) => { const dataProduct1 = new DataProduct([domain]); const dataProduct2 = new DataProduct([domain]); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts index 14d93795002e..f1e0dbed0c5d 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Domains.spec.ts @@ -32,6 +32,7 @@ import { TagClass } from '../../support/tag/TagClass'; import { TeamClass } from '../../support/team/TeamClass'; import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; +import { verifyDrawerAssetFilters } from '../../utils/assetSelection'; import { clickOutside, descriptionBox, @@ -242,6 +243,39 @@ test.describe('Domains', () => { await assetCleanup(); }); + test('Domain add assets drawer quick filters are interactive', async ({ + page, + }) => { + const { assetCleanup } = await setupAssetsForDomain(page); + const { apiContext, afterAction } = await getApiContext(page); + const filterDomain = new Domain(); + await filterDomain.create(apiContext); + + try { + await sidebarClick(page, SidebarItem.DOMAIN); + await selectDomain(page, filterDomain.data); + await waitForAllLoadersToDisappear(page); + + const assetRes = page.waitForResponse( + '/api/v1/search/query?q=&index=all&*' + ); + await page.getByTestId('domain-details-add-button').click(); + await page.getByRole('menuitem', { name: 'Assets', exact: true }).click(); + await assetRes; + + await page + .getByTestId('asset-selection-modal') + .waitFor({ state: 'visible' }); + await waitForAllLoadersToDisappear(page); + + await verifyDrawerAssetFilters(page); + } finally { + await filterDomain.delete(apiContext); + await assetCleanup(); + await afterAction(); + } + }); + test('Create DataProducts and add remove assets', async ({ page }) => { test.slow(true); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/InputOutputPorts.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/InputOutputPorts.spec.ts index 5b2013a21e7d..9ee4edd72783 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/InputOutputPorts.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/InputOutputPorts.spec.ts @@ -20,6 +20,7 @@ import { DashboardClass } from '../../support/entity/DashboardClass'; import { TableClass } from '../../support/entity/TableClass'; import { TopicClass } from '../../support/entity/TopicClass'; import { performAdminLogin } from '../../utils/admin'; +import { verifyDrawerAssetFilters } from '../../utils/assetSelection'; import { getApiContext, redirectToHomePage, @@ -634,7 +635,9 @@ test.describe('Input Output Ports', () => { }); }); - test('Port drawers show Entity Type quick filter', async ({ page }) => { + test('Port drawers allow selecting, scrolling and applying quick filters', async ({ + page, + }) => { const dataProduct = new DataProduct([domain]); await test.step('Create data product with assets', async () => { @@ -651,30 +654,26 @@ test.describe('Input Output Ports', () => { await navigateToPortsTab(page); }); - await test.step('Verify Entity Type filter in input port drawer', async () => { + await test.step('Verify quick filters in input port drawer', async () => { await page.getByTestId('add-input-port-button').click(); await page.getByTestId('asset-selection-modal').waitFor({ state: 'visible', }); await waitForAllLoadersToDisappear(page); - await expect( - page.getByTestId('search-dropdown-Entity Type') - ).toBeVisible(); + await verifyDrawerAssetFilters(page); await page.getByTestId('cancel-btn').click(); }); - await test.step('Verify Entity Type filter in output port drawer', async () => { + await test.step('Verify quick filters in output port drawer', async () => { await page.getByTestId('add-output-port-button').click(); await page.getByTestId('asset-selection-modal').waitFor({ state: 'visible', }); await waitForAllLoadersToDisappear(page); - await expect( - page.getByTestId('search-dropdown-Entity Type') - ).toBeVisible(); + await verifyDrawerAssetFilters(page); await page.getByTestId('cancel-btn').click(); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Tag.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Tag.spec.ts index 85a0d904dc41..da439335e1e5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Tag.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Tag.spec.ts @@ -21,8 +21,13 @@ import { TagClass } from '../../support/tag/TagClass'; import { TeamClass } from '../../support/team/TeamClass'; import { UserClass } from '../../support/user/UserClass'; import { performAdminLogin } from '../../utils/admin'; +import { applyAndClearFirstAssetFilterOption } from '../../utils/assetSelection'; import { getApiContext, redirectToHomePage, uuid } from '../../utils/common'; -import { addMultiOwner, removeOwner } from '../../utils/entity'; +import { + addMultiOwner, + removeOwner, + waitForAllLoadersToDisappear, +} from '../../utils/entity'; import { sidebarClick } from '../../utils/sidebar'; import { addAssetsToTag, @@ -216,6 +221,23 @@ test.describe('Tag Page with Admin Roles', () => { await addAssetsToTag(adminPage, assets, tag1); }); + await test.step('Verify add asset modal filters are interactive', async () => { + const initialFetch = adminPage.waitForResponse( + '/api/v1/search/query?q=&index=all&from=0&size=25&deleted=false**' + ); + await adminPage.getByTestId('data-classification-add-button').click(); + await initialFetch; + + await adminPage + .getByTestId('asset-selection-modal') + .waitFor({ state: 'visible' }); + await waitForAllLoadersToDisappear(adminPage); + + await applyAndClearFirstAssetFilterOption(adminPage, 'Entity Type'); + + await adminPage.getByTestId('cancel-btn').click(); + }); + await test.step('Verify EntityType Filter', async () => { await verifyEntityTypeFilterInTagAssets(adminPage, assets); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/assetSelection.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/assetSelection.ts new file mode 100644 index 000000000000..c6a3a4ebefd1 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/assetSelection.ts @@ -0,0 +1,173 @@ +/* + * 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'; +import { waitForAllLoadersToDisappear } from './entity'; + +/** + * Quick filters rendered inside the "Add Assets" dialog for the Domain / Data + * Product / Input-Output Port surfaces (DOMAIN_DATAPRODUCT_DROPDOWN_ITEMS). + */ +export const DOMAIN_DATA_PRODUCT_ASSET_FILTERS = [ + 'Entity Type', + 'Owners', + 'Tag', + 'Tier', + 'Service', + 'Service Type', +]; + +/** + * Opens a quick-filter dropdown inside the "Add Assets" dialog and waits for its + * options (or empty state) to settle. + */ +const openAssetFilterDropdown = async (page: Page, filterName: string) => { + await page.getByTestId(`search-dropdown-${filterName}`).click(); + + await page.getByTestId('drop-down-menu').waitFor(); + await waitForAllLoadersToDisappear(page); +}; + +/** + * With an "Add Assets" quick-filter dropdown already open, selects the first + * available option, applies it via Update and asserts the filter took effect (a + * query_filter search fires and the clear-filter affordance appears), then + * clears it. When the filter has no aggregation buckets in the current scope it + * simply closes — every drawer surface scopes assets differently, so not every + * filter has data to apply. + * + * The clear-filter affordance is only rendered when a quick filter is applied, + * so it is the reliable signal that the option was actually selectable — the + * functional regression guard for the non-interactive popup bug. + */ +const applyFirstOptionOrClose = async (page: Page) => { + const options = page.locator( + '[data-testid="drop-down-menu"] .ant-dropdown-menu-item' + ); + + if ((await options.count()) === 0) { + await page.getByTestId('close-btn').click(); + + return; + } + + const filterResponse = page.waitForResponse( + '/api/v1/search/query?*query_filter=*' + ); + + await options.first().click(); + await page.getByTestId('update-btn').click(); + + await filterResponse; + + const clearFilter = page.locator( + '.asset-filters-wrapper .text-primary.cursor-pointer' + ); + await expect(clearFilter).toBeVisible(); + + const clearResponse = page.waitForResponse('/api/v1/search/query?*'); + await clearFilter.click(); + await clearResponse; +}; + +/** + * Opens a quick-filter dropdown and applies + clears its first option. Works for + * both the modal and the drawer variants of the "Add Assets" dialog. + */ +export const applyAndClearFirstAssetFilterOption = async ( + page: Page, + filterName: string +) => { + await openAssetFilterDropdown(page, filterName); + await applyFirstOptionOrClose(page); +}; + +/** + * Drawer-only verification of a single quick filter: opens it, asserts the popup + * renders inside the drawer dialog (the structural regression guard — a popup + * portalled to document.body lands outside the react-aria focus scope and turns + * non-interactive), then applies + clears its first option. + */ +export const verifyAssetFilterInteractive = async ( + page: Page, + filterName: string +) => { + await openAssetFilterDropdown(page, filterName); + + await expect( + page.locator('[role="dialog"] [data-testid="drop-down-menu"]') + ).toBeVisible(); + + await applyFirstOptionOrClose(page); +}; + +/** + * Verifies the options list inside an "Add Assets" quick-filter dropdown can be + * scrolled with the mouse wheel. Directly targets the "cannot scroll" symptom: + * react-aria's usePreventScroll blocks wheel events outside the drawer's scroll + * region when the popup is portalled to document.body. + */ +export const verifyAssetFilterDropdownScrollable = async ( + page: Page, + filterName: string +) => { + await openAssetFilterDropdown(page, filterName); + + const optionsList = page.locator( + '[data-testid="drop-down-menu"] .ant-dropdown-menu' + ); + await optionsList.waitFor(); + + const isScrollable = await optionsList.evaluate( + (el) => el.scrollHeight > el.clientHeight + 5 + ); + + if (isScrollable) { + await optionsList.hover(); + await page.mouse.wheel(0, 240); + + await expect + .poll(async () => optionsList.evaluate((el) => el.scrollTop), { + timeout: 5000, + }) + .toBeGreaterThan(0); + } + + await page.getByTestId('close-btn').click(); +}; + +/** + * Full interactive verification of the "Add Assets" filter dropdowns for the + * Domain / Data Product / Port drawer surfaces. Assumes the drawer is already + * open. Asserts every filter is visible, that each filter's popup renders inside + * the drawer dialog and can be applied, and that the options list scrolls. + */ +export const verifyDrawerAssetFilters = async (page: Page) => { + await expect(page.locator('.asset-filters-wrapper')).toBeVisible(); + await expect( + page.locator('.asset-filters-wrapper .explore-quick-filters-container') + ).toBeVisible(); + + for (const filterName of DOMAIN_DATA_PRODUCT_ASSET_FILTERS) { + await expect( + page.getByTestId(`search-dropdown-${filterName}`) + ).toBeVisible(); + } + + await waitForAllLoadersToDisappear(page); + + await verifyAssetFilterDropdownScrollable(page, 'Entity Type'); + + for (const filterName of DOMAIN_DATA_PRODUCT_ASSET_FILTERS) { + await verifyAssetFilterInteractive(page, filterName); + } +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/DrawerPopupContainerProvider.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/DrawerPopupContainerProvider.tsx new file mode 100644 index 000000000000..6957cdc9d6c2 --- /dev/null +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/DrawerPopupContainerProvider.tsx @@ -0,0 +1,44 @@ +/* + * 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 { ConfigProvider } from 'antd'; +import { ReactNode } from 'react'; + +/** + * Resolves the popup container for an Ant Design overlay rendered inside a + * SlideoutMenu drawer. The drawer is a react-aria modal that contains focus and + * blocks pointer/scroll events outside its dialog subtree. Ant overlays portal + * to document.body by default, which lands outside that subtree and leaves the + * popup non-interactive (options can't be selected or scrolled). Returning the + * enclosing dialog keeps the popup within the focus scope. + */ +export const getDrawerPopupContainer = ( + triggerNode?: HTMLElement +): HTMLElement => + triggerNode?.closest('[role="dialog"]') ?? document.body; + +/** + * Sets {@link getDrawerPopupContainer} as the default `getPopupContainer` for + * every Ant Design overlay (Select, Dropdown, DatePicker, TreeSelect, Tooltip, + * ...) rendered within a drawer's content, so drawer-hosted overlays stay + * interactive without each call site remembering to wire it up. A component + * that passes its own `getPopupContainer` still takes precedence. + */ +export const DrawerPopupContainerProvider = ({ + children, +}: { + children: ReactNode; +}) => ( + + {children} + +); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/index.ts b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/index.ts index 80b8e3126e74..23de3d6d8357 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/index.ts +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/index.ts @@ -11,6 +11,7 @@ * limitations under the License. */ +export * from './DrawerPopupContainerProvider'; export * from './useCompositeDrawer'; export * from './useDrawer'; export * from './useDrawerBody'; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useCompositeDrawer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useCompositeDrawer.tsx index b7377ffcf508..f1021fdee1d3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useCompositeDrawer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useCompositeDrawer.tsx @@ -12,6 +12,7 @@ */ import { useCallback, useMemo } from 'react'; +import { DrawerPopupContainerProvider } from './DrawerPopupContainerProvider'; import { DrawerConfig, useDrawer } from './useDrawer'; import { DrawerBodyConfig, useDrawerBody } from './useDrawerBody'; import { DrawerFooterConfig, useDrawerFooter } from './useDrawerFooter'; @@ -53,11 +54,11 @@ export const useCompositeDrawer = (config: CompositeDrawerConfig = {}) => { const drawerContent = useMemo( () => ( - <> + {drawerHeader} {drawerBody} {drawerFooter} - + ), [drawerHeader, drawerBody, drawerFooter] ); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useDrawer.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useDrawer.tsx index 8959151ca932..b591ff4823df 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useDrawer.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/atoms/drawer/useDrawer.tsx @@ -13,6 +13,7 @@ import { SlideoutMenu } from '@openmetadata/ui-core-components'; import { ReactNode, useCallback, useMemo, useState } from 'react'; +import { DrawerPopupContainerProvider } from './DrawerPopupContainerProvider'; export interface DrawerConfig { width?: number | string; @@ -64,7 +65,9 @@ export const useDrawer = (config: DrawerConfig = {}) => { isOpen={open} width={config.width} onOpenChange={handleOpenChange}> - {config.children} + + {config.children} + ), [ diff --git a/openmetadata-ui/src/main/resources/ui/src/locale/languages/sv-se.json b/openmetadata-ui/src/main/resources/ui/src/locale/languages/sv-se.json index 7a57553f15f8..e1bbe137cf8a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/locale/languages/sv-se.json +++ b/openmetadata-ui/src/main/resources/ui/src/locale/languages/sv-se.json @@ -2288,6 +2288,7 @@ "test-entity": "Testa {{entity}}", "test-level-lowercase": "testnivå", "test-library": "Testbibliotek", + "test-login": "Test Login", "test-platform-plural": "Testplattformar", "test-plural": "Tester", "test-plural-type": "{{type}}-tester", @@ -3462,7 +3463,18 @@ "special-character-not-allowed": "Specialtecken är inte tillåtna.", "sql-query-tooltip": "Frågor som returnerar en eller flera rader leder till att testet misslyckas.", "sso-configuration-directly-from-the-ui": "SSO-konfiguration direkt från användargränssnittet", + "sso-configuration-test-failed-description": "Validation failed. Please review your configuration and try again.", + "sso-configuration-test-failed-with-count": "Validation failed with {{count}} error(s). Review the highlighted fields below and try again.", + "sso-configuration-test-success": "Your SSO configuration is valid and reachable. You can save it when you're ready.", + "sso-new-config-save-warning": "Saving a new SSO configuration will sign you out and redirect you to the login page. Test your configuration first to avoid being locked out.", "sso-provider-not-supported": "SSO-leverantören {{provider}} stöds inte.", + "sso-test-login-description": "Sign in with your identity provider using these unsaved settings. This runs in a separate window and never changes your current session.", + "sso-test-login-error": "The sign-in could not be validated due to a server error. Please try again.", + "sso-test-login-failed": "Sign-in completed, but this configuration would reject the login.", + "sso-test-login-no-token": "No token was returned from the identity provider.", + "sso-test-login-popup-failed": "The test sign-in was cancelled or could not be completed. Please try again.", + "sso-test-login-success": "You signed in successfully as {{email}}. These settings are safe to save.", + "sso-test-login-waiting": "Complete the sign-in in the popup window to continue.", "stage-file-location-message": "Tillfälligt filnamn för att lagra frågeloggarna före bearbetning. Absolut filsökväg krävs.", "star-on-github-description": "Gillar du {{brandName}}? Dina GitHub-stjärnor hjälper communityn att växa sig starkare! Stjärnmärk oss idag, sprid kärleken och låt oss bygga framtiden för metadata tillsammans! 🚀", "starting-offset-description": "Den initiala offseten för händelseprenumerationen när den började bearbeta.",