From 297981311110e3654a744d618b2d672b84e104da Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Wed, 15 Apr 2026 02:18:42 +0530 Subject: [PATCH 01/24] Fix bot name matching on the Bots page search --- .../Bot/BotListV1/BotListV1.component.tsx | 12 +--- .../resources/ui/src/utils/BotsUtils.test.tsx | 56 ++++++++++++++++++- .../main/resources/ui/src/utils/BotsUtils.tsx | 25 +++++++++ 3 files changed, 83 insertions(+), 10 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 3ff53b359ff2..2d2cf9fa2740 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -15,7 +15,7 @@ import Icon from '@ant-design/icons/lib/components/Icon'; import { Button, Col, Row, Space, Switch, Tooltip, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; -import { isEmpty, lowerCase } from 'lodash'; +import { isEmpty } from 'lodash'; import { useCallback, useEffect, useMemo, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; @@ -38,6 +38,7 @@ import { getEntityName, highlightSearchText, } from '../../../../utils/EntityUtils'; +import { filterBotsBySearchTerm } from '../../../../utils/BotsUtils'; import { getSettingPageEntityBreadCrumb } from '../../../../utils/GlobalSettingsUtils'; import { getBotsPath } from '../../../../utils/RouterUtils'; import { stringToHTML } from '../../../../utils/StringsUtils'; @@ -223,14 +224,7 @@ const BotListV1 = ({ const handleSearch = (text: string) => { setSearchTerm(text); if (text) { - const normalizeText = lowerCase(text); - const matchedData = botUsers.filter( - (bot) => - bot.name.includes(normalizeText) || - bot.displayName?.includes(normalizeText) || - bot.description?.includes(normalizeText) - ); - setSearchedData(matchedData); + setSearchedData(filterBotsBySearchTerm(botUsers, text)); } else { setSearchedData(botUsers); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx index 110d0afc5992..aae02169b3f1 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx @@ -12,7 +12,8 @@ */ import { render, screen } from '@testing-library/react'; -import { getJWTTokenExpiryOptions } from './BotsUtils'; +import { Bot } from '../generated/entity/bot'; +import { getJWTTokenExpiryOptions, filterBotsBySearchTerm } from './BotsUtils'; jest.mock('antd', () => ({ ...jest.requireActual('antd'), @@ -104,3 +105,56 @@ describe('getJWTTokenExpiryOptions', () => { ]); }); }); + +describe('filterBotsBySearchTerm', () => { + const createBot = (overrides: Partial = {}): Bot => + ({ + id: 'bot-id', + name: 'ingestion-bot@example.com', + botUser: { + id: 'bot-user-id', + type: 'user', + name: 'ingestion-bot@example.com', + displayName: 'Ingestion Bot User', + }, + displayName: 'Ingestion Bot', + description: 'Handles ingestion workflows', + ...overrides, + }) as Bot; + + it('matches bot display names case-insensitively', () => { + const bots = [createBot()]; + + expect(filterBotsBySearchTerm(bots, 'ingestion bot')).toHaveLength(1); + expect(filterBotsBySearchTerm(bots, 'Ingestion Bot')).toHaveLength(1); + }); + + it('matches bot identifiers containing email-style values', () => { + const bots = [createBot()]; + + expect(filterBotsBySearchTerm(bots, 'example.com')).toHaveLength(1); + expect(filterBotsBySearchTerm(bots, 'ingestion-bot@example.com')).toHaveLength( + 1 + ); + }); + + it('returns only bots matching the normalized search text', () => { + const bots = [ + createBot(), + createBot({ + id: 'second-bot-id', + name: 'quality-bot@example.com', + displayName: 'Quality Bot', + description: 'Profiles datasets', + botUser: { + id: 'quality-bot-user-id', + type: 'user', + name: 'quality-bot@example.com', + displayName: 'Quality Bot User', + }, + }), + ]; + + expect(filterBotsBySearchTerm(bots, 'quality bot')).toEqual([bots[1]]); + }); +}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx index 34e280af5372..886fd20e4249 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx @@ -12,7 +12,9 @@ */ import { Select } from 'antd'; +import { lowerCase } from 'lodash'; import { TOKEN_EXPIRY_NUMERIC_VALUES_IN_DAYS } from '../constants/User.constants'; +import { Bot } from '../generated/entity/bot'; import { JWTTokenExpiry } from '../generated/entity/teams/user'; import { DATE_TIME_WEEKDAY_WITH_ORDINAL, @@ -84,3 +86,26 @@ export const getTokenExpiry = (expiry: number) => { isTokenExpired, }; }; + +const normalizeBotSearchValue = (value?: string) => lowerCase(value ?? ''); + +export const filterBotsBySearchTerm = (bots: Bot[], searchText: string) => { + const normalizedSearchText = normalizeBotSearchValue(searchText); + + if (!normalizedSearchText) { + return bots; + } + + return bots.filter((bot) => + [ + bot.name, + bot.displayName, + bot.description, + bot.fullyQualifiedName, + bot.botUser?.name, + bot.botUser?.displayName, + ].some((value) => + normalizeBotSearchValue(value).includes(normalizedSearchText) + ) + ); +}; From a3d616522e23525c8b63ab0e193d4ad338a1bff9 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Wed, 15 Apr 2026 23:41:10 +0530 Subject: [PATCH 02/24] Fixes #26970: migrate Bots search to API-based name/email lookup with robust partial email matching --- .../ui/playwright/e2e/Pages/Bots.spec.ts | 5 + .../main/resources/ui/playwright/utils/bot.ts | 17 ++ .../Bot/BotListV1/BotListV1.component.tsx | 197 ++++++++++++++++-- .../src/main/resources/ui/src/rest/botsAPI.ts | 18 ++ .../resources/ui/src/utils/BotsUtils.test.tsx | 56 +---- .../main/resources/ui/src/utils/BotsUtils.tsx | 25 --- 6 files changed, 226 insertions(+), 92 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts index fe1a4b195c39..e926c92bcb35 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts @@ -19,6 +19,7 @@ import { tokenExpirationForDays, tokenExpirationUnlimitedDays, updateBotDetails, + verifyBotSearch, verifyGenerateTokenAPIContract, } from '../../utils/bot'; @@ -48,6 +49,10 @@ test.describe( await updateBotDetails(page); }); + await test.step('Verify bot search works by name and email', async () => { + await verifyBotSearch(page); + }); + await test.step('Verify generateToken API contract', async () => { await verifyGenerateTokenAPIContract(page); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 238f185221a8..1802906962dc 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -154,6 +154,23 @@ export const updateBotDetails = async (page: Page) => { ).toContainText(BOT_DETAILS.updatedDescription); }; +export const verifyBotSearch = async (page: Page) => { + const searchInput = page.getByTestId('searchbar'); + const createdBotLink = page.getByTestId( + `bot-link-${BOT_DETAILS.updatedBotName}` + ); + + await searchInput.fill(BOT_DETAILS.updatedBotName); + await expect(createdBotLink).toBeVisible(); + + await searchInput.clear(); + await searchInput.fill(BOT_DETAILS.botEmail); + await expect(createdBotLink).toBeVisible(); + + await searchInput.clear(); + await expect(createdBotLink).toBeVisible(); +}; + export const tokenExpirationForDays = async (page: Page) => { await getCreatedBot(page, { botName, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 2d2cf9fa2740..ac1646b7b50a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -27,21 +27,28 @@ import { PAGE_HEADERS } from '../../../../constants/PageHeaders.constant'; import { useLimitStore } from '../../../../context/LimitsProvider/useLimitsStore'; import { ERROR_PLACEHOLDER_TYPE } from '../../../../enums/common.enum'; import { EntityType } from '../../../../enums/entity.enum'; +import { SearchIndex } from '../../../../enums/search.enum'; import { Bot, ProviderType } from '../../../../generated/entity/bot'; +import { User } from '../../../../generated/entity/teams/user'; import { Include } from '../../../../generated/type/include'; import { Paging } from '../../../../generated/type/paging'; import LimitWrapper from '../../../../hoc/LimitWrapper'; import { useAuth } from '../../../../hooks/authHooks'; import { usePaging } from '../../../../hooks/paging/usePaging'; -import { getBots } from '../../../../rest/botsAPI'; +import { getBotByName, getBots } from '../../../../rest/botsAPI'; +import { searchQuery } from '../../../../rest/searchAPI'; +import { formatUsersResponse } from '../../../../utils/APIUtils'; import { getEntityName, highlightSearchText, } from '../../../../utils/EntityUtils'; -import { filterBotsBySearchTerm } from '../../../../utils/BotsUtils'; import { getSettingPageEntityBreadCrumb } from '../../../../utils/GlobalSettingsUtils'; import { getBotsPath } from '../../../../utils/RouterUtils'; -import { stringToHTML } from '../../../../utils/StringsUtils'; +import { getTermQuery } from '../../../../utils/SearchUtils'; +import { + escapeESReservedCharacters, + stringToHTML, +} from '../../../../utils/StringsUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; import DeleteWidgetModal from '../../../common/DeleteWidget/DeleteWidgetModal'; import ErrorPlaceHolder from '../../../common/ErrorWithPlaceholder/ErrorPlaceHolder'; @@ -81,11 +88,66 @@ const BotListV1 = ({ const [searchedData, setSearchedData] = useState([]); const [searchTerm, setSearchTerm] = useState(''); + const getBotIncludeFilter = useCallback( + () => (showDeleted ? Include.Deleted : Include.NonDeleted), + [showDeleted] + ); + const breadcrumbs: TitleBreadcrumbProps['titleLinks'] = useMemo( () => getSettingPageEntityBreadCrumb(GlobalSettingsMenuCategory.BOTS), [] ); + const enrichBotsWithBotUsers = async (bots: Bot[]) => { + if (!bots.length) { + return bots; + } + + try { + const response = await searchQuery({ + query: '', + pageNumber: 1, + pageSize: bots.length, + searchIndex: SearchIndex.USER, + queryFilter: { + bool: { + must: [{ term: { isBot: true } }], + should: bots.map((bot) => ({ + term: { name: bot.name }, + })), + minimum_should_match: 1, + }, + }, + }); + const botUsers = formatUsersResponse(response.hits.hits); + const botUsersByName = new Map( + botUsers.map((botUser) => [botUser.name, botUser]) + ); + + return bots.map((bot) => { + const botUser = botUsersByName.get(bot.name); + + if (!botUser) { + return bot; + } + + return { + ...bot, + botUser: { + ...(bot.botUser ?? {}), + id: botUser.id, + name: botUser.name, + displayName: botUser.displayName, + fullyQualifiedName: botUser.fullyQualifiedName, + email: botUser.email, + } as Bot['botUser'], + }; + }); + } catch { + return bots; + } + }; + /** * * @param after - Pagination value if passed data will be fetched post cursor value @@ -102,10 +164,12 @@ const BotListV1 = ({ limit: pageSize, include: showDeleted ? Include.Deleted : Include.NonDeleted, }); + const botsWithUsers = await enrichBotsWithBotUsers(data); + handlePagingChange(paging); - setBotUsers(data); - setSearchedData(data); - if (!showDeleted && isEmpty(data)) { + setBotUsers(botsWithUsers); + setSearchedData(botsWithUsers); + if (!showDeleted && isEmpty(botsWithUsers)) { setHandleErrorPlaceholder(true); } else { setHandleErrorPlaceholder(false); @@ -221,17 +285,126 @@ const BotListV1 = ({ fetchBots(showDeleted); }, [selectedUser]); - const handleSearch = (text: string) => { - setSearchTerm(text); - if (text) { - setSearchedData(filterBotsBySearchTerm(botUsers, text)); - } else { - setSearchedData(botUsers); + const searchBots = async (text: string) => { + const include = getBotIncludeFilter(); + const getMatchedBots = async (matchedBotUsers: User[]) => { + const matchedBotUserNames = Array.from( + new Set( + matchedBotUsers + .map((botUser) => botUser.name) + .filter((name): name is string => Boolean(name)) + ) + ); + + if (!matchedBotUserNames.length) { + return []; + } + + const matchedBotsResponse = await Promise.allSettled( + matchedBotUserNames.map((name) => + getBotByName(name, { + include, + }) + ) + ); + const matchedBotUsersByName = new Map( + matchedBotUsers.map((botUser) => [botUser.name, botUser]) + ); + + return matchedBotsResponse.flatMap((result) => { + if (result.status !== 'fulfilled') { + return []; + } + + const botUserName = result.value.botUser?.name; + const matchedBotUser = botUserName + ? matchedBotUsersByName.get(botUserName) + : undefined; + + if (!matchedBotUser) { + return [result.value]; + } + + return [ + { + ...result.value, + botUser: { + ...(result.value.botUser ?? {}), + id: matchedBotUser.id, + name: matchedBotUser.name, + displayName: matchedBotUser.displayName, + fullyQualifiedName: matchedBotUser.fullyQualifiedName, + email: matchedBotUser.email, + } as Bot['botUser'], + }, + ]; + }); + }; + + const matchedUsersBySearchQuery = await searchQuery({ + query: text, + pageNumber: 1, + pageSize: 100, + queryFilter: getTermQuery({ isBot: 'true' }), + searchIndex: SearchIndex.USER, + }); + const matchedBotUsers = formatUsersResponse( + matchedUsersBySearchQuery.hits.hits + ); + const matchedBots = await getMatchedBots(matchedBotUsers); + + if (matchedBots.length) { + return matchedBots; } + + const escapedText = escapeESReservedCharacters(text.toLowerCase()); + const wildcardPattern = `*${escapedText}*`; + const matchedUsersByWildcardFilter = await searchQuery({ + query: '*', + pageNumber: 1, + pageSize: 100, + queryFilter: getTermQuery({ isBot: 'true' }, 'must', undefined, { + wildcardShouldQueries: { + 'name.keyword': wildcardPattern, + 'displayName.keyword': wildcardPattern, + 'fullyQualifiedName.keyword': wildcardPattern, + 'email.keyword': wildcardPattern, + }, + }), + searchIndex: SearchIndex.USER, + }); + const fallbackMatchedBotUsers = formatUsersResponse( + matchedUsersByWildcardFilter.hits.hits + ); + + return getMatchedBots(fallbackMatchedBotUsers); + }; + + const handleSearch = async (text: string) => { + setSearchTerm(text); + handlePageChange(INITIAL_PAGING_VALUE, { cursorType: null, cursorValue: undefined, }); + + if (!text) { + setSearchedData(botUsers); + + return; + } + + try { + setLoading(true); + const matchedBots = await searchBots(text); + + setSearchedData(matchedBots); + } catch (error) { + showErrorToast((error as AxiosError).message); + setSearchedData([]); + } finally { + setLoading(false); + } }; const handleShowDeletedBots = (checked: boolean) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts index aeb271b9e696..afb664c029bf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts @@ -27,6 +27,10 @@ interface GetBotParams { include?: Include; } +interface GetBotByNameParams { + include?: Include; +} + export const getBots = async (params: GetBotParams) => { const response = await axiosClient.get<{ data: Bot[]; paging: Paging }>( BASE_URL, @@ -38,6 +42,20 @@ export const getBots = async (params: GetBotParams) => { return response.data; }; +export const getBotByName = async ( + name: string, + params: GetBotByNameParams = {} +) => { + const response = await axiosClient.get( + `${BASE_URL}/name/${encodeURIComponent(name)}`, + { + params, + } + ); + + return response.data; +}; + export const createBot = async (data: CreateBot) => { const response = await axiosClient.post>( BASE_URL, diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx index aae02169b3f1..110d0afc5992 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.test.tsx @@ -12,8 +12,7 @@ */ import { render, screen } from '@testing-library/react'; -import { Bot } from '../generated/entity/bot'; -import { getJWTTokenExpiryOptions, filterBotsBySearchTerm } from './BotsUtils'; +import { getJWTTokenExpiryOptions } from './BotsUtils'; jest.mock('antd', () => ({ ...jest.requireActual('antd'), @@ -105,56 +104,3 @@ describe('getJWTTokenExpiryOptions', () => { ]); }); }); - -describe('filterBotsBySearchTerm', () => { - const createBot = (overrides: Partial = {}): Bot => - ({ - id: 'bot-id', - name: 'ingestion-bot@example.com', - botUser: { - id: 'bot-user-id', - type: 'user', - name: 'ingestion-bot@example.com', - displayName: 'Ingestion Bot User', - }, - displayName: 'Ingestion Bot', - description: 'Handles ingestion workflows', - ...overrides, - }) as Bot; - - it('matches bot display names case-insensitively', () => { - const bots = [createBot()]; - - expect(filterBotsBySearchTerm(bots, 'ingestion bot')).toHaveLength(1); - expect(filterBotsBySearchTerm(bots, 'Ingestion Bot')).toHaveLength(1); - }); - - it('matches bot identifiers containing email-style values', () => { - const bots = [createBot()]; - - expect(filterBotsBySearchTerm(bots, 'example.com')).toHaveLength(1); - expect(filterBotsBySearchTerm(bots, 'ingestion-bot@example.com')).toHaveLength( - 1 - ); - }); - - it('returns only bots matching the normalized search text', () => { - const bots = [ - createBot(), - createBot({ - id: 'second-bot-id', - name: 'quality-bot@example.com', - displayName: 'Quality Bot', - description: 'Profiles datasets', - botUser: { - id: 'quality-bot-user-id', - type: 'user', - name: 'quality-bot@example.com', - displayName: 'Quality Bot User', - }, - }), - ]; - - expect(filterBotsBySearchTerm(bots, 'quality bot')).toEqual([bots[1]]); - }); -}); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx index 886fd20e4249..34e280af5372 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/utils/BotsUtils.tsx @@ -12,9 +12,7 @@ */ import { Select } from 'antd'; -import { lowerCase } from 'lodash'; import { TOKEN_EXPIRY_NUMERIC_VALUES_IN_DAYS } from '../constants/User.constants'; -import { Bot } from '../generated/entity/bot'; import { JWTTokenExpiry } from '../generated/entity/teams/user'; import { DATE_TIME_WEEKDAY_WITH_ORDINAL, @@ -86,26 +84,3 @@ export const getTokenExpiry = (expiry: number) => { isTokenExpired, }; }; - -const normalizeBotSearchValue = (value?: string) => lowerCase(value ?? ''); - -export const filterBotsBySearchTerm = (bots: Bot[], searchText: string) => { - const normalizedSearchText = normalizeBotSearchValue(searchText); - - if (!normalizedSearchText) { - return bots; - } - - return bots.filter((bot) => - [ - bot.name, - bot.displayName, - bot.description, - bot.fullyQualifiedName, - bot.botUser?.name, - bot.botUser?.displayName, - ].some((value) => - normalizeBotSearchValue(value).includes(normalizedSearchText) - ) - ); -}; From 925d005eb247a73099c06beaf13cddb86241d0ad Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 17 Apr 2026 01:18:38 +0530 Subject: [PATCH 03/24] Fix flaky Playwright setup by making Table/User creation idempotent and hardening glossary/tag and cleanup flows --- .../e2e/Features/CustomizeDetailPage.spec.ts | 12 +- .../ui/playwright/e2e/Flow/Tour.spec.ts | 7 +- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 18 ++- .../e2e/Pages/Lineage/LineageFilters.spec.ts | 12 ++ .../VersionPages/EntityVersionPages.spec.ts | 3 + .../playwright/support/entity/TableClass.ts | 21 ++- .../ui/playwright/support/user/UserClass.ts | 37 ++++- .../ui/playwright/utils/searchRBAC.ts | 17 ++- .../resources/ui/playwright/utils/user.ts | 37 +++-- .../Bot/BotListV1/BotListV1.component.tsx | 130 +++++++++++------- 10 files changed, 213 insertions(+), 81 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index dcb6a6b37840..71ca9d1d1ed8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -451,10 +451,16 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .getByTestId('add-widget-button'); await addWidgetButton.waitFor({ state: 'visible' }); await expect(addWidgetButton).toBeEnabled(); + await addWidgetButton.scrollIntoViewIfNeeded(); + await addWidgetButton.click({ trial: true }); await addWidgetButton.click(); - await adminPage - .getByTestId('widget-info-tabs') - .waitFor({ state: 'visible' }); + + await expect(adminPage.getByTestId('add-widget-modal')).toBeVisible({ + timeout: 60000, + }); + await expect(adminPage.getByTestId('widget-info-tabs')).toBeVisible({ + timeout: 60000, + }); await adminPage .getByTestId('add-widget-modal') diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts index b4283d5318b0..6d78019a9ec5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts @@ -25,6 +25,11 @@ const waitForTourBadgeWithRetry = async ( ) => { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { + await page.waitForURL('**/tour', { timeout: 60000 }); + await page.locator('#feedWidgetData').waitFor({ + state: 'visible', + timeout: 60000, + }); await page.locator('[data-tour-elem="badge"]').waitFor({ state: 'visible', timeout, @@ -221,7 +226,7 @@ test.describe( await page.locator('#feedWidgetData').waitFor(); // Since the tour steps are already tested in the first test, // here we only validate whether the tour is loading or not. - await waitForTourBadgeWithRetry(page); + await waitForTourBadgeWithRetry(page, 3, 40000); }); } ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 8c0e408eb900..f5ab2bfb717e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -577,9 +577,19 @@ test.describe('Glossary tests', () => { await patchRequest; // Add non mutually exclusive tags - await page.click( - '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="add-tag"]' - ); + await waitForAllLoadersToDisappear(page); + const addGlossaryBtn = page + .getByTestId('KnowledgePanel.GlossaryTerms') + .getByTestId('glossary-container') + .getByTestId('add-tag'); + + await addGlossaryBtn.waitFor({ + state: 'visible', + timeout: 60000, + }); + await expect(addGlossaryBtn).toBeEnabled(); + await addGlossaryBtn.scrollIntoViewIfNeeded(); + await addGlossaryBtn.click(); // Select 1st term await page.click('[data-testid="tag-selector"] #tagsForm_tags'); @@ -1794,7 +1804,7 @@ test.describe('Glossary tests', () => { test('Check for duplicate Glossary Term with Glossary having dot in name', async ({ browser, }) => { - const { page, afterAction, apiContext } = await performAdminLogin(browser); + const { page, apiContext } = await performAdminLogin(browser); const glossary1 = new Glossary(); const glossaryTerm1 = new GlossaryTerm( glossary1, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts index 2bc1bdcd7085..e9ad4716051f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts @@ -124,6 +124,18 @@ test.describe('Lineage Filters', () => { await afterAction(); }); + test.afterAll(async ({ browser }) => { + const { apiContext, afterAction } = await getDefaultAdminAPIContext( + browser + ); + + await Promise.allSettled( + entities.map((entity) => entity.delete(apiContext)) + ); + await Promise.allSettled([lineageEntity.delete(apiContext)]); + await afterAction(); + }); + test.beforeEach(async ({ page }) => { await lineageEntity.visitEntityPage(page); await visitLineageTab(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts index d2bd1933d4e9..4318cab02888 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts @@ -143,6 +143,9 @@ test.describe('Entity Version pages', () => { test.slow(); const { apiContext, afterAction } = await performAdminLogin(browser); + await Promise.allSettled( + entities.map((entity) => entity.delete(apiContext)) + ); await adminUser.delete(apiContext); await afterAction(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts index 880389aab1a5..5b722c1eee42 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/entity/TableClass.ts @@ -230,18 +230,35 @@ export class TableClass extends EntityClass { } async create(apiContext: APIRequestContext) { + let service; const serviceResponse = await apiContext.post( '/api/v1/services/databaseServices', { data: this.service, } ); - if (!serviceResponse.ok()) { + + if (serviceResponse.status() === 409) { + const existingServiceResponse = await apiContext.get( + `/api/v1/services/databaseServices/name/${encodeURIComponent( + this.service.name + )}` + ); + + if (!existingServiceResponse.ok()) { + throw new Error( + `TableClass: service exists but fetch failed (${existingServiceResponse.status()}): ${await existingServiceResponse.text()}` + ); + } + + service = await existingServiceResponse.json(); + } else if (!serviceResponse.ok()) { throw new Error( `TableClass: service create failed (${serviceResponse.status()}): ${await serviceResponse.text()}` ); + } else { + service = await serviceResponse.json(); } - const service = await serviceResponse.json(); const databaseResponse = await apiContext.post('/api/v1/databases', { data: { ...this.database, service: service.fullyQualifiedName }, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index 42c5a745bec7..d4a161888281 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -55,12 +55,41 @@ export class UserClass { }); if (!response.ok()) { - throw new Error( - `UserClass.create() failed with status ${response.status()}: ${await response.text()}` - ); + const responseText = await response.text(); + + if ( + response.status() === 400 && + responseText.includes('User with Email Already Exists') + ) { + const existingUserResponse = await apiContext.get( + `/api/v1/users?email=${encodeURIComponent(this.data.email)}&limit=1` + ); + + if (!existingUserResponse.ok()) { + throw new Error( + `UserClass.create() user exists but fetch failed (${existingUserResponse.status()}): ${await existingUserResponse.text()}` + ); + } + + const existingUsersData = await existingUserResponse.json(); + const existingUser = existingUsersData?.data?.[0]; + + if (!existingUser) { + throw new Error( + `UserClass.create() user exists but no user found for email ${this.data.email}` + ); + } + + this.responseData = existingUser; + } else { + throw new Error( + `UserClass.create() failed with status ${response.status()}: ${responseText}` + ); + } + } else { + this.responseData = await response.json(); } - this.responseData = await response.json(); if (assignRole) { const { entity } = await this.patch({ apiContext, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts index af7d3c8d93db..0bd8d6d4d576 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts @@ -88,17 +88,26 @@ export const searchForEntityShouldWorkShowNoResult = async ( await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); - await page.getByTestId('searchBox').press('Enter'); - - await page.waitForResponse(`api/v1/search/query?**`); + await Promise.all([ + page.waitForResponse( + (response) => + response.url().includes('/api/v1/search/query') && + response.status() === 200 + ), + page.getByTestId('searchBox').press('Enter'), + ]); await waitForAllLoadersToDisappear(page); + await expect( + page.getByTestId('no-search-results').getByText('No result found.') + ).toBeVisible({ timeout: 30000 }); + await expect( page.locator('[data-testid="entity-header-display-name"]', { hasText: displayName, }) - ).not.toBeAttached(); + ).toHaveCount(0); await expect( page.getByTestId('no-search-results').getByText('No result found.') diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index ecd114126fa2..b80210f60ad6 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -107,12 +107,6 @@ export const visitUserProfilePage = async (page: Page, userName: string) => { const userResponse = page.waitForResponse( '/api/v1/search/query?q=*&index=*&from=0&size=*' ); - const loaderPromise = page - .getByTestId('user-list-v1-component') - .getByTestId('loader') - .waitFor({ - state: 'detached', - }); const searchBar = page.getByTestId('searchbar'); await expect @@ -122,12 +116,18 @@ export const visitUserProfilePage = async (page: Page, userName: string) => { await searchBar.fill(''); await searchBar.fill(userName); await searchRequest; - await loaderPromise.catch(() => undefined); + await page + .getByTestId('user-list-v1-component') + .getByTestId('loader') + .waitFor({ + state: 'detached', + }) + .catch(() => undefined); return await page.getByTestId(userName).count(); }, { - timeout: 60000, + timeout: 120000, intervals: [1000, 2000, 5000], message: `Timed out waiting for user ${userName} to become visible in the user list`, } @@ -503,7 +503,17 @@ export const generateToken = async (page: Page) => { }; export const revokeToken = async (page: Page, isBot?: boolean) => { - await page.click('[data-testid="revoke-button"]'); + const revokeButton = page + .locator('[data-testid="center-panel"] [data-testid="revoke-button"]') + .first(); + + await revokeButton.waitFor({ + state: 'visible', + timeout: 60000, + }); + await expect(revokeButton).toBeEnabled(); + await revokeButton.scrollIntoViewIfNeeded(); + await revokeButton.click(); await expect(page.locator('[data-testid="body-text"]')).toContainText( `Are you sure you want to revoke access for ${ @@ -511,9 +521,16 @@ export const revokeToken = async (page: Page, isBot?: boolean) => { }?` ); + const revokeResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/users/security/token') && + ['DELETE', 'POST'].includes(response.request().method()) + ); + await page.click('[data-testid="save-button"]'); + await revokeResponse.catch(() => undefined); - await expect(page.locator('[data-testid="revoke-button"]')).not.toBeVisible(); + await expect(revokeButton).toBeHidden({ timeout: 60000 }); }; export const updateExpiration = async (page: Page, expiry: number) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index ac1646b7b50a..366f06fb1adf 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -16,7 +16,7 @@ import { Button, Col, Row, Space, Switch, Tooltip, Typography } from 'antd'; import { ColumnsType } from 'antd/lib/table'; import { AxiosError } from 'axios'; import { isEmpty } from 'lodash'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { useCallback, useEffect, useMemo, useRef, useState } from 'react'; import { useTranslation } from 'react-i18next'; import { Link } from 'react-router-dom'; import { ReactComponent as IconDelete } from '../../../../assets/svg/ic-delete.svg'; @@ -35,7 +35,7 @@ import { Paging } from '../../../../generated/type/paging'; import LimitWrapper from '../../../../hoc/LimitWrapper'; import { useAuth } from '../../../../hooks/authHooks'; import { usePaging } from '../../../../hooks/paging/usePaging'; -import { getBotByName, getBots } from '../../../../rest/botsAPI'; +import { getBots } from '../../../../rest/botsAPI'; import { searchQuery } from '../../../../rest/searchAPI'; import { formatUsersResponse } from '../../../../utils/APIUtils'; import { @@ -87,6 +87,7 @@ const BotListV1 = ({ const [handleErrorPlaceholder, setHandleErrorPlaceholder] = useState(false); const [searchedData, setSearchedData] = useState([]); const [searchTerm, setSearchTerm] = useState(''); + const latestSearchRequest = useRef(0); const getBotIncludeFilter = useCallback( () => (showDeleted ? Include.Deleted : Include.NonDeleted), @@ -98,6 +99,24 @@ const BotListV1 = ({ [] ); + const enrichBotWithMatchedUser = useCallback((bot: Bot, botUser?: User) => { + if (!botUser) { + return bot; + } + + return { + ...bot, + botUser: { + ...(bot.botUser ?? {}), + id: botUser.id, + name: botUser.name, + displayName: botUser.displayName, + fullyQualifiedName: botUser.fullyQualifiedName, + email: botUser.email, + } as Bot['botUser'], + }; + }, []); + const enrichBotsWithBotUsers = async (bots: Bot[]) => { if (!bots.length) { return bots; @@ -124,28 +143,40 @@ const BotListV1 = ({ botUsers.map((botUser) => [botUser.name, botUser]) ); - return bots.map((bot) => { - const botUser = botUsersByName.get(bot.name); + return bots.map((bot) => + enrichBotWithMatchedUser(bot, botUsersByName.get(bot.name)) + ); + } catch { + return bots; + } + }; - if (!botUser) { - return bot; + const getBotsByBotUserNames = async (botUserNames: string[]) => { + const include = getBotIncludeFilter(); + const remainingBotNames = new Set(botUserNames); + const botsByBotUserName = new Map(); + let after: string | undefined; + + do { + const { data, paging } = await getBots({ + after, + include, + limit: 100, + }); + + data.forEach((bot) => { + if (!remainingBotNames.has(bot.name)) { + return; } - return { - ...bot, - botUser: { - ...(bot.botUser ?? {}), - id: botUser.id, - name: botUser.name, - displayName: botUser.displayName, - fullyQualifiedName: botUser.fullyQualifiedName, - email: botUser.email, - } as Bot['botUser'], - }; + botsByBotUserName.set(bot.name, bot); + remainingBotNames.delete(bot.name); }); - } catch { - return bots; - } + + after = paging.after; + } while (after && remainingBotNames.size > 0); + + return botsByBotUserName; }; /** @@ -286,7 +317,6 @@ const BotListV1 = ({ }, [selectedUser]); const searchBots = async (text: string) => { - const include = getBotIncludeFilter(); const getMatchedBots = async (matchedBotUsers: User[]) => { const matchedBotUserNames = Array.from( new Set( @@ -300,43 +330,25 @@ const BotListV1 = ({ return []; } - const matchedBotsResponse = await Promise.allSettled( - matchedBotUserNames.map((name) => - getBotByName(name, { - include, - }) - ) + const botsByBotUserName = await getBotsByBotUserNames( + matchedBotUserNames ); const matchedBotUsersByName = new Map( matchedBotUsers.map((botUser) => [botUser.name, botUser]) ); - return matchedBotsResponse.flatMap((result) => { - if (result.status !== 'fulfilled') { - return []; - } + return matchedBotUserNames.flatMap((botUserName) => { + const matchedBot = botsByBotUserName.get(botUserName); - const botUserName = result.value.botUser?.name; - const matchedBotUser = botUserName - ? matchedBotUsersByName.get(botUserName) - : undefined; - - if (!matchedBotUser) { - return [result.value]; + if (!matchedBot) { + return []; } return [ - { - ...result.value, - botUser: { - ...(result.value.botUser ?? {}), - id: matchedBotUser.id, - name: matchedBotUser.name, - displayName: matchedBotUser.displayName, - fullyQualifiedName: matchedBotUser.fullyQualifiedName, - email: matchedBotUser.email, - } as Bot['botUser'], - }, + enrichBotWithMatchedUser( + matchedBot, + matchedBotUsersByName.get(botUserName) + ), ]; }); }; @@ -345,7 +357,7 @@ const BotListV1 = ({ query: text, pageNumber: 1, pageSize: 100, - queryFilter: getTermQuery({ isBot: 'true' }), + queryFilter: getTermQuery({ isBot: true }), searchIndex: SearchIndex.USER, }); const matchedBotUsers = formatUsersResponse( @@ -357,13 +369,13 @@ const BotListV1 = ({ return matchedBots; } - const escapedText = escapeESReservedCharacters(text.toLowerCase()); + const escapedText = escapeESReservedCharacters(text); const wildcardPattern = `*${escapedText}*`; const matchedUsersByWildcardFilter = await searchQuery({ query: '*', pageNumber: 1, pageSize: 100, - queryFilter: getTermQuery({ isBot: 'true' }, 'must', undefined, { + queryFilter: getTermQuery({ isBot: true }, 'must', undefined, { wildcardShouldQueries: { 'name.keyword': wildcardPattern, 'displayName.keyword': wildcardPattern, @@ -381,6 +393,7 @@ const BotListV1 = ({ }; const handleSearch = async (text: string) => { + const searchRequestId = ++latestSearchRequest.current; setSearchTerm(text); handlePageChange(INITIAL_PAGING_VALUE, { @@ -390,6 +403,7 @@ const BotListV1 = ({ if (!text) { setSearchedData(botUsers); + setLoading(false); return; } @@ -398,12 +412,22 @@ const BotListV1 = ({ setLoading(true); const matchedBots = await searchBots(text); + if (searchRequestId !== latestSearchRequest.current) { + return; + } + setSearchedData(matchedBots); } catch (error) { + if (searchRequestId !== latestSearchRequest.current) { + return; + } + showErrorToast((error as AxiosError).message); setSearchedData([]); } finally { - setLoading(false); + if (searchRequestId === latestSearchRequest.current) { + setLoading(false); + } } }; From 9c0d08539ea687c7ca6ce2ed7d297cdba0e3306e Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 17 Apr 2026 01:24:51 +0530 Subject: [PATCH 04/24] Fixes #26970: avoid full bot scan by switching search result resolution to direct getBotByName lookups --- .../Bot/BotListV1/BotListV1.component.tsx | 34 ++++++++----------- 1 file changed, 14 insertions(+), 20 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 366f06fb1adf..ea1ce43191b8 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -35,7 +35,7 @@ import { Paging } from '../../../../generated/type/paging'; import LimitWrapper from '../../../../hoc/LimitWrapper'; import { useAuth } from '../../../../hooks/authHooks'; import { usePaging } from '../../../../hooks/paging/usePaging'; -import { getBots } from '../../../../rest/botsAPI'; +import { getBotByName, getBots } from '../../../../rest/botsAPI'; import { searchQuery } from '../../../../rest/searchAPI'; import { formatUsersResponse } from '../../../../utils/APIUtils'; import { @@ -153,28 +153,22 @@ const BotListV1 = ({ const getBotsByBotUserNames = async (botUserNames: string[]) => { const include = getBotIncludeFilter(); - const remainingBotNames = new Set(botUserNames); const botsByBotUserName = new Map(); - let after: string | undefined; - - do { - const { data, paging } = await getBots({ - after, - include, - limit: 100, - }); - - data.forEach((bot) => { - if (!remainingBotNames.has(bot.name)) { - return; - } + const matchedBotsResponse = await Promise.allSettled( + botUserNames.map((name) => + getBotByName(name, { + include, + }) + ) + ); - botsByBotUserName.set(bot.name, bot); - remainingBotNames.delete(bot.name); - }); + matchedBotsResponse.forEach((response) => { + if (response.status !== 'fulfilled') { + return; + } - after = paging.after; - } while (after && remainingBotNames.size > 0); + botsByBotUserName.set(response.value.name, response.value); + }); return botsByBotUserName; }; From 339804fdf19cefbea0cdb4585554d2923506572f Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 17 Apr 2026 01:27:28 +0530 Subject: [PATCH 05/24] Fixes #26970: align bot user search with deleted toggle and tighten tour retry timeout handling --- .../ui/playwright/e2e/Flow/Tour.spec.ts | 17 ++++++++++++++--- .../Bot/BotListV1/BotListV1.component.tsx | 6 +++++- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts index 6d78019a9ec5..99b85c673e26 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts @@ -23,16 +23,27 @@ const waitForTourBadgeWithRetry = async ( maxAttempts = 3, timeout = 20000 ) => { + const startedAt = Date.now(); + const getRemainingTimeout = () => { + const remainingTimeout = timeout - (Date.now() - startedAt); + + if (remainingTimeout <= 0) { + throw new Error(`Timed out waiting for tour badge after ${timeout}ms.`); + } + + return remainingTimeout; + }; + for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - await page.waitForURL('**/tour', { timeout: 60000 }); + await page.waitForURL('**/tour', { timeout: getRemainingTimeout() }); await page.locator('#feedWidgetData').waitFor({ state: 'visible', - timeout: 60000, + timeout: getRemainingTimeout(), }); await page.locator('[data-tour-elem="badge"]').waitFor({ state: 'visible', - timeout, + timeout: getRemainingTimeout(), }); return; // Success diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index ea1ce43191b8..e565774b9dd2 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -35,8 +35,9 @@ import { Paging } from '../../../../generated/type/paging'; import LimitWrapper from '../../../../hoc/LimitWrapper'; import { useAuth } from '../../../../hooks/authHooks'; import { usePaging } from '../../../../hooks/paging/usePaging'; -import { getBotByName, getBots } from '../../../../rest/botsAPI'; +import { getBots } from '../../../../rest/botsAPI'; import { searchQuery } from '../../../../rest/searchAPI'; +import { getBotByName } from '../../../../rest/userAPI'; import { formatUsersResponse } from '../../../../utils/APIUtils'; import { getEntityName, @@ -127,6 +128,7 @@ const BotListV1 = ({ query: '', pageNumber: 1, pageSize: bots.length, + includeDeleted: showDeleted, searchIndex: SearchIndex.USER, queryFilter: { bool: { @@ -351,6 +353,7 @@ const BotListV1 = ({ query: text, pageNumber: 1, pageSize: 100, + includeDeleted: showDeleted, queryFilter: getTermQuery({ isBot: true }), searchIndex: SearchIndex.USER, }); @@ -369,6 +372,7 @@ const BotListV1 = ({ query: '*', pageNumber: 1, pageSize: 100, + includeDeleted: showDeleted, queryFilter: getTermQuery({ isBot: true }, 'must', undefined, { wildcardShouldQueries: { 'name.keyword': wildcardPattern, From 324c3d68e5e19c53062c1a254c1f7e7055022f9d Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 17 Apr 2026 10:25:53 +0530 Subject: [PATCH 06/24] Fixes #26970: keep bot search API-driven, align wildcard matching with name/email expectations, and revert unrelated Playwright changes --- .../e2e/Features/CustomizeDetailPage.spec.ts | 10 +------ .../ui/playwright/e2e/Flow/Tour.spec.ts | 17 ++--------- .../main/resources/ui/playwright/utils/bot.ts | 13 +++++++-- .../ui/playwright/utils/searchRBAC.ts | 9 +----- .../resources/ui/playwright/utils/user.ts | 2 +- .../Bot/BotListV1/BotListV1.component.tsx | 28 +++++++++---------- .../src/main/resources/ui/src/rest/botsAPI.ts | 18 ------------ 7 files changed, 30 insertions(+), 67 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts index f519864e6545..a33d551fefe1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Features/CustomizeDetailPage.spec.ts @@ -463,16 +463,8 @@ test.describe('Persona customization', PLAYWRIGHT_BASIC_TEST_TAG_OBJ, () => { .getByTestId('add-widget-button'); await expect(addWidgetButton).toBeVisible(); await expect(addWidgetButton).toBeEnabled(); - await addWidgetButton.scrollIntoViewIfNeeded(); - await addWidgetButton.click({ trial: true }); await addWidgetButton.click(); - - await expect(adminPage.getByTestId('add-widget-modal')).toBeVisible({ - timeout: 60000, - }); - await expect(adminPage.getByTestId('widget-info-tabs')).toBeVisible({ - timeout: 60000, - }); + await expect(adminPage.getByTestId('widget-info-tabs')).toBeVisible(); await adminPage .getByTestId('add-widget-modal') diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts index 99b85c673e26..6d78019a9ec5 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts @@ -23,27 +23,16 @@ const waitForTourBadgeWithRetry = async ( maxAttempts = 3, timeout = 20000 ) => { - const startedAt = Date.now(); - const getRemainingTimeout = () => { - const remainingTimeout = timeout - (Date.now() - startedAt); - - if (remainingTimeout <= 0) { - throw new Error(`Timed out waiting for tour badge after ${timeout}ms.`); - } - - return remainingTimeout; - }; - for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - await page.waitForURL('**/tour', { timeout: getRemainingTimeout() }); + await page.waitForURL('**/tour', { timeout: 60000 }); await page.locator('#feedWidgetData').waitFor({ state: 'visible', - timeout: getRemainingTimeout(), + timeout: 60000, }); await page.locator('[data-tour-elem="badge"]').waitFor({ state: 'visible', - timeout: getRemainingTimeout(), + timeout, }); return; // Success diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 1802906962dc..73af3d3784d3 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -23,11 +23,12 @@ import { waitForAllLoadersToDisappear } from './entity'; import { settingClick } from './sidebar'; import { revokeToken } from './user'; -const botName = `a-bot-pw%test-${uuid()}`; +const botName = `test-bot-${uuid()}`; +const botEmailPrefix = `hello-${uuid()}`; const BOT_DETAILS = { botName: botName, - botEmail: `${botName}@mail.com`, + botEmail: `${botEmailPrefix}@open-metadata.org`, description: `This is bot description for ${botName}`, updatedDescription: `This is updated bot description for ${botName}`, updatedBotName: `updated-${botName}`, @@ -167,6 +168,14 @@ export const verifyBotSearch = async (page: Page) => { await searchInput.fill(BOT_DETAILS.botEmail); await expect(createdBotLink).toBeVisible(); + await searchInput.clear(); + await searchInput.fill('test'); + await expect(createdBotLink).toBeVisible(); + + await searchInput.clear(); + await searchInput.fill('hello'); + await expect(createdBotLink).toBeVisible(); + await searchInput.clear(); await expect(createdBotLink).toBeVisible(); }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts index 0bd8d6d4d576..927b71c9b74f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts @@ -88,14 +88,7 @@ export const searchForEntityShouldWorkShowNoResult = async ( await page.getByTestId('searchBox').click(); await page.getByTestId('searchBox').fill(fqn); - await Promise.all([ - page.waitForResponse( - (response) => - response.url().includes('/api/v1/search/query') && - response.status() === 200 - ), - page.getByTestId('searchBox').press('Enter'), - ]); + await page.getByTestId('searchBox').press('Enter'); await waitForAllLoadersToDisappear(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index b80210f60ad6..487569f976c8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -127,7 +127,7 @@ export const visitUserProfilePage = async (page: Page, userName: string) => { return await page.getByTestId(userName).count(); }, { - timeout: 120000, + timeout: 60000, intervals: [1000, 2000, 5000], message: `Timed out waiting for user ${userName} to become visible in the user list`, } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index e565774b9dd2..9de964935ff4 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -321,14 +321,9 @@ const BotListV1 = ({ .filter((name): name is string => Boolean(name)) ) ); - - if (!matchedBotUserNames.length) { - return []; - } - - const botsByBotUserName = await getBotsByBotUserNames( - matchedBotUserNames - ); + const botsByBotUserName = matchedBotUserNames.length + ? await getBotsByBotUserNames(matchedBotUserNames) + : new Map(); const matchedBotUsersByName = new Map( matchedBotUsers.map((botUser) => [botUser.name, botUser]) ); @@ -373,14 +368,17 @@ const BotListV1 = ({ pageNumber: 1, pageSize: 100, includeDeleted: showDeleted, - queryFilter: getTermQuery({ isBot: true }, 'must', undefined, { - wildcardShouldQueries: { - 'name.keyword': wildcardPattern, - 'displayName.keyword': wildcardPattern, - 'fullyQualifiedName.keyword': wildcardPattern, - 'email.keyword': wildcardPattern, + queryFilter: { + bool: { + must: [{ term: { isBot: true } }], + should: [ + { wildcard: { 'name.keyword': wildcardPattern } }, + { wildcard: { 'displayName.keyword': wildcardPattern } }, + { wildcard: { 'email.keyword': wildcardPattern } }, + ], + minimum_should_match: 1, }, - }), + }, searchIndex: SearchIndex.USER, }); const fallbackMatchedBotUsers = formatUsersResponse( diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts index afb664c029bf..aeb271b9e696 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts @@ -27,10 +27,6 @@ interface GetBotParams { include?: Include; } -interface GetBotByNameParams { - include?: Include; -} - export const getBots = async (params: GetBotParams) => { const response = await axiosClient.get<{ data: Bot[]; paging: Paging }>( BASE_URL, @@ -42,20 +38,6 @@ export const getBots = async (params: GetBotParams) => { return response.data; }; -export const getBotByName = async ( - name: string, - params: GetBotByNameParams = {} -) => { - const response = await axiosClient.get( - `${BASE_URL}/name/${encodeURIComponent(name)}`, - { - params, - } - ); - - return response.data; -}; - export const createBot = async (data: CreateBot) => { const response = await axiosClient.post>( BASE_URL, From 2ed61b3db83ab06c3593dc5717fab69f7b676140 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 17 Apr 2026 16:54:07 +0530 Subject: [PATCH 07/24] fix: stabilize bot search behavior and flaky Playwright flows across bots, glossary, lineage, and announcements --- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 52 +++++++++++-------- .../main/resources/ui/playwright/utils/bot.ts | 20 +++---- .../resources/ui/playwright/utils/entity.ts | 4 +- .../resources/ui/playwright/utils/lineage.ts | 3 +- .../resources/ui/playwright/utils/user.ts | 21 +++++++- .../Bot/BotListV1/BotListV1.component.tsx | 34 +++++++----- 6 files changed, 84 insertions(+), 50 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 7334654d1fea..537693fc6125 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -1808,7 +1808,7 @@ test.describe('Glossary tests', () => { test('Check for duplicate Glossary Term with Glossary having dot in name', async ({ browser, }) => { - const { page, apiContext } = await performAdminLogin(browser); + const { page, apiContext, afterAction } = await performAdminLogin(browser); const glossary1 = new Glossary(); const glossaryTerm1 = new GlossaryTerm( glossary1, @@ -1820,34 +1820,40 @@ test.describe('Glossary tests', () => { undefined, 'Pw_test_term' ); - await glossary1.create(apiContext); - await sidebarClick(page, SidebarItem.GLOSSARY); - await selectActiveGlossary(page, glossary1.data.displayName); + try { + await glossary1.create(apiContext); - await test.step('Create Glossary Term One', async () => { - await fillGlossaryTermDetails(page, glossaryTerm1.data, false, false); + await sidebarClick(page, SidebarItem.GLOSSARY); + await selectActiveGlossary(page, glossary1.data.displayName); - const glossaryTermResponse = page.waitForResponse( - '/api/v1/glossaryTerms' - ); - await page.click('[data-testid="save-glossary-term"]'); - await glossaryTermResponse; - }); + await test.step('Create Glossary Term One', async () => { + await fillGlossaryTermDetails(page, glossaryTerm1.data, false, false); - await test.step('Create Glossary Term Two', async () => { - await fillGlossaryTermDetails(page, glossaryTerm2.data, false, false); + const glossaryTermResponse = page.waitForResponse( + '/api/v1/glossaryTerms' + ); + await page.click('[data-testid="save-glossary-term"]'); + await glossaryTermResponse; + }); - const glossaryTermResponse = page.waitForResponse( - '/api/v1/glossaryTerms' - ); - await page.click('[data-testid="save-glossary-term"]'); - await glossaryTermResponse; + await test.step('Create Glossary Term Two', async () => { + await fillGlossaryTermDetails(page, glossaryTerm2.data, false, false); - await expect(page.locator('#name_help')).toHaveText( - `A term with the name '${glossaryTerm2.data.name}' already exists in '${glossary1.responseData.fullyQualifiedName}' glossary.` - ); - }); + const glossaryTermResponse = page.waitForResponse( + '/api/v1/glossaryTerms' + ); + await page.click('[data-testid="save-glossary-term"]'); + await glossaryTermResponse; + + await expect(page.locator('#name_help')).toHaveText( + `A term with the name '${glossaryTerm2.data.name}' already exists in '${glossary1.responseData.fullyQualifiedName}' glossary.` + ); + }); + } finally { + await glossary1.delete(apiContext); + await afterAction(); + } }); test('Verify Glossary Deny Permission', async ({ browser }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 73af3d3784d3..eaa8deaba663 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -49,11 +49,9 @@ export const getCreatedBot = async ( } ) => { // Click on created Bot name - const fetchResponse = page.waitForResponse( - `/api/v1/bots/name/${encodeURIComponent(botName)}?*` - ); await page.getByTestId(`bot-link-${botDisplayName ?? botName}`).click(); - await fetchResponse; + + await expect(page.getByTestId('token-expiry')).toBeVisible(); }; export const createBot = async (page: Page) => { @@ -71,9 +69,16 @@ export const createBot = async (page: Page) => { await page.locator(descriptionBox).fill(BOT_DETAILS.description); - const saveResponse = page.waitForResponse('/api/v1/bots'); + const saveResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/bots') && + response.request().method() === 'POST' + ); await page.click('[data-testid="save-user"]'); - await saveResponse; + const createBotResponse = await saveResponse; + + expect(createBotResponse.status()).toBeGreaterThanOrEqual(200); + expect(createBotResponse.status()).toBeLessThan(300); // Verify bot is getting added in the bots listing page await expect( @@ -84,9 +89,6 @@ export const createBot = async (page: Page) => { page.getByRole('cell', { name: BOT_DETAILS.description }) ).toBeVisible(); - // Get created bot - await getCreatedBot(page, { botName }); - await expect(page.getByTestId('revoke-button')).toContainText('Revoke token'); await expect(page.getByTestId('center-panel')).toContainText( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index c30acf88f074..2be3e1718c2b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -1532,6 +1532,8 @@ export const editAnnouncement = async ( page: Page, data: { title: string; description: string } ) => { + await waitForAllLoadersToDisappear(page); + // Open announcement drawer via manage button await page.getByTestId('manage-button').click(); await page.getByTestId('announcement-button').click(); @@ -1553,7 +1555,7 @@ export const editAnnouncement = async ( await page.locator('.ant-popover').first().waitFor({ state: 'visible' }); // Click the edit message button in the popover - await page.click('[data-testid="edit-message"]'); + await page.getByTestId('edit-message').first().click(); // Wait for the edit announcement modal to open await expect(page.locator('.ant-modal-header')).toContainText( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts index 9e0b0320225f..85b969da4107 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts @@ -719,6 +719,7 @@ export const verifyCSVHeaders = async (headers: string[]) => { }; export const getLineageCSVData = async (page: Page) => { + await waitForAllLoadersToDisappear(page); await expect(page.getByTestId('export-button')).toBeEnabled(); await page.getByTestId('export-button').click(); @@ -730,7 +731,7 @@ export const getLineageCSVData = async (page: Page) => { }); const [download] = await Promise.all([ - page.waitForEvent('download'), + page.waitForEvent('download', { timeout: 60000 }), page.click( '[data-testid="export-entity-modal"] button#submit-button:visible' ), diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index 487569f976c8..94cda3868066 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -503,6 +503,10 @@ export const generateToken = async (page: Page) => { }; export const revokeToken = async (page: Page, isBot?: boolean) => { + if (page.isClosed()) { + return; + } + const revokeButton = page .locator('[data-testid="center-panel"] [data-testid="revoke-button"]') .first(); @@ -528,9 +532,15 @@ export const revokeToken = async (page: Page, isBot?: boolean) => { ); await page.click('[data-testid="save-button"]'); - await revokeResponse.catch(() => undefined); + await revokeResponse; - await expect(revokeButton).toBeHidden({ timeout: 60000 }); + if (page.isClosed()) { + return; + } + + await expect(page.locator('[data-testid="revoke-button"]')).toHaveCount(0, { + timeout: 60000, + }); }; export const updateExpiration = async (page: Page, expiry: number) => { @@ -551,8 +561,15 @@ export const updateExpiration = async (page: Page, expiry: number) => { // Wait for any dropdown animations to complete await page.locator('.ant-select-dropdown').waitFor({ state: 'hidden' }); + const saveTokenResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/users/security/token') && + response.request().method() === 'POST' + ); + // Now click the save button await page.click('[data-testid="save-edit"]'); + await saveTokenResponse; await expect( page.locator('[data-testid="center-panel"] [data-testid="revoke-button"]') diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 9de964935ff4..9c7e256c0d48 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -156,21 +156,26 @@ const BotListV1 = ({ const getBotsByBotUserNames = async (botUserNames: string[]) => { const include = getBotIncludeFilter(); const botsByBotUserName = new Map(); - const matchedBotsResponse = await Promise.allSettled( - botUserNames.map((name) => - getBotByName(name, { - include, - }) - ) - ); + const batchSize = 10; + + for (let index = 0; index < botUserNames.length; index += batchSize) { + const batch = botUserNames.slice(index, index + batchSize); + const matchedBotsResponse = await Promise.allSettled( + batch.map((name) => + getBotByName(name, { + include, + }) + ) + ); - matchedBotsResponse.forEach((response) => { - if (response.status !== 'fulfilled') { - return; - } + matchedBotsResponse.forEach((response) => { + if (response.status !== 'fulfilled') { + return; + } - botsByBotUserName.set(response.value.name, response.value); - }); + botsByBotUserName.set(response.value.name, response.value); + }); + } return botsByBotUserName; }; @@ -374,6 +379,7 @@ const BotListV1 = ({ should: [ { wildcard: { 'name.keyword': wildcardPattern } }, { wildcard: { 'displayName.keyword': wildcardPattern } }, + { wildcard: { 'fullyQualifiedName.keyword': wildcardPattern } }, { wildcard: { 'email.keyword': wildcardPattern } }, ], minimum_should_match: 1, @@ -538,7 +544,7 @@ const BotListV1 = ({ paging, pagingHandler: handleBotPageChange, onShowSizeChange: handlePageSizeChange, - showPagination, + showPagination: showPagination && !searchTerm, }} dataSource={searchedData} loading={loading} From 2f6bbfcbbef6173d9d6cca7b98c4fb2da27241c1 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 17 Apr 2026 17:43:00 +0530 Subject: [PATCH 08/24] fix: limit PR scope to bot search by reverting unrelated Tour Playwright changes --- .../src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts index 6d78019a9ec5..b020e183ea4e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts @@ -25,11 +25,6 @@ const waitForTourBadgeWithRetry = async ( ) => { for (let attempt = 1; attempt <= maxAttempts; attempt++) { try { - await page.waitForURL('**/tour', { timeout: 60000 }); - await page.locator('#feedWidgetData').waitFor({ - state: 'visible', - timeout: 60000, - }); await page.locator('[data-tour-elem="badge"]').waitFor({ state: 'visible', timeout, From ce64825829463558019b7134d0137dae285eb084 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 17 Apr 2026 17:59:58 +0530 Subject: [PATCH 09/24] fix: remove unrelated Playwright changes and keep bot search scope focused --- .../ui/playwright/e2e/Flow/Tour.spec.ts | 2 +- .../ui/playwright/e2e/Pages/Glossary.spec.ts | 68 +++++++------------ .../e2e/Pages/Lineage/LineageFilters.spec.ts | 12 ---- .../VersionPages/EntityVersionPages.spec.ts | 3 - .../ui/playwright/support/user/UserClass.ts | 37 ++-------- .../resources/ui/playwright/utils/entity.ts | 4 +- .../resources/ui/playwright/utils/lineage.ts | 3 +- .../ui/playwright/utils/searchRBAC.ts | 8 +-- .../resources/ui/playwright/utils/user.ts | 52 +++----------- 9 files changed, 45 insertions(+), 144 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts index b020e183ea4e..b4283d5318b0 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Flow/Tour.spec.ts @@ -221,7 +221,7 @@ test.describe( await page.locator('#feedWidgetData').waitFor(); // Since the tour steps are already tested in the first test, // here we only validate whether the tour is loading or not. - await waitForTourBadgeWithRetry(page, 3, 40000); + await waitForTourBadgeWithRetry(page); }); } ); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts index 537693fc6125..32ec55e1f230 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Glossary.spec.ts @@ -581,19 +581,9 @@ test.describe('Glossary tests', () => { await patchRequest; // Add non mutually exclusive tags - await waitForAllLoadersToDisappear(page); - const addGlossaryBtn = page - .getByTestId('KnowledgePanel.GlossaryTerms') - .getByTestId('glossary-container') - .getByTestId('add-tag'); - - await addGlossaryBtn.waitFor({ - state: 'visible', - timeout: 60000, - }); - await expect(addGlossaryBtn).toBeEnabled(); - await addGlossaryBtn.scrollIntoViewIfNeeded(); - await addGlossaryBtn.click(); + await page.click( + '[data-testid="KnowledgePanel.GlossaryTerms"] [data-testid="glossary-container"] [data-testid="add-tag"]' + ); // Select 1st term await page.click('[data-testid="tag-selector"] #tagsForm_tags'); @@ -1808,7 +1798,7 @@ test.describe('Glossary tests', () => { test('Check for duplicate Glossary Term with Glossary having dot in name', async ({ browser, }) => { - const { page, apiContext, afterAction } = await performAdminLogin(browser); + const { page, afterAction, apiContext } = await performAdminLogin(browser); const glossary1 = new Glossary(); const glossaryTerm1 = new GlossaryTerm( glossary1, @@ -1820,40 +1810,34 @@ test.describe('Glossary tests', () => { undefined, 'Pw_test_term' ); + await glossary1.create(apiContext); - try { - await glossary1.create(apiContext); - - await sidebarClick(page, SidebarItem.GLOSSARY); - await selectActiveGlossary(page, glossary1.data.displayName); + await sidebarClick(page, SidebarItem.GLOSSARY); + await selectActiveGlossary(page, glossary1.data.displayName); - await test.step('Create Glossary Term One', async () => { - await fillGlossaryTermDetails(page, glossaryTerm1.data, false, false); + await test.step('Create Glossary Term One', async () => { + await fillGlossaryTermDetails(page, glossaryTerm1.data, false, false); - const glossaryTermResponse = page.waitForResponse( - '/api/v1/glossaryTerms' - ); - await page.click('[data-testid="save-glossary-term"]'); - await glossaryTermResponse; - }); + const glossaryTermResponse = page.waitForResponse( + '/api/v1/glossaryTerms' + ); + await page.click('[data-testid="save-glossary-term"]'); + await glossaryTermResponse; + }); - await test.step('Create Glossary Term Two', async () => { - await fillGlossaryTermDetails(page, glossaryTerm2.data, false, false); + await test.step('Create Glossary Term Two', async () => { + await fillGlossaryTermDetails(page, glossaryTerm2.data, false, false); - const glossaryTermResponse = page.waitForResponse( - '/api/v1/glossaryTerms' - ); - await page.click('[data-testid="save-glossary-term"]'); - await glossaryTermResponse; + const glossaryTermResponse = page.waitForResponse( + '/api/v1/glossaryTerms' + ); + await page.click('[data-testid="save-glossary-term"]'); + await glossaryTermResponse; - await expect(page.locator('#name_help')).toHaveText( - `A term with the name '${glossaryTerm2.data.name}' already exists in '${glossary1.responseData.fullyQualifiedName}' glossary.` - ); - }); - } finally { - await glossary1.delete(apiContext); - await afterAction(); - } + await expect(page.locator('#name_help')).toHaveText( + `A term with the name '${glossaryTerm2.data.name}' already exists in '${glossary1.responseData.fullyQualifiedName}' glossary.` + ); + }); }); test('Verify Glossary Deny Permission', async ({ browser }) => { diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts index e9ad4716051f..2bc1bdcd7085 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Lineage/LineageFilters.spec.ts @@ -124,18 +124,6 @@ test.describe('Lineage Filters', () => { await afterAction(); }); - test.afterAll(async ({ browser }) => { - const { apiContext, afterAction } = await getDefaultAdminAPIContext( - browser - ); - - await Promise.allSettled( - entities.map((entity) => entity.delete(apiContext)) - ); - await Promise.allSettled([lineageEntity.delete(apiContext)]); - await afterAction(); - }); - test.beforeEach(async ({ page }) => { await lineageEntity.visitEntityPage(page); await visitLineageTab(page); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts index 4318cab02888..d2bd1933d4e9 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/VersionPages/EntityVersionPages.spec.ts @@ -143,9 +143,6 @@ test.describe('Entity Version pages', () => { test.slow(); const { apiContext, afterAction } = await performAdminLogin(browser); - await Promise.allSettled( - entities.map((entity) => entity.delete(apiContext)) - ); await adminUser.delete(apiContext); await afterAction(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts index d4a161888281..42c5a745bec7 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/support/user/UserClass.ts @@ -55,41 +55,12 @@ export class UserClass { }); if (!response.ok()) { - const responseText = await response.text(); - - if ( - response.status() === 400 && - responseText.includes('User with Email Already Exists') - ) { - const existingUserResponse = await apiContext.get( - `/api/v1/users?email=${encodeURIComponent(this.data.email)}&limit=1` - ); - - if (!existingUserResponse.ok()) { - throw new Error( - `UserClass.create() user exists but fetch failed (${existingUserResponse.status()}): ${await existingUserResponse.text()}` - ); - } - - const existingUsersData = await existingUserResponse.json(); - const existingUser = existingUsersData?.data?.[0]; - - if (!existingUser) { - throw new Error( - `UserClass.create() user exists but no user found for email ${this.data.email}` - ); - } - - this.responseData = existingUser; - } else { - throw new Error( - `UserClass.create() failed with status ${response.status()}: ${responseText}` - ); - } - } else { - this.responseData = await response.json(); + throw new Error( + `UserClass.create() failed with status ${response.status()}: ${await response.text()}` + ); } + this.responseData = await response.json(); if (assignRole) { const { entity } = await this.patch({ apiContext, diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts index 2be3e1718c2b..c30acf88f074 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/entity.ts @@ -1532,8 +1532,6 @@ export const editAnnouncement = async ( page: Page, data: { title: string; description: string } ) => { - await waitForAllLoadersToDisappear(page); - // Open announcement drawer via manage button await page.getByTestId('manage-button').click(); await page.getByTestId('announcement-button').click(); @@ -1555,7 +1553,7 @@ export const editAnnouncement = async ( await page.locator('.ant-popover').first().waitFor({ state: 'visible' }); // Click the edit message button in the popover - await page.getByTestId('edit-message').first().click(); + await page.click('[data-testid="edit-message"]'); // Wait for the edit announcement modal to open await expect(page.locator('.ant-modal-header')).toContainText( diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts index 85b969da4107..9e0b0320225f 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/lineage.ts @@ -719,7 +719,6 @@ export const verifyCSVHeaders = async (headers: string[]) => { }; export const getLineageCSVData = async (page: Page) => { - await waitForAllLoadersToDisappear(page); await expect(page.getByTestId('export-button')).toBeEnabled(); await page.getByTestId('export-button').click(); @@ -731,7 +730,7 @@ export const getLineageCSVData = async (page: Page) => { }); const [download] = await Promise.all([ - page.waitForEvent('download', { timeout: 60000 }), + page.waitForEvent('download'), page.click( '[data-testid="export-entity-modal"] button#submit-button:visible' ), diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts index 927b71c9b74f..af7d3c8d93db 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/searchRBAC.ts @@ -90,17 +90,15 @@ export const searchForEntityShouldWorkShowNoResult = async ( await page.getByTestId('searchBox').fill(fqn); await page.getByTestId('searchBox').press('Enter'); - await waitForAllLoadersToDisappear(page); + await page.waitForResponse(`api/v1/search/query?**`); - await expect( - page.getByTestId('no-search-results').getByText('No result found.') - ).toBeVisible({ timeout: 30000 }); + await waitForAllLoadersToDisappear(page); await expect( page.locator('[data-testid="entity-header-display-name"]', { hasText: displayName, }) - ).toHaveCount(0); + ).not.toBeAttached(); await expect( page.getByTestId('no-search-results').getByText('No result found.') diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts index 94cda3868066..ecd114126fa2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/user.ts @@ -107,6 +107,12 @@ export const visitUserProfilePage = async (page: Page, userName: string) => { const userResponse = page.waitForResponse( '/api/v1/search/query?q=*&index=*&from=0&size=*' ); + const loaderPromise = page + .getByTestId('user-list-v1-component') + .getByTestId('loader') + .waitFor({ + state: 'detached', + }); const searchBar = page.getByTestId('searchbar'); await expect @@ -116,13 +122,7 @@ export const visitUserProfilePage = async (page: Page, userName: string) => { await searchBar.fill(''); await searchBar.fill(userName); await searchRequest; - await page - .getByTestId('user-list-v1-component') - .getByTestId('loader') - .waitFor({ - state: 'detached', - }) - .catch(() => undefined); + await loaderPromise.catch(() => undefined); return await page.getByTestId(userName).count(); }, @@ -503,21 +503,7 @@ export const generateToken = async (page: Page) => { }; export const revokeToken = async (page: Page, isBot?: boolean) => { - if (page.isClosed()) { - return; - } - - const revokeButton = page - .locator('[data-testid="center-panel"] [data-testid="revoke-button"]') - .first(); - - await revokeButton.waitFor({ - state: 'visible', - timeout: 60000, - }); - await expect(revokeButton).toBeEnabled(); - await revokeButton.scrollIntoViewIfNeeded(); - await revokeButton.click(); + await page.click('[data-testid="revoke-button"]'); await expect(page.locator('[data-testid="body-text"]')).toContainText( `Are you sure you want to revoke access for ${ @@ -525,22 +511,9 @@ export const revokeToken = async (page: Page, isBot?: boolean) => { }?` ); - const revokeResponse = page.waitForResponse( - (response) => - response.url().includes('/api/v1/users/security/token') && - ['DELETE', 'POST'].includes(response.request().method()) - ); - await page.click('[data-testid="save-button"]'); - await revokeResponse; - if (page.isClosed()) { - return; - } - - await expect(page.locator('[data-testid="revoke-button"]')).toHaveCount(0, { - timeout: 60000, - }); + await expect(page.locator('[data-testid="revoke-button"]')).not.toBeVisible(); }; export const updateExpiration = async (page: Page, expiry: number) => { @@ -561,15 +534,8 @@ export const updateExpiration = async (page: Page, expiry: number) => { // Wait for any dropdown animations to complete await page.locator('.ant-select-dropdown').waitFor({ state: 'hidden' }); - const saveTokenResponse = page.waitForResponse( - (response) => - response.url().includes('/api/v1/users/security/token') && - response.request().method() === 'POST' - ); - // Now click the save button await page.click('[data-testid="save-edit"]'); - await saveTokenResponse; await expect( page.locator('[data-testid="center-panel"] [data-testid="revoke-button"]') From 774f59bba546f7e230cb31b5bfe0016d5b5efbc6 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Sat, 18 Apr 2026 22:29:18 +0530 Subject: [PATCH 10/24] fix: optimize bot search scalability with paginated user-index retrieval and bounded-concurrency bot resolution --- .../Bot/BotListV1/BotListV1.component.tsx | 116 ++++++++++-------- 1 file changed, 68 insertions(+), 48 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 9c7e256c0d48..f190f5dfd040 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -89,6 +89,8 @@ const BotListV1 = ({ const [searchedData, setSearchedData] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const latestSearchRequest = useRef(0); + const BOT_SEARCH_PAGE_SIZE = 100; + const BOT_SEARCH_CONCURRENCY = 10; const getBotIncludeFilter = useCallback( () => (showDeleted ? Include.Deleted : Include.NonDeleted), @@ -156,28 +158,63 @@ const BotListV1 = ({ const getBotsByBotUserNames = async (botUserNames: string[]) => { const include = getBotIncludeFilter(); const botsByBotUserName = new Map(); - const batchSize = 10; - - for (let index = 0; index < botUserNames.length; index += batchSize) { - const batch = botUserNames.slice(index, index + batchSize); - const matchedBotsResponse = await Promise.allSettled( - batch.map((name) => - getBotByName(name, { - include, - }) - ) - ); - - matchedBotsResponse.forEach((response) => { - if (response.status !== 'fulfilled') { - return; + const workerCount = Math.min(BOT_SEARCH_CONCURRENCY, botUserNames.length); + let currentIndex = 0; + + await Promise.all( + Array.from({ length: workerCount }, async () => { + while (currentIndex < botUserNames.length) { + const botIndex = currentIndex; + currentIndex += 1; + const botName = botUserNames[botIndex]; + + try { + const bot = await getBotByName(botName, { + include, + }); + + botsByBotUserName.set(bot.name, bot); + } catch { + continue; + } } + }) + ); - botsByBotUserName.set(response.value.name, response.value); + return botsByBotUserName; + }; + + const getSearchMatchedBotUsers = async ( + text: string, + queryFilter: Record + ) => { + const usersByName = new Map(); + let pageNumber = 1; + let totalMatches = 0; + + do { + const response = await searchQuery({ + query: text, + pageNumber, + pageSize: BOT_SEARCH_PAGE_SIZE, + includeDeleted: showDeleted, + queryFilter, + searchIndex: SearchIndex.USER, + trackTotalHits: true, }); - } + const users = formatUsersResponse(response.hits.hits); - return botsByBotUserName; + users.forEach((user) => { + if (user.name) { + usersByName.set(user.name, user); + } + }); + + totalMatches = response.hits.total.value ?? usersByName.size; + pageNumber += 1; + } while ((pageNumber - 1) * BOT_SEARCH_PAGE_SIZE < totalMatches); + + return Array.from(usersByName.values()); }; /** @@ -349,16 +386,9 @@ const BotListV1 = ({ }); }; - const matchedUsersBySearchQuery = await searchQuery({ - query: text, - pageNumber: 1, - pageSize: 100, - includeDeleted: showDeleted, - queryFilter: getTermQuery({ isBot: true }), - searchIndex: SearchIndex.USER, - }); - const matchedBotUsers = formatUsersResponse( - matchedUsersBySearchQuery.hits.hits + const matchedBotUsers = await getSearchMatchedBotUsers( + text, + getTermQuery({ isBot: true }) ); const matchedBots = await getMatchedBots(matchedBotUsers); @@ -368,28 +398,18 @@ const BotListV1 = ({ const escapedText = escapeESReservedCharacters(text); const wildcardPattern = `*${escapedText}*`; - const matchedUsersByWildcardFilter = await searchQuery({ - query: '*', - pageNumber: 1, - pageSize: 100, - includeDeleted: showDeleted, - queryFilter: { - bool: { - must: [{ term: { isBot: true } }], - should: [ - { wildcard: { 'name.keyword': wildcardPattern } }, - { wildcard: { 'displayName.keyword': wildcardPattern } }, - { wildcard: { 'fullyQualifiedName.keyword': wildcardPattern } }, - { wildcard: { 'email.keyword': wildcardPattern } }, - ], - minimum_should_match: 1, - }, + const fallbackMatchedBotUsers = await getSearchMatchedBotUsers('*', { + bool: { + must: [{ term: { isBot: true } }], + should: [ + { wildcard: { 'name.keyword': wildcardPattern } }, + { wildcard: { 'displayName.keyword': wildcardPattern } }, + { wildcard: { 'fullyQualifiedName.keyword': wildcardPattern } }, + { wildcard: { 'email.keyword': wildcardPattern } }, + ], + minimum_should_match: 1, }, - searchIndex: SearchIndex.USER, }); - const fallbackMatchedBotUsers = formatUsersResponse( - matchedUsersByWildcardFilter.hits.hits - ); return getMatchedBots(fallbackMatchedBotUsers); }; From d2e68dccc633bc95de3a1e461651406323478195 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Sat, 18 Apr 2026 22:41:44 +0530 Subject: [PATCH 11/24] fix: harden Bots API search with bounded pagination/concurrency and consistent active-search refresh behavior --- .../Bot/BotListV1/BotListV1.component.tsx | 182 +++++++++--------- 1 file changed, 96 insertions(+), 86 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index f190f5dfd040..3165fb1baf3a 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -91,6 +91,8 @@ const BotListV1 = ({ const latestSearchRequest = useRef(0); const BOT_SEARCH_PAGE_SIZE = 100; const BOT_SEARCH_CONCURRENCY = 10; + const MAX_BOT_SEARCH_PAGES = 5; + const MAX_BOT_USER_RESOLUTION = BOT_SEARCH_PAGE_SIZE * MAX_BOT_SEARCH_PAGES; const getBotIncludeFilter = useCallback( () => (showDeleted ? Include.Deleted : Include.NonDeleted), @@ -136,7 +138,7 @@ const BotListV1 = ({ bool: { must: [{ term: { isBot: true } }], should: bots.map((bot) => ({ - term: { name: bot.name }, + term: { 'name.keyword': bot.name }, })), minimum_should_match: 1, }, @@ -192,7 +194,7 @@ const BotListV1 = ({ let pageNumber = 1; let totalMatches = 0; - do { + while (pageNumber <= MAX_BOT_SEARCH_PAGES) { const response = await searchQuery({ query: text, pageNumber, @@ -211,12 +213,93 @@ const BotListV1 = ({ }); totalMatches = response.hits.total.value ?? usersByName.size; + if (!users.length || pageNumber * BOT_SEARCH_PAGE_SIZE >= totalMatches) { + break; + } + pageNumber += 1; - } while ((pageNumber - 1) * BOT_SEARCH_PAGE_SIZE < totalMatches); + } return Array.from(usersByName.values()); }; + const searchBots = async (text: string) => { + const getMatchedBots = async (matchedBotUsers: User[]) => { + const matchedBotUserNames = Array.from( + new Set( + matchedBotUsers + .map((botUser) => botUser.name) + .filter((name): name is string => Boolean(name)) + ) + ).slice(0, MAX_BOT_USER_RESOLUTION); + const botsByBotUserName = matchedBotUserNames.length + ? await getBotsByBotUserNames(matchedBotUserNames) + : new Map(); + const matchedBotUsersByName = new Map( + matchedBotUsers.map((botUser) => [botUser.name, botUser]) + ); + + return matchedBotUserNames.flatMap((botUserName) => { + const matchedBot = botsByBotUserName.get(botUserName); + + if (!matchedBot) { + return []; + } + + return [ + enrichBotWithMatchedUser( + matchedBot, + matchedBotUsersByName.get(botUserName) + ), + ]; + }); + }; + + const matchedBotUsers = await getSearchMatchedBotUsers( + text, + getTermQuery({ isBot: true }) + ); + const matchedBots = await getMatchedBots(matchedBotUsers); + + if (matchedBots.length) { + return matchedBots; + } + + const escapedText = escapeESReservedCharacters(text); + const wildcardPattern = `*${escapedText}*`; + const fallbackMatchedBotUsers = await getSearchMatchedBotUsers('*', { + bool: { + must: [{ term: { isBot: true } }], + should: [ + { wildcard: { 'name.keyword': wildcardPattern } }, + { wildcard: { 'displayName.keyword': wildcardPattern } }, + { wildcard: { 'fullyQualifiedName.keyword': wildcardPattern } }, + { wildcard: { 'email.keyword': wildcardPattern } }, + ], + minimum_should_match: 1, + }, + }); + + return getMatchedBots(fallbackMatchedBotUsers); + }; + + const runActiveSearch = async (activeSearchTerm: string) => { + const searchRequestId = ++latestSearchRequest.current; + + try { + const matchedBots = await searchBots(activeSearchTerm); + + if (searchRequestId === latestSearchRequest.current) { + setSearchedData(matchedBots); + } + } catch (error) { + if (searchRequestId === latestSearchRequest.current) { + showErrorToast((error as AxiosError).message); + setSearchedData([]); + } + } + }; + /** * * @param after - Pagination value if passed data will be fetched post cursor value @@ -234,10 +317,16 @@ const BotListV1 = ({ include: showDeleted ? Include.Deleted : Include.NonDeleted, }); const botsWithUsers = await enrichBotsWithBotUsers(data); + const activeSearchTerm = searchTerm.trim(); handlePagingChange(paging); setBotUsers(botsWithUsers); - setSearchedData(botsWithUsers); + if (activeSearchTerm) { + await runActiveSearch(activeSearchTerm); + } else { + setSearchedData(botsWithUsers); + } + if (!showDeleted && isEmpty(botsWithUsers)) { setHandleErrorPlaceholder(true); } else { @@ -354,68 +443,7 @@ const BotListV1 = ({ fetchBots(showDeleted); }, [selectedUser]); - const searchBots = async (text: string) => { - const getMatchedBots = async (matchedBotUsers: User[]) => { - const matchedBotUserNames = Array.from( - new Set( - matchedBotUsers - .map((botUser) => botUser.name) - .filter((name): name is string => Boolean(name)) - ) - ); - const botsByBotUserName = matchedBotUserNames.length - ? await getBotsByBotUserNames(matchedBotUserNames) - : new Map(); - const matchedBotUsersByName = new Map( - matchedBotUsers.map((botUser) => [botUser.name, botUser]) - ); - - return matchedBotUserNames.flatMap((botUserName) => { - const matchedBot = botsByBotUserName.get(botUserName); - - if (!matchedBot) { - return []; - } - - return [ - enrichBotWithMatchedUser( - matchedBot, - matchedBotUsersByName.get(botUserName) - ), - ]; - }); - }; - - const matchedBotUsers = await getSearchMatchedBotUsers( - text, - getTermQuery({ isBot: true }) - ); - const matchedBots = await getMatchedBots(matchedBotUsers); - - if (matchedBots.length) { - return matchedBots; - } - - const escapedText = escapeESReservedCharacters(text); - const wildcardPattern = `*${escapedText}*`; - const fallbackMatchedBotUsers = await getSearchMatchedBotUsers('*', { - bool: { - must: [{ term: { isBot: true } }], - should: [ - { wildcard: { 'name.keyword': wildcardPattern } }, - { wildcard: { 'displayName.keyword': wildcardPattern } }, - { wildcard: { 'fullyQualifiedName.keyword': wildcardPattern } }, - { wildcard: { 'email.keyword': wildcardPattern } }, - ], - minimum_should_match: 1, - }, - }); - - return getMatchedBots(fallbackMatchedBotUsers); - }; - const handleSearch = async (text: string) => { - const searchRequestId = ++latestSearchRequest.current; setSearchTerm(text); handlePageChange(INITIAL_PAGING_VALUE, { @@ -430,27 +458,9 @@ const BotListV1 = ({ return; } - try { - setLoading(true); - const matchedBots = await searchBots(text); - - if (searchRequestId !== latestSearchRequest.current) { - return; - } - - setSearchedData(matchedBots); - } catch (error) { - if (searchRequestId !== latestSearchRequest.current) { - return; - } - - showErrorToast((error as AxiosError).message); - setSearchedData([]); - } finally { - if (searchRequestId === latestSearchRequest.current) { - setLoading(false); - } - } + setLoading(true); + await runActiveSearch(text); + setLoading(false); }; const handleShowDeletedBots = (checked: boolean) => { From 9bed9d31a729a6ababbb3bae2e721dbcf8900321 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Sat, 18 Apr 2026 22:51:57 +0530 Subject: [PATCH 12/24] fix: prevent stale bot search state and strengthen Bots Playwright coverage with deterministic positive/negative assertions --- .../main/resources/ui/playwright/utils/bot.ts | 22 +++++++++++++------ .../Bot/BotListV1/BotListV1.component.tsx | 17 +++++++------- 2 files changed, 24 insertions(+), 15 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index eaa8deaba663..c880cb9a32f8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -162,23 +162,31 @@ export const verifyBotSearch = async (page: Page) => { const createdBotLink = page.getByTestId( `bot-link-${BOT_DETAILS.updatedBotName}` ); + const nonMatchingSearchTerm = `${BOT_DETAILS.updatedBotName}-no-match`; - await searchInput.fill(BOT_DETAILS.updatedBotName); + const searchBotAndWait = async (searchTerm: string) => { + await searchInput.clear(); + await searchInput.fill(searchTerm); + await waitForAllLoadersToDisappear(page); + }; + + await searchBotAndWait(BOT_DETAILS.updatedBotName); await expect(createdBotLink).toBeVisible(); - await searchInput.clear(); - await searchInput.fill(BOT_DETAILS.botEmail); + await searchBotAndWait(BOT_DETAILS.botEmail); await expect(createdBotLink).toBeVisible(); - await searchInput.clear(); - await searchInput.fill('test'); + await searchBotAndWait('test'); await expect(createdBotLink).toBeVisible(); - await searchInput.clear(); - await searchInput.fill('hello'); + await searchBotAndWait('hello'); await expect(createdBotLink).toBeVisible(); + await searchBotAndWait(nonMatchingSearchTerm); + await expect(createdBotLink).not.toBeVisible(); + await searchInput.clear(); + await waitForAllLoadersToDisappear(page); await expect(createdBotLink).toBeVisible(); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 3165fb1baf3a..57b786b4490e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -445,13 +445,14 @@ const BotListV1 = ({ const handleSearch = async (text: string) => { setSearchTerm(text); + const normalizedSearchTerm = text.trim(); - handlePageChange(INITIAL_PAGING_VALUE, { - cursorType: null, - cursorValue: undefined, - }); - - if (!text) { + if (!normalizedSearchTerm) { + latestSearchRequest.current += 1; + handlePageChange(INITIAL_PAGING_VALUE, { + cursorType: null, + cursorValue: undefined, + }); setSearchedData(botUsers); setLoading(false); @@ -459,7 +460,7 @@ const BotListV1 = ({ } setLoading(true); - await runActiveSearch(text); + await runActiveSearch(normalizedSearchTerm); setLoading(false); }; @@ -574,7 +575,7 @@ const BotListV1 = ({ paging, pagingHandler: handleBotPageChange, onShowSizeChange: handlePageSizeChange, - showPagination: showPagination && !searchTerm, + showPagination: showPagination && !searchTerm.trim(), }} dataSource={searchedData} loading={loading} From 8716fb215cdac42c262bee535ffac32be74426ea Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Mon, 20 Apr 2026 11:06:56 +0530 Subject: [PATCH 13/24] test: scope Playwright fixes to bot flow and remove unrelated test changes --- .../src/main/resources/ui/playwright/utils/bot.ts | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index c880cb9a32f8..c9880bbc984c 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -89,6 +89,12 @@ export const createBot = async (page: Page) => { page.getByRole('cell', { name: BOT_DETAILS.description }) ).toBeVisible(); + await expect + .poll(async () => page.getByTestId('revoke-button').count(), { + timeout: 30000, + }) + .toBeGreaterThan(0); + await expect(page.getByTestId('revoke-button')).toContainText('Revoke token'); await expect(page.getByTestId('center-panel')).toContainText( @@ -163,6 +169,8 @@ export const verifyBotSearch = async (page: Page) => { `bot-link-${BOT_DETAILS.updatedBotName}` ); const nonMatchingSearchTerm = `${BOT_DETAILS.updatedBotName}-no-match`; + const uniqueNameToken = BOT_DETAILS.updatedBotName.split('-').slice(-1)[0]; + const uniqueEmailToken = BOT_DETAILS.botEmail.split('@')[0]; const searchBotAndWait = async (searchTerm: string) => { await searchInput.clear(); @@ -176,10 +184,10 @@ export const verifyBotSearch = async (page: Page) => { await searchBotAndWait(BOT_DETAILS.botEmail); await expect(createdBotLink).toBeVisible(); - await searchBotAndWait('test'); + await searchBotAndWait(uniqueNameToken); await expect(createdBotLink).toBeVisible(); - await searchBotAndWait('hello'); + await searchBotAndWait(uniqueEmailToken); await expect(createdBotLink).toBeVisible(); await searchBotAndWait(nonMatchingSearchTerm); From 09566713c32d099398fce206dd46d2b6775c7a6e Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Mon, 20 Apr 2026 12:20:52 +0530 Subject: [PATCH 14/24] Add local bot search and stabilize tests --- .../main/resources/ui/playwright/utils/bot.ts | 39 +++++++++++------- .../Bot/BotListV1/BotListV1.component.tsx | 41 ++++++++++++++++++- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index c9880bbc984c..4cbb3665a3c4 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -89,11 +89,7 @@ export const createBot = async (page: Page) => { page.getByRole('cell', { name: BOT_DETAILS.description }) ).toBeVisible(); - await expect - .poll(async () => page.getByTestId('revoke-button').count(), { - timeout: 30000, - }) - .toBeGreaterThan(0); + await getCreatedBot(page, { botName }); await expect(page.getByTestId('revoke-button')).toContainText('Revoke token'); @@ -158,8 +154,12 @@ export const updateBotDetails = async (page: Page) => { page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`) ).toContainText(BOT_DETAILS.updatedBotName); + const updatedBotRow = page + .getByRole('row') + .filter({ has: page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`) }); + await expect( - page.locator(`[data-row-key="${botName}"] [data-testid="markdown-parser"]`) + updatedBotRow.getByTestId('markdown-parser') ).toContainText(BOT_DETAILS.updatedDescription); }; @@ -172,29 +172,38 @@ export const verifyBotSearch = async (page: Page) => { const uniqueNameToken = BOT_DETAILS.updatedBotName.split('-').slice(-1)[0]; const uniqueEmailToken = BOT_DETAILS.botEmail.split('@')[0]; - const searchBotAndWait = async (searchTerm: string) => { + const searchBot = async (searchTerm: string) => { await searchInput.clear(); await searchInput.fill(searchTerm); - await waitForAllLoadersToDisappear(page); + await expect(searchInput).toHaveValue(searchTerm); }; - await searchBotAndWait(BOT_DETAILS.updatedBotName); + await searchBot(BOT_DETAILS.updatedBotName); await expect(createdBotLink).toBeVisible(); - await searchBotAndWait(BOT_DETAILS.botEmail); + await searchBot(nonMatchingSearchTerm); + await expect(createdBotLink).toHaveCount(0); + + await searchBot(BOT_DETAILS.botEmail); await expect(createdBotLink).toBeVisible(); - await searchBotAndWait(uniqueNameToken); + await searchBot(nonMatchingSearchTerm); + await expect(createdBotLink).toHaveCount(0); + + await searchBot(uniqueNameToken); await expect(createdBotLink).toBeVisible(); - await searchBotAndWait(uniqueEmailToken); + await searchBot(nonMatchingSearchTerm); + await expect(createdBotLink).toHaveCount(0); + + await searchBot(uniqueEmailToken); await expect(createdBotLink).toBeVisible(); - await searchBotAndWait(nonMatchingSearchTerm); - await expect(createdBotLink).not.toBeVisible(); + await searchBot(nonMatchingSearchTerm); + await expect(createdBotLink).toHaveCount(0); await searchInput.clear(); - await waitForAllLoadersToDisappear(page); + await expect(searchInput).toHaveValue(''); await expect(createdBotLink).toBeVisible(); }; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 57b786b4490e..d1c671d8b995 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -122,6 +122,30 @@ const BotListV1 = ({ }; }, []); + const getLocalSearchMatchedBots = useCallback( + (text: string) => { + const normalizedSearchText = text.trim().toLowerCase(); + + return botUsers.filter((bot) => { + const searchableFields = [ + bot.name, + bot.fullyQualifiedName, + bot.displayName, + bot.description, + bot.botUser?.name, + bot.botUser?.fullyQualifiedName, + bot.botUser?.displayName, + (bot.botUser as unknown as User | undefined)?.email, + ]; + + return searchableFields.some((field) => + field?.toLowerCase().includes(normalizedSearchText) + ); + }); + }, + [botUsers] + ); + const enrichBotsWithBotUsers = async (bots: Bot[]) => { if (!bots.length) { return bots; @@ -224,6 +248,12 @@ const BotListV1 = ({ }; const searchBots = async (text: string) => { + const localMatchedBots = getLocalSearchMatchedBots(text); + + if (localMatchedBots.length) { + return localMatchedBots; + } + const getMatchedBots = async (matchedBotUsers: User[]) => { const matchedBotUserNames = Array.from( new Set( @@ -459,9 +489,16 @@ const BotListV1 = ({ return; } + const currentSearchRequestId = latestSearchRequest.current + 1; setLoading(true); - await runActiveSearch(normalizedSearchTerm); - setLoading(false); + + try { + await runActiveSearch(normalizedSearchTerm); + } finally { + if (latestSearchRequest.current === currentSearchRequestId) { + setLoading(false); + } + } }; const handleShowDeletedBots = (checked: boolean) => { From edf3496b911002fe6933140122320283bdcb980f Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Mon, 20 Apr 2026 12:40:36 +0530 Subject: [PATCH 15/24] Fixes: keep Bot search API-driven for complete results and stabilize bot cleanup assertions in Playwright --- .../main/resources/ui/playwright/utils/bot.ts | 23 +++++++++----- .../Bot/BotListV1/BotListV1.component.tsx | 30 ------------------- 2 files changed, 16 insertions(+), 37 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 4cbb3665a3c4..be184da40e0b 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -105,8 +105,13 @@ export const createBot = async (page: Page) => { export const deleteBot = async (page: Page) => { await settingClick(page, GlobalSettingOptions.BOTS); - // Click on delete button - await page.getByTestId(`bot-delete-${botName}`).click(); + const updatedBotRow = page + .getByRole('row') + .filter({ + has: page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`), + }); + + await updatedBotRow.locator('[data-testid^="bot-delete-"]').click(); await page.getByTestId('hard-delete-option').click(); @@ -120,7 +125,9 @@ export const deleteBot = async (page: Page) => { await toastNotification(page, /deleted successfully!/); - await expect(page.locator('.ant-table-tbody')).not.toContainText(botName); + await expect( + page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`) + ).toHaveCount(0); }; export const updateBotDetails = async (page: Page) => { @@ -156,11 +163,13 @@ export const updateBotDetails = async (page: Page) => { const updatedBotRow = page .getByRole('row') - .filter({ has: page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`) }); + .filter({ + has: page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`), + }); - await expect( - updatedBotRow.getByTestId('markdown-parser') - ).toContainText(BOT_DETAILS.updatedDescription); + await expect(updatedBotRow.getByTestId('markdown-parser')).toContainText( + BOT_DETAILS.updatedDescription + ); }; export const verifyBotSearch = async (page: Page) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index d1c671d8b995..be7ddfc8a10b 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -122,30 +122,6 @@ const BotListV1 = ({ }; }, []); - const getLocalSearchMatchedBots = useCallback( - (text: string) => { - const normalizedSearchText = text.trim().toLowerCase(); - - return botUsers.filter((bot) => { - const searchableFields = [ - bot.name, - bot.fullyQualifiedName, - bot.displayName, - bot.description, - bot.botUser?.name, - bot.botUser?.fullyQualifiedName, - bot.botUser?.displayName, - (bot.botUser as unknown as User | undefined)?.email, - ]; - - return searchableFields.some((field) => - field?.toLowerCase().includes(normalizedSearchText) - ); - }); - }, - [botUsers] - ); - const enrichBotsWithBotUsers = async (bots: Bot[]) => { if (!bots.length) { return bots; @@ -248,12 +224,6 @@ const BotListV1 = ({ }; const searchBots = async (text: string) => { - const localMatchedBots = getLocalSearchMatchedBots(text); - - if (localMatchedBots.length) { - return localMatchedBots; - } - const getMatchedBots = async (matchedBotUsers: User[]) => { const matchedBotUserNames = Array.from( new Set( From 1ec1666428a3c573ec102a8989b453a9984bb5e1 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Mon, 20 Apr 2026 16:53:00 +0530 Subject: [PATCH 16/24] chore: revert out-of-scope bot Playwright test changes --- .../ui/playwright/e2e/Pages/Bots.spec.ts | 5 - .../main/resources/ui/playwright/utils/bot.ts | 92 +++---------------- 2 files changed, 15 insertions(+), 82 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts index e926c92bcb35..fe1a4b195c39 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts @@ -19,7 +19,6 @@ import { tokenExpirationForDays, tokenExpirationUnlimitedDays, updateBotDetails, - verifyBotSearch, verifyGenerateTokenAPIContract, } from '../../utils/bot'; @@ -49,10 +48,6 @@ test.describe( await updateBotDetails(page); }); - await test.step('Verify bot search works by name and email', async () => { - await verifyBotSearch(page); - }); - await test.step('Verify generateToken API contract', async () => { await verifyGenerateTokenAPIContract(page); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index be184da40e0b..238f185221a8 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -23,12 +23,11 @@ import { waitForAllLoadersToDisappear } from './entity'; import { settingClick } from './sidebar'; import { revokeToken } from './user'; -const botName = `test-bot-${uuid()}`; -const botEmailPrefix = `hello-${uuid()}`; +const botName = `a-bot-pw%test-${uuid()}`; const BOT_DETAILS = { botName: botName, - botEmail: `${botEmailPrefix}@open-metadata.org`, + botEmail: `${botName}@mail.com`, description: `This is bot description for ${botName}`, updatedDescription: `This is updated bot description for ${botName}`, updatedBotName: `updated-${botName}`, @@ -49,9 +48,11 @@ export const getCreatedBot = async ( } ) => { // Click on created Bot name + const fetchResponse = page.waitForResponse( + `/api/v1/bots/name/${encodeURIComponent(botName)}?*` + ); await page.getByTestId(`bot-link-${botDisplayName ?? botName}`).click(); - - await expect(page.getByTestId('token-expiry')).toBeVisible(); + await fetchResponse; }; export const createBot = async (page: Page) => { @@ -69,16 +70,9 @@ export const createBot = async (page: Page) => { await page.locator(descriptionBox).fill(BOT_DETAILS.description); - const saveResponse = page.waitForResponse( - (response) => - response.url().includes('/api/v1/bots') && - response.request().method() === 'POST' - ); + const saveResponse = page.waitForResponse('/api/v1/bots'); await page.click('[data-testid="save-user"]'); - const createBotResponse = await saveResponse; - - expect(createBotResponse.status()).toBeGreaterThanOrEqual(200); - expect(createBotResponse.status()).toBeLessThan(300); + await saveResponse; // Verify bot is getting added in the bots listing page await expect( @@ -89,6 +83,7 @@ export const createBot = async (page: Page) => { page.getByRole('cell', { name: BOT_DETAILS.description }) ).toBeVisible(); + // Get created bot await getCreatedBot(page, { botName }); await expect(page.getByTestId('revoke-button')).toContainText('Revoke token'); @@ -105,13 +100,8 @@ export const createBot = async (page: Page) => { export const deleteBot = async (page: Page) => { await settingClick(page, GlobalSettingOptions.BOTS); - const updatedBotRow = page - .getByRole('row') - .filter({ - has: page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`), - }); - - await updatedBotRow.locator('[data-testid^="bot-delete-"]').click(); + // Click on delete button + await page.getByTestId(`bot-delete-${botName}`).click(); await page.getByTestId('hard-delete-option').click(); @@ -125,9 +115,7 @@ export const deleteBot = async (page: Page) => { await toastNotification(page, /deleted successfully!/); - await expect( - page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`) - ).toHaveCount(0); + await expect(page.locator('.ant-table-tbody')).not.toContainText(botName); }; export const updateBotDetails = async (page: Page) => { @@ -161,59 +149,9 @@ export const updateBotDetails = async (page: Page) => { page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`) ).toContainText(BOT_DETAILS.updatedBotName); - const updatedBotRow = page - .getByRole('row') - .filter({ - has: page.getByTestId(`bot-link-${BOT_DETAILS.updatedBotName}`), - }); - - await expect(updatedBotRow.getByTestId('markdown-parser')).toContainText( - BOT_DETAILS.updatedDescription - ); -}; - -export const verifyBotSearch = async (page: Page) => { - const searchInput = page.getByTestId('searchbar'); - const createdBotLink = page.getByTestId( - `bot-link-${BOT_DETAILS.updatedBotName}` - ); - const nonMatchingSearchTerm = `${BOT_DETAILS.updatedBotName}-no-match`; - const uniqueNameToken = BOT_DETAILS.updatedBotName.split('-').slice(-1)[0]; - const uniqueEmailToken = BOT_DETAILS.botEmail.split('@')[0]; - - const searchBot = async (searchTerm: string) => { - await searchInput.clear(); - await searchInput.fill(searchTerm); - await expect(searchInput).toHaveValue(searchTerm); - }; - - await searchBot(BOT_DETAILS.updatedBotName); - await expect(createdBotLink).toBeVisible(); - - await searchBot(nonMatchingSearchTerm); - await expect(createdBotLink).toHaveCount(0); - - await searchBot(BOT_DETAILS.botEmail); - await expect(createdBotLink).toBeVisible(); - - await searchBot(nonMatchingSearchTerm); - await expect(createdBotLink).toHaveCount(0); - - await searchBot(uniqueNameToken); - await expect(createdBotLink).toBeVisible(); - - await searchBot(nonMatchingSearchTerm); - await expect(createdBotLink).toHaveCount(0); - - await searchBot(uniqueEmailToken); - await expect(createdBotLink).toBeVisible(); - - await searchBot(nonMatchingSearchTerm); - await expect(createdBotLink).toHaveCount(0); - - await searchInput.clear(); - await expect(searchInput).toHaveValue(''); - await expect(createdBotLink).toBeVisible(); + await expect( + page.locator(`[data-row-key="${botName}"] [data-testid="markdown-parser"]`) + ).toContainText(BOT_DETAILS.updatedDescription); }; export const tokenExpirationForDays = async (page: Page) => { From d61e5df4048ddbe5c501e6f61c1902a3f40be866 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Mon, 20 Apr 2026 17:18:08 +0530 Subject: [PATCH 17/24] test: add bot search e2e coverage and tighten bot API response assertions --- .../ui/playwright/e2e/Pages/Bots.spec.ts | 5 +++ .../main/resources/ui/playwright/utils/bot.ts | 42 +++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts index fe1a4b195c39..e926c92bcb35 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/e2e/Pages/Bots.spec.ts @@ -19,6 +19,7 @@ import { tokenExpirationForDays, tokenExpirationUnlimitedDays, updateBotDetails, + verifyBotSearch, verifyGenerateTokenAPIContract, } from '../../utils/bot'; @@ -48,6 +49,10 @@ test.describe( await updateBotDetails(page); }); + await test.step('Verify bot search works by name and email', async () => { + await verifyBotSearch(page); + }); + await test.step('Verify generateToken API contract', async () => { await verifyGenerateTokenAPIContract(page); }); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 238f185221a8..de76b97b3bab 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -70,9 +70,15 @@ export const createBot = async (page: Page) => { await page.locator(descriptionBox).fill(BOT_DETAILS.description); - const saveResponse = page.waitForResponse('/api/v1/bots'); + const saveResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/bots') && + response.request().method() === 'POST' + ); await page.click('[data-testid="save-user"]'); - await saveResponse; + const createBotResponse = await saveResponse; + + expect(createBotResponse.status()).toBe(201); // Verify bot is getting added in the bots listing page await expect( @@ -115,7 +121,10 @@ export const deleteBot = async (page: Page) => { await toastNotification(page, /deleted successfully!/); - await expect(page.locator('.ant-table-tbody')).not.toContainText(botName); + await page.getByTestId('searchbar').clear(); + await page.getByTestId('searchbar').fill(BOT_DETAILS.updatedBotName); + await waitForAllLoadersToDisappear(page); + await expect(page.getByText(/No data found!?/i)).toBeVisible(); }; export const updateBotDetails = async (page: Page) => { @@ -154,6 +163,33 @@ export const updateBotDetails = async (page: Page) => { ).toContainText(BOT_DETAILS.updatedDescription); }; +export const verifyBotSearch = async (page: Page) => { + const searchInput = page.getByTestId('searchbar'); + const createdBotLink = page.getByTestId( + `bot-link-${BOT_DETAILS.updatedBotName}` + ); + + const searchBot = async (searchTerm: string) => { + await searchInput.clear(); + await searchInput.fill(searchTerm); + await expect(searchInput).toHaveValue(searchTerm); + await waitForAllLoadersToDisappear(page); + }; + + await searchBot(BOT_DETAILS.updatedBotName); + await expect(createdBotLink).toBeVisible(); + + await searchBot(BOT_DETAILS.botEmail); + await expect(createdBotLink).toBeVisible(); + + await searchBot(`${BOT_DETAILS.updatedBotName}-no-match`); + await expect(page.getByText(/No data found!?/i)).toBeVisible(); + + await searchInput.clear(); + await waitForAllLoadersToDisappear(page); + await expect(createdBotLink).toBeVisible(); +}; + export const tokenExpirationForDays = async (page: Page) => { await getCreatedBot(page, { botName, From 9482345f3c5af2738742cc94267dd3d1f11712bd Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 24 Apr 2026 13:01:46 +0530 Subject: [PATCH 18/24] test: stabilize bot search no-match assertions using filter placeholder testid --- openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index de76b97b3bab..3765262e28bc 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -124,7 +124,7 @@ export const deleteBot = async (page: Page) => { await page.getByTestId('searchbar').clear(); await page.getByTestId('searchbar').fill(BOT_DETAILS.updatedBotName); await waitForAllLoadersToDisappear(page); - await expect(page.getByText(/No data found!?/i)).toBeVisible(); + await expect(page.getByTestId('search-error-placeholder')).toBeVisible(); }; export const updateBotDetails = async (page: Page) => { @@ -183,7 +183,7 @@ export const verifyBotSearch = async (page: Page) => { await expect(createdBotLink).toBeVisible(); await searchBot(`${BOT_DETAILS.updatedBotName}-no-match`); - await expect(page.getByText(/No data found!?/i)).toBeVisible(); + await expect(page.getByTestId('search-error-placeholder')).toBeVisible(); await searchInput.clear(); await waitForAllLoadersToDisappear(page); From 9b4f6c0756ca05e886bc4b1acfd6af6f6d17114c Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 24 Apr 2026 15:27:35 +0530 Subject: [PATCH 19/24] fix: add search API wait in bot Playwright flow and refactor bot-user mapping helper --- .../main/resources/ui/playwright/utils/bot.ts | 17 +++++++++++------ .../resources/ui/playwright/utils/common.ts | 11 +++++++++++ .../Bot/BotListV1/BotListV1.component.tsx | 14 +++++++++----- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 3765262e28bc..3caa6560282e 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -16,6 +16,7 @@ import { descriptionBox, redirectToHomePage, toastNotification, + updateSearchInputAndWait, uuid, } from './common'; import { customFormatDateTime, getEpochMillisForFutureDays } from './dateTime'; @@ -170,10 +171,15 @@ export const verifyBotSearch = async (page: Page) => { ); const searchBot = async (searchTerm: string) => { - await searchInput.clear(); - await searchInput.fill(searchTerm); - await expect(searchInput).toHaveValue(searchTerm); - await waitForAllLoadersToDisappear(page); + const searchResponse = page.waitForResponse( + (response) => + response.url().includes('/api/v1/search/query') && + response.request().method() === 'GET' && + response.url().includes(encodeURIComponent(searchTerm)) + ); + + await updateSearchInputAndWait(page, searchInput, searchTerm); + await searchResponse; }; await searchBot(BOT_DETAILS.updatedBotName); @@ -185,8 +191,7 @@ export const verifyBotSearch = async (page: Page) => { await searchBot(`${BOT_DETAILS.updatedBotName}-no-match`); await expect(page.getByTestId('search-error-placeholder')).toBeVisible(); - await searchInput.clear(); - await waitForAllLoadersToDisappear(page); + await updateSearchInputAndWait(page, searchInput, ''); await expect(createdBotLink).toBeVisible(); }; diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 8d152be44215..897a89c6f829 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -197,6 +197,17 @@ export const clickOutside = async (page: Page) => { }); }; +export const updateSearchInputAndWait = async ( + page: Page, + searchInput: Locator, + value: string +) => { + await searchInput.clear(); + await searchInput.fill(value); + await expect(searchInput).toHaveValue(value); + await waitForAllLoadersToDisappear(page); +}; + export const visitOwnProfilePage = async (page: Page) => { await page.locator('[data-testid="dropdown-profile"] svg').click(); await page.locator('[role="menu"].profile-dropdown').waitFor({ diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index be7ddfc8a10b..05828575ba9d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -104,6 +104,14 @@ const BotListV1 = ({ [] ); + const getBotUserFromUser = (botUser: User): NonNullable => ({ + id: botUser.id, + name: botUser.name, + displayName: botUser.displayName, + fullyQualifiedName: botUser.fullyQualifiedName, + email: botUser.email, + }); + const enrichBotWithMatchedUser = useCallback((bot: Bot, botUser?: User) => { if (!botUser) { return bot; @@ -113,11 +121,7 @@ const BotListV1 = ({ ...bot, botUser: { ...(bot.botUser ?? {}), - id: botUser.id, - name: botUser.name, - displayName: botUser.displayName, - fullyQualifiedName: botUser.fullyQualifiedName, - email: botUser.email, + ...getBotUserFromUser(botUser), } as Bot['botUser'], }; }, []); From eac9242d86b7aa60b7e751eee4a4a1fbc9f62820 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 24 Apr 2026 15:33:16 +0530 Subject: [PATCH 20/24] fix: extract reusable searchbar helper with search API wait and deduplicate bot user mapping logic --- .../main/resources/ui/playwright/utils/bot.ts | 30 ++++++++-------- .../resources/ui/playwright/utils/common.ts | 34 +++++++++++++++++++ 2 files changed, 49 insertions(+), 15 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 3caa6560282e..353760e89cb1 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -15,6 +15,7 @@ import { GlobalSettingOptions } from '../constant/settings'; import { descriptionBox, redirectToHomePage, + searchFromSearchInput, toastNotification, updateSearchInputAndWait, uuid, @@ -170,25 +171,24 @@ export const verifyBotSearch = async (page: Page) => { `bot-link-${BOT_DETAILS.updatedBotName}` ); - const searchBot = async (searchTerm: string) => { - const searchResponse = page.waitForResponse( - (response) => - response.url().includes('/api/v1/search/query') && - response.request().method() === 'GET' && - response.url().includes(encodeURIComponent(searchTerm)) - ); - - await updateSearchInputAndWait(page, searchInput, searchTerm); - await searchResponse; - }; - - await searchBot(BOT_DETAILS.updatedBotName); + await searchFromSearchInput(page, searchInput, BOT_DETAILS.updatedBotName, { + waitForSearchApi: true, + }); await expect(createdBotLink).toBeVisible(); - await searchBot(BOT_DETAILS.botEmail); + await searchFromSearchInput(page, searchInput, BOT_DETAILS.botEmail, { + waitForSearchApi: true, + }); await expect(createdBotLink).toBeVisible(); - await searchBot(`${BOT_DETAILS.updatedBotName}-no-match`); + await searchFromSearchInput( + page, + searchInput, + `${BOT_DETAILS.updatedBotName}-no-match`, + { + waitForSearchApi: true, + } + ); await expect(page.getByTestId('search-error-placeholder')).toBeVisible(); await updateSearchInputAndWait(page, searchInput, ''); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 897a89c6f829..565d26bb56be 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -208,6 +208,40 @@ export const updateSearchInputAndWait = async ( await waitForAllLoadersToDisappear(page); }; +interface SearchInputOptions { + waitForSearchApi?: boolean; + searchApiPattern?: string; +} + +export const searchFromSearchInput = async ( + page: Page, + searchInput: Locator, + searchTerm: string, + options: SearchInputOptions = {} +) => { + const { + waitForSearchApi = false, + searchApiPattern = '/api/v1/search/query', + } = options; + + const searchResponsePromise = + waitForSearchApi && searchTerm + ? page.waitForResponse( + (response) => + response.url().includes(searchApiPattern) && + response.request().method() === 'GET' && + response.url().includes(encodeURIComponent(searchTerm)) + ) + : undefined; + + await updateSearchInputAndWait(page, searchInput, searchTerm); + + if (searchResponsePromise) { + const searchResponse = await searchResponsePromise; + expect(searchResponse.status()).toBe(200); + } +}; + export const visitOwnProfilePage = async (page: Page) => { await page.locator('[data-testid="dropdown-profile"] svg').click(); await page.locator('[role="menu"].profile-dropdown').waitFor({ From 0d75ae2defeca8348043d0c44db66ca8c1c2abd0 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Wed, 29 Apr 2026 17:56:01 +0530 Subject: [PATCH 21/24] fix(playwright): make bot search test stable by removing brittle API wait --- .../src/main/resources/ui/playwright/utils/bot.ts | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 353760e89cb1..60dee39ec24a 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -171,23 +171,16 @@ export const verifyBotSearch = async (page: Page) => { `bot-link-${BOT_DETAILS.updatedBotName}` ); - await searchFromSearchInput(page, searchInput, BOT_DETAILS.updatedBotName, { - waitForSearchApi: true, - }); + await searchFromSearchInput(page, searchInput, BOT_DETAILS.updatedBotName); await expect(createdBotLink).toBeVisible(); - await searchFromSearchInput(page, searchInput, BOT_DETAILS.botEmail, { - waitForSearchApi: true, - }); + await searchFromSearchInput(page, searchInput, BOT_DETAILS.botEmail); await expect(createdBotLink).toBeVisible(); await searchFromSearchInput( page, searchInput, - `${BOT_DETAILS.updatedBotName}-no-match`, - { - waitForSearchApi: true, - } + `${BOT_DETAILS.updatedBotName}-no-match` ); await expect(page.getByTestId('search-error-placeholder')).toBeVisible(); From 13fbb2195696622ea4e9d18b5d0d45291453b551 Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Thu, 30 Apr 2026 18:34:56 +0530 Subject: [PATCH 22/24] Refactor bot search integration and Playwright synchronization for reliable name/email query behavior --- .../main/resources/ui/playwright/utils/bot.ts | 11 +++-- .../resources/ui/playwright/utils/common.ts | 29 ++++++------- .../Bot/BotListV1/BotListV1.component.tsx | 42 ++++++++++--------- .../src/main/resources/ui/src/rest/botsAPI.ts | 11 +++++ .../src/main/resources/ui/src/rest/userAPI.ts | 9 ---- 5 files changed, 57 insertions(+), 45 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts index 60dee39ec24a..8a787b159beb 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/bot.ts @@ -171,16 +171,21 @@ export const verifyBotSearch = async (page: Page) => { `bot-link-${BOT_DETAILS.updatedBotName}` ); - await searchFromSearchInput(page, searchInput, BOT_DETAILS.updatedBotName); + await searchFromSearchInput(page, searchInput, BOT_DETAILS.updatedBotName, { + waitForSearchApi: true, + }); await expect(createdBotLink).toBeVisible(); - await searchFromSearchInput(page, searchInput, BOT_DETAILS.botEmail); + await searchFromSearchInput(page, searchInput, BOT_DETAILS.botEmail, { + waitForSearchApi: true, + }); await expect(createdBotLink).toBeVisible(); await searchFromSearchInput( page, searchInput, - `${BOT_DETAILS.updatedBotName}-no-match` + `${BOT_DETAILS.updatedBotName}-no-match`, + { waitForSearchApi: true } ); await expect(page.getByTestId('search-error-placeholder')).toBeVisible(); diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index d8ce43053fb5..3c1ed62656ca 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -209,10 +209,10 @@ export const updateSearchInputAndWait = async ( await waitForAllLoadersToDisappear(page); }; -interface SearchInputOptions { +type SearchInputOptions = { waitForSearchApi?: boolean; searchApiPattern?: string; -} +}; export const searchFromSearchInput = async ( page: Page, @@ -225,22 +225,23 @@ export const searchFromSearchInput = async ( searchApiPattern = '/api/v1/search/query', } = options; - const searchResponsePromise = - waitForSearchApi && searchTerm - ? page.waitForResponse( - (response) => - response.url().includes(searchApiPattern) && - response.request().method() === 'GET' && - response.url().includes(encodeURIComponent(searchTerm)) - ) - : undefined; + const searchResponsePromise = waitForSearchApi + ? page.waitForResponse( + (response) => + response.url().includes(searchApiPattern) && + response.request().method() === 'GET' && + response.url().includes(encodeURIComponent(searchTerm)) + ) + : undefined; await updateSearchInputAndWait(page, searchInput, searchTerm); - if (searchResponsePromise) { - const searchResponse = await searchResponsePromise; - expect(searchResponse.status()).toBe(200); + if (!searchResponsePromise) { + return; } + + const searchResponse = await searchResponsePromise; + expect(searchResponse.status()).toBe(200); }; export const visitOwnProfilePage = async (page: Page) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 05828575ba9d..c52dec0c2081 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -35,9 +35,8 @@ import { Paging } from '../../../../generated/type/paging'; import LimitWrapper from '../../../../hoc/LimitWrapper'; import { useAuth } from '../../../../hooks/authHooks'; import { usePaging } from '../../../../hooks/paging/usePaging'; -import { getBots } from '../../../../rest/botsAPI'; +import { getBotByName, getBots } from '../../../../rest/botsAPI'; import { searchQuery } from '../../../../rest/searchAPI'; -import { getBotByName } from '../../../../rest/userAPI'; import { formatUsersResponse } from '../../../../utils/APIUtils'; import { getEntityName, @@ -63,6 +62,12 @@ import { TitleBreadcrumbProps } from '../../../common/TitleBreadcrumb/TitleBread import PageHeader from '../../../PageHeader/PageHeader.component'; import './bot-list-v1.less'; import { BotListV1Props } from './BotListV1.interfaces'; + +const BOT_SEARCH_PAGE_SIZE = 100; +const BOT_SEARCH_CONCURRENCY = 10; +const MAX_BOT_SEARCH_PAGES = 5; +const MAX_BOT_USER_RESOLUTION = BOT_SEARCH_PAGE_SIZE * MAX_BOT_SEARCH_PAGES; + const BotListV1 = ({ showDeleted, handleAddBotClick, @@ -89,10 +94,6 @@ const BotListV1 = ({ const [searchedData, setSearchedData] = useState([]); const [searchTerm, setSearchTerm] = useState(''); const latestSearchRequest = useRef(0); - const BOT_SEARCH_PAGE_SIZE = 100; - const BOT_SEARCH_CONCURRENCY = 10; - const MAX_BOT_SEARCH_PAGES = 5; - const MAX_BOT_USER_RESOLUTION = BOT_SEARCH_PAGE_SIZE * MAX_BOT_SEARCH_PAGES; const getBotIncludeFilter = useCallback( () => (showDeleted ? Include.Deleted : Include.NonDeleted), @@ -112,19 +113,22 @@ const BotListV1 = ({ email: botUser.email, }); - const enrichBotWithMatchedUser = useCallback((bot: Bot, botUser?: User) => { - if (!botUser) { - return bot; - } + const enrichBotWithMatchedUser = useCallback( + (bot: Bot, botUser?: User) => { + if (!botUser) { + return bot; + } - return { - ...bot, - botUser: { - ...(bot.botUser ?? {}), - ...getBotUserFromUser(botUser), - } as Bot['botUser'], - }; - }, []); + return { + ...bot, + botUser: { + ...(bot.botUser ?? {}), + ...getBotUserFromUser(botUser), + } as Bot['botUser'], + }; + }, + [getBotUserFromUser] + ); const enrichBotsWithBotUsers = async (bots: Bot[]) => { if (!bots.length) { @@ -415,7 +419,7 @@ const BotListV1 = ({ }, }, ], - [] + [t, searchTerm, isAdminUser] ); /** diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts index aeb271b9e696..8b2272fd2697 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts @@ -16,6 +16,8 @@ import axiosClient from '.'; import { CreateBot } from '../generated/api/createBot'; import { Bot } from '../generated/entity/bot'; import { Include } from '../generated/type/include'; +import { ListParams } from '../interface/API.interface'; +import { getEncodedFqn } from '../utils/StringsUtils'; import { Paging } from '../generated/type/paging'; const BASE_URL = '/bots'; @@ -46,3 +48,12 @@ export const createBot = async (data: CreateBot) => { return response.data; }; + +export const getBotByName = async (name: string, params?: ListParams) => { + const response = await axiosClient.get( + `${BASE_URL}/name/${getEncodedFqn(name)}`, + { params } + ); + + return response.data; +}; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts index 7e8c6209627a..de5a529d1464 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts @@ -149,15 +149,6 @@ export const getAuthMechanismForBotUser = async (botId: string) => { return response.data; }; -export const getBotByName = async (name: string, params?: ListParams) => { - const response = await APIClient.get( - `/bots/name/${getEncodedFqn(name)}`, - { params } - ); - - return response.data; -}; - export const updateBotDetail = async (id: string, data: Operation[]) => { const response = await APIClient.patch>( `/bots/${id}`, From 3f5166416c73f0d216a9f693b1101e936cd39b2e Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 1 May 2026 11:07:38 +0530 Subject: [PATCH 23/24] Fix bot search regressions by preserving getBotByName compatibility export, stabilizing BotListV1 memoized enrichment, and skipping Playwright search API wait for empty terms --- .../resources/ui/playwright/utils/common.ts | 20 ++++++++------- .../Bot/BotListV1/BotListV1.component.tsx | 25 +++++++++---------- .../src/main/resources/ui/src/rest/userAPI.ts | 5 ++-- 3 files changed, 26 insertions(+), 24 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index 3c1ed62656ca..e9d5dc2ed6de 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -224,15 +224,17 @@ export const searchFromSearchInput = async ( waitForSearchApi = false, searchApiPattern = '/api/v1/search/query', } = options; - - const searchResponsePromise = waitForSearchApi - ? page.waitForResponse( - (response) => - response.url().includes(searchApiPattern) && - response.request().method() === 'GET' && - response.url().includes(encodeURIComponent(searchTerm)) - ) - : undefined; + const normalizedSearchTerm = searchTerm.trim(); + + const searchResponsePromise = + waitForSearchApi && normalizedSearchTerm + ? page.waitForResponse( + (response) => + response.url().includes(searchApiPattern) && + response.request().method() === 'GET' && + response.url().includes(encodeURIComponent(normalizedSearchTerm)) + ) + : undefined; await updateSearchInputAndWait(page, searchInput, searchTerm); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index c52dec0c2081..52a9b93b5832 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -39,15 +39,15 @@ import { getBotByName, getBots } from '../../../../rest/botsAPI'; import { searchQuery } from '../../../../rest/searchAPI'; import { formatUsersResponse } from '../../../../utils/APIUtils'; import { - getEntityName, - highlightSearchText, + getEntityName, + highlightSearchText } from '../../../../utils/EntityUtils'; import { getSettingPageEntityBreadCrumb } from '../../../../utils/GlobalSettingsUtils'; import { getBotsPath } from '../../../../utils/RouterUtils'; import { getTermQuery } from '../../../../utils/SearchUtils'; import { - escapeESReservedCharacters, - stringToHTML, + escapeESReservedCharacters, + stringToHTML } from '../../../../utils/StringsUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; import DeleteWidgetModal from '../../../common/DeleteWidget/DeleteWidgetModal'; @@ -67,6 +67,13 @@ const BOT_SEARCH_PAGE_SIZE = 100; const BOT_SEARCH_CONCURRENCY = 10; const MAX_BOT_SEARCH_PAGES = 5; const MAX_BOT_USER_RESOLUTION = BOT_SEARCH_PAGE_SIZE * MAX_BOT_SEARCH_PAGES; +const getBotUserFromUser = (botUser: User): NonNullable => ({ + id: botUser.id, + name: botUser.name, + displayName: botUser.displayName, + fullyQualifiedName: botUser.fullyQualifiedName, + email: botUser.email, +}); const BotListV1 = ({ showDeleted, @@ -105,14 +112,6 @@ const BotListV1 = ({ [] ); - const getBotUserFromUser = (botUser: User): NonNullable => ({ - id: botUser.id, - name: botUser.name, - displayName: botUser.displayName, - fullyQualifiedName: botUser.fullyQualifiedName, - email: botUser.email, - }); - const enrichBotWithMatchedUser = useCallback( (bot: Bot, botUser?: User) => { if (!botUser) { @@ -127,7 +126,7 @@ const BotListV1 = ({ } as Bot['botUser'], }; }, - [getBotUserFromUser] + [] ); const enrichBotsWithBotUsers = async (bots: Bot[]) => { diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts index de5a529d1464..c2cdff24d28d 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts @@ -16,8 +16,8 @@ import { Operation } from 'fast-json-patch'; import { PagingResponse, RestoreRequestType } from 'Models'; import { - AuthenticationMechanism, - CreateUser, + AuthenticationMechanism, + CreateUser } from '../generated/api/teams/createUser'; import { PersonalAccessToken } from '../generated/auth/personalAccessToken'; import { Bot } from '../generated/entity/bot'; @@ -26,6 +26,7 @@ import { Include } from '../generated/type/include'; import { ListParams } from '../interface/API.interface'; import { getEncodedFqn } from '../utils/StringsUtils'; import APIClient from './index'; +export { getBotByName } from './botsAPI'; export interface UsersQueryParams { fields?: string; From e6b17aafae7ed9d8c389697e5a96ceabd5725baf Mon Sep 17 00:00:00 2001 From: Siddhi Gupta Date: Fri, 1 May 2026 13:40:42 +0530 Subject: [PATCH 24/24] fix: stabilize bot search Playwright API wait to prevent encoded-query timeout flakes --- .../resources/ui/playwright/utils/common.ts | 3 +- .../Bot/BotListV1/BotListV1.component.tsx | 35 +++++++++---------- .../src/main/resources/ui/src/rest/botsAPI.ts | 2 +- .../src/main/resources/ui/src/rest/userAPI.ts | 5 +-- 4 files changed, 21 insertions(+), 24 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts index e9d5dc2ed6de..b892646c92b2 100644 --- a/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts +++ b/openmetadata-ui/src/main/resources/ui/playwright/utils/common.ts @@ -231,8 +231,7 @@ export const searchFromSearchInput = async ( ? page.waitForResponse( (response) => response.url().includes(searchApiPattern) && - response.request().method() === 'GET' && - response.url().includes(encodeURIComponent(normalizedSearchTerm)) + response.request().method() === 'GET' ) : undefined; diff --git a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx index 52a9b93b5832..40527c21fd5f 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/Settings/Bot/BotListV1/BotListV1.component.tsx @@ -39,15 +39,15 @@ import { getBotByName, getBots } from '../../../../rest/botsAPI'; import { searchQuery } from '../../../../rest/searchAPI'; import { formatUsersResponse } from '../../../../utils/APIUtils'; import { - getEntityName, - highlightSearchText + getEntityName, + highlightSearchText, } from '../../../../utils/EntityUtils'; import { getSettingPageEntityBreadCrumb } from '../../../../utils/GlobalSettingsUtils'; import { getBotsPath } from '../../../../utils/RouterUtils'; import { getTermQuery } from '../../../../utils/SearchUtils'; import { - escapeESReservedCharacters, - stringToHTML + escapeESReservedCharacters, + stringToHTML, } from '../../../../utils/StringsUtils'; import { showErrorToast } from '../../../../utils/ToastUtils'; import DeleteWidgetModal from '../../../common/DeleteWidget/DeleteWidgetModal'; @@ -112,22 +112,19 @@ const BotListV1 = ({ [] ); - const enrichBotWithMatchedUser = useCallback( - (bot: Bot, botUser?: User) => { - if (!botUser) { - return bot; - } + const enrichBotWithMatchedUser = useCallback((bot: Bot, botUser?: User) => { + if (!botUser) { + return bot; + } - return { - ...bot, - botUser: { - ...(bot.botUser ?? {}), - ...getBotUserFromUser(botUser), - } as Bot['botUser'], - }; - }, - [] - ); + return { + ...bot, + botUser: { + ...(bot.botUser ?? {}), + ...getBotUserFromUser(botUser), + } as Bot['botUser'], + }; + }, []); const enrichBotsWithBotUsers = async (bots: Bot[]) => { if (!bots.length) { diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts index 8b2272fd2697..60e931313466 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/botsAPI.ts @@ -16,9 +16,9 @@ import axiosClient from '.'; import { CreateBot } from '../generated/api/createBot'; import { Bot } from '../generated/entity/bot'; import { Include } from '../generated/type/include'; +import { Paging } from '../generated/type/paging'; import { ListParams } from '../interface/API.interface'; import { getEncodedFqn } from '../utils/StringsUtils'; -import { Paging } from '../generated/type/paging'; const BASE_URL = '/bots'; diff --git a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts index c2cdff24d28d..709f2e4a02df 100644 --- a/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts +++ b/openmetadata-ui/src/main/resources/ui/src/rest/userAPI.ts @@ -16,8 +16,8 @@ import { Operation } from 'fast-json-patch'; import { PagingResponse, RestoreRequestType } from 'Models'; import { - AuthenticationMechanism, - CreateUser + AuthenticationMechanism, + CreateUser, } from '../generated/api/teams/createUser'; import { PersonalAccessToken } from '../generated/auth/personalAccessToken'; import { Bot } from '../generated/entity/bot'; @@ -26,6 +26,7 @@ import { Include } from '../generated/type/include'; import { ListParams } from '../interface/API.interface'; import { getEncodedFqn } from '../utils/StringsUtils'; import APIClient from './index'; + export { getBotByName } from './botsAPI'; export interface UsersQueryParams {