From b7eee76c684a22e0527f4b46d02ad8b1669ab1c5 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Thu, 20 Aug 2026 14:58:22 +0530 Subject: [PATCH 1/7] fix(ui): surface domain tree search failures instead of rejecting MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `onSearch` in both domain trees awaited `searchDomains` inside a `try/finally` with no `catch`. The callback is async and is invoked by `debounce`, so nobody awaits it and a rejected request escaped as an unhandled promise rejection: no error toast, loader silently cleared, and a bare minified `AxiosError` in the browser error reporter. The sibling calls in the same component (`fetchAPI`, `loadChildDomains`) already toast, so the search path was the outlier. Also add the missing single-`|` entry to `ES_RESERVED_CHARACTERS`. The map is applied one character at a time, so its `'||'` key never matched and a lone pipe reached OpenSearch unescaped — `a||||b` becomes `q=*a||||b*`, consecutive OR operators, a `query_string` parse error, i.e. the 400 above. `'&'` was already mapped individually alongside `'&&'`; `'|'` was simply missing. Fixes #31822 Co-Authored-By: Claude Opus 5 (1M context) --- .../DomainSelectableTree.test.tsx | 28 +++++++++++++++- .../DomainSelectableTree.tsx | 2 ++ .../DomainSelectableTreeNew.tsx | 2 ++ .../ui/src/utils/StringUtils.test.ts | 32 +++++++++++++++++++ .../resources/ui/src/utils/StringUtils.ts | 1 + 5 files changed, 64 insertions(+), 1 deletion(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx index a0c1cd83bf17..f6d8309d5650 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx @@ -10,13 +10,15 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { render, screen, waitFor } from '@testing-library/react'; +import { fireEvent, render, screen, waitFor } from '@testing-library/react'; +import { AxiosError } from 'axios'; import { DEFAULT_DOMAIN_VALUE } from '../../../constants/constants'; import { EntityType } from '../../../enums/entity.enum'; import { Domain, DomainType } from '../../../generated/entity/domains/domain'; import { EntityReference } from '../../../generated/entity/type'; import * as domainAPI from '../../../rest/domainAPI'; import { convertDomainsToTreeOptions } from '../../../utils/DomainUtils'; +import { showErrorToast } from '../../../utils/ToastUtils'; import DomainSelectableTree from './DomainSelectableTree'; const mockDomains: Domain[] = [ @@ -433,4 +435,28 @@ describe('DomainSelectableTree', () => { expect(screen.getByText('label.no-entity-available')).toBeInTheDocument(); }); }); + + it('should surface a failed search instead of rejecting unhandled', async () => { + const error = new AxiosError('Request failed with status code 400'); + jest.spyOn(domainAPI, 'searchDomains').mockRejectedValueOnce(error); + + renderComponent(); + + await waitFor(() => { + expect(screen.queryByText('Loader')).not.toBeInTheDocument(); + }); + + fireEvent.change(screen.getByTestId('searchbar'), { + target: { value: 'a||||b' }, + }); + + await waitFor(() => { + expect(showErrorToast).toHaveBeenCalledWith(error); + }); + + // the loader must clear so the tree is usable again after the failure + await waitFor(() => { + expect(screen.queryByText('Loader')).not.toBeInTheDocument(); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx index 5d062f6f0072..c942be6b5b78 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx @@ -475,6 +475,8 @@ const DomainSelectablTree: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); + } catch (error) { + showErrorToast(error as AxiosError); } finally { setIsLoading(false); } diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx index 7cc9b8ad576f..a8484c4ae031 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx @@ -473,6 +473,8 @@ const DomainSelectablTreeNew: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); + } catch (error) { + showErrorToast(error as AxiosError); } finally { setIsLoading(false); } diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts index 238d9dbccb1b..99a3e5a9c25e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts @@ -31,6 +31,7 @@ jest.mock('./i18next/LocalUtil', () => ({ import { AxiosError } from 'axios'; import { decodeHtmlEntities, + escapeESReservedCharacters, formatJsonString, getDecodedFqn, getEncodedFqn, @@ -452,4 +453,35 @@ describe('StringUtils', () => { expect(result).not.toContain('line-four'); }); }); + + describe('escapeESReservedCharacters', () => { + it('should escape every Lucene reserved character', () => { + expect(escapeESReservedCharacters('a+b-c=d&e')).toBe( + String.raw`a\+b\-c\=d\&e` + ); + expect(escapeESReservedCharacters('(a){b}[c]')).toBe( + String.raw`\(a\)\{b\}\[c\]` + ); + expect(escapeESReservedCharacters('a*b?c:d/e')).toBe( + String.raw`a\*b\?c\:d\/e` + ); + }); + + it('should escape a single pipe so consecutive pipes cannot form an OR operator', () => { + expect(escapeESReservedCharacters('a|b')).toBe(String.raw`a\|b`); + expect(escapeESReservedCharacters('a||||b')).toBe( + String.raw`a\|\|\|\|b` + ); + }); + + it('should leave a plain term untouched', () => { + expect(escapeESReservedCharacters('Customer Support')).toBe( + 'Customer Support' + ); + }); + + it('should return an empty string for an undefined term', () => { + expect(escapeESReservedCharacters()).toBe(''); + }); + }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.ts index 3d5660be117a..8388967a378e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.ts @@ -267,6 +267,7 @@ export const ES_RESERVED_CHARACTERS: Record = { '=': String.raw`\=`, '&': String.raw`\&`, '&&': String.raw`\&&`, + '|': String.raw`\|`, '||': String.raw`\||`, '>': String.raw`\>`, '<': String.raw`\<`, From d09f477fef5b9328b204d5ff2990ab2404baf745 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Thu, 20 Aug 2026 17:33:23 +0530 Subject: [PATCH 2/7] fix checkstyle --- .../src/main/resources/ui/src/utils/StringUtils.test.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts b/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts index 99a3e5a9c25e..4eaea67e9536 100644 --- a/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts +++ b/openmetadata-ui/src/main/resources/ui/src/utils/StringUtils.test.ts @@ -469,9 +469,7 @@ describe('StringUtils', () => { it('should escape a single pipe so consecutive pipes cannot form an OR operator', () => { expect(escapeESReservedCharacters('a|b')).toBe(String.raw`a\|b`); - expect(escapeESReservedCharacters('a||||b')).toBe( - String.raw`a\|\|\|\|b` - ); + expect(escapeESReservedCharacters('a||||b')).toBe(String.raw`a\|\|\|\|b`); }); it('should leave a plain term untouched', () => { From c7cb139d8d8aba65222b27b031d538d2a1d5ba2b Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Thu, 20 Aug 2026 22:10:04 +0530 Subject: [PATCH 3/7] fix(ui): keep a failed domain search inside the dropdown MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: a toast is the wrong surface for a search that failed inside a selectable list — the user is looking at the list, so that is where the failure and the recovery belong. Replace the toast with an inline error state in the tree body, matching each file's existing empty state, plus a "Try again" that re-runs the same term. The error clears on the next search and when the search box is cleared (`fetchAPI`). `showErrorToast` stays on `fetchAPI` and `loadChildDomains` — those are the initial/child loads, not a search. Co-Authored-By: Claude Opus 5 (1M context) --- .../DomainSelectableTree.test.tsx | 59 +++++++++++++++++-- .../DomainSelectableTree.tsx | 35 ++++++++++- .../DomainSelectableTreeNew.tsx | 30 +++++++++- 3 files changed, 116 insertions(+), 8 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx index f6d8309d5650..8678d9c85476 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx @@ -436,7 +436,7 @@ describe('DomainSelectableTree', () => { }); }); - it('should surface a failed search instead of rejecting unhandled', async () => { + it('should show an inline error inside the list when a search fails', async () => { const error = new AxiosError('Request failed with status code 400'); jest.spyOn(domainAPI, 'searchDomains').mockRejectedValueOnce(error); @@ -450,13 +450,64 @@ describe('DomainSelectableTree', () => { target: { value: 'a||||b' }, }); + // the failure stays in the dropdown - no toast, and the loader clears await waitFor(() => { - expect(showErrorToast).toHaveBeenCalledWith(error); + expect(screen.getByTestId('domain-search-error')).toBeInTheDocument(); + }); + + expect(screen.getByTestId('retry-domain-search')).toBeInTheDocument(); + expect(screen.queryByText('Loader')).not.toBeInTheDocument(); + expect(showErrorToast).not.toHaveBeenCalled(); + }); + + it('should retry the search from the inline error and recover', async () => { + jest + .spyOn(domainAPI, 'searchDomains') + .mockRejectedValueOnce(new AxiosError('Request failed with status code 400')) + .mockResolvedValueOnce([mockDomains[0]]); + + renderComponent(); + + fireEvent.change(screen.getByTestId('searchbar'), { + target: { value: 'Engineering' }, }); - // the loader must clear so the tree is usable again after the failure await waitFor(() => { - expect(screen.queryByText('Loader')).not.toBeInTheDocument(); + expect(screen.getByTestId('domain-search-error')).toBeInTheDocument(); + }); + + fireEvent.click(screen.getByTestId('retry-domain-search')); + + await waitFor(() => { + expect( + screen.queryByTestId('domain-search-error') + ).not.toBeInTheDocument(); + }); + + expect(domainAPI.searchDomains).toHaveBeenCalledTimes(2); + }); + + it('should clear the inline error when the search box is cleared', async () => { + jest + .spyOn(domainAPI, 'searchDomains') + .mockRejectedValueOnce(new AxiosError('Request failed with status code 400')); + + renderComponent(); + + fireEvent.change(screen.getByTestId('searchbar'), { + target: { value: 'a||||b' }, + }); + + await waitFor(() => { + expect(screen.getByTestId('domain-search-error')).toBeInTheDocument(); + }); + + fireEvent.change(screen.getByTestId('searchbar'), { target: { value: '' } }); + + await waitFor(() => { + expect( + screen.queryByTestId('domain-search-error') + ).not.toBeInTheDocument(); }); }); }); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx index c942be6b5b78..175bc2b9eabb 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx @@ -100,6 +100,7 @@ const DomainSelectablTree: FC = ({ > >({}); const [domainMapper, setDomainMapper] = useState>({}); + const [hasSearchError, setHasSearchError] = useState(false); const { activeDomain } = useDomainStore(); const pagingRef = useRef(INITIAL_PAGING_STATE); @@ -352,6 +353,7 @@ const DomainSelectablTree: FC = ({ async (isLoadMore = false) => { const setLoadingState = isLoadMore ? setIsLoadingMore : setIsLoading; setLoadingState(true); + setHasSearchError(false); if (!isLoadMore) { setPaging(INITIAL_PAGING_STATE); @@ -449,6 +451,7 @@ const DomainSelectablTree: FC = ({ if (value) { try { setIsLoading(true); + setHasSearchError(false); const encodedValue = getEncodedFqn(escapeESReservedCharacters(value)); const results: Domain[] = await searchDomains(encodedValue); const filteredResults = restrictedDomains?.length @@ -475,8 +478,11 @@ const DomainSelectablTree: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); - } catch (error) { - showErrorToast(error as AxiosError); + } catch { + // Keep the failure inside the dropdown: the list renders an inline + // error with a retry, so the user can act on it where they are. + setHasSearchError(true); + setTreeData([]); } finally { setIsLoading(false); } @@ -533,6 +539,29 @@ const DomainSelectablTree: FC = ({ const treeContent = useMemo(() => { if (isLoading) { return ; + } else if (hasSearchError) { + return ( + + + {t('server.entity-fetch-error', { + entity: t('label.domain-plural'), + })} + + + + ); } else if (treeData.length === 0) { return ( = ({ ); } }, [ + hasSearchError, isLoading, isLoadingMore, isSubmitLoading, + onSearch, treeData, value, onSelect, diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx index a8484c4ae031..23a86ab54a83 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx @@ -99,6 +99,7 @@ const DomainSelectablTreeNew: FC = ({ > >({}); const [domainMapper, setDomainMapper] = useState>({}); + const [hasSearchError, setHasSearchError] = useState(false); const pagingRef = useRef(INITIAL_PAGING_STATE); const scrollContainerRef = useRef(null); @@ -346,6 +347,7 @@ const DomainSelectablTreeNew: FC = ({ async (isLoadMore = false) => { const setLoadingState = isLoadMore ? setIsLoadingMore : setIsLoading; setLoadingState(true); + setHasSearchError(false); if (!isLoadMore) { setPaging(INITIAL_PAGING_STATE); @@ -451,6 +453,7 @@ const DomainSelectablTreeNew: FC = ({ if (value) { try { setIsLoading(true); + setHasSearchError(false); const encodedValue = getEncodedFqn(escapeESReservedCharacters(value)); const results: Domain[] = await searchDomains(encodedValue); @@ -473,8 +476,11 @@ const DomainSelectablTreeNew: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); - } catch (error) { - showErrorToast(error as AxiosError); + } catch { + // Keep the failure inside the dropdown: the list renders an inline + // error with a retry, so the user can act on it where they are. + setHasSearchError(true); + setTreeData([]); } finally { setIsLoading(false); } @@ -525,6 +531,24 @@ const DomainSelectablTreeNew: FC = ({ const treeContent = useMemo(() => { if (isLoading) { return ; + } else if (hasSearchError) { + return ( +
+ {t('server.entity-fetch-error', { + entity: t('label.domain-plural'), + })} + +
+ ); } else if (treeData.length === 0) { return (
@@ -566,8 +590,10 @@ const DomainSelectablTreeNew: FC = ({ ); } }, [ + hasSearchError, isLoading, isLoadingMore, + onSearch, treeData, selectedKeys, onSelect, From 5c1c5b115d3af0d636ee728107214d70c246a44a Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Thu, 20 Aug 2026 22:16:37 +0530 Subject: [PATCH 4/7] fix(ui): log the swallowed domain search error MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Review feedback: moving the failure inline dropped the last trace of it. The inline state is deliberately identical for a malformed query and a 5xx, so a genuine backend failure had nowhere left to show up — no toast, no console, nothing for an error reporter to pick up. Log it in the catch, matching the existing `no-console` disable pattern used elsewhere in the UI (there is no central logger). This is a breadcrumb, not an alert: the user-facing surface stays inline. Co-Authored-By: Claude Opus 5 (1M context) --- .../DomainSelectableTree.test.tsx | 13 +++++++++++++ .../DomainSelectableTree/DomainSelectableTree.tsx | 9 +++++++-- .../DomainSelectableTreeNew.tsx | 9 +++++++-- 3 files changed, 27 insertions(+), 4 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx index 8678d9c85476..c2d3bb260aa6 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx @@ -438,6 +438,9 @@ describe('DomainSelectableTree', () => { it('should show an inline error inside the list when a search fails', async () => { const error = new AxiosError('Request failed with status code 400'); + const consoleError = jest + .spyOn(console, 'error') + .mockImplementation(() => undefined); jest.spyOn(domainAPI, 'searchDomains').mockRejectedValueOnce(error); renderComponent(); @@ -458,9 +461,18 @@ describe('DomainSelectableTree', () => { expect(screen.getByTestId('retry-domain-search')).toBeInTheDocument(); expect(screen.queryByText('Loader')).not.toBeInTheDocument(); expect(showErrorToast).not.toHaveBeenCalled(); + // the inline state cannot distinguish a bad query from a 5xx, so the error + // must still reach the console for anything collecting breadcrumbs + expect(consoleError).toHaveBeenCalledWith( + 'Error occurred while searching domains:', + error + ); + + consoleError.mockRestore(); }); it('should retry the search from the inline error and recover', async () => { + jest.spyOn(console, 'error').mockImplementation(() => undefined); jest .spyOn(domainAPI, 'searchDomains') .mockRejectedValueOnce(new AxiosError('Request failed with status code 400')) @@ -488,6 +500,7 @@ describe('DomainSelectableTree', () => { }); it('should clear the inline error when the search box is cleared', async () => { + jest.spyOn(console, 'error').mockImplementation(() => undefined); jest .spyOn(domainAPI, 'searchDomains') .mockRejectedValueOnce(new AxiosError('Request failed with status code 400')); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx index 175bc2b9eabb..1c08bfe9854e 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx @@ -478,9 +478,14 @@ const DomainSelectablTree: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); - } catch { + } catch (error) { // Keep the failure inside the dropdown: the list renders an inline - // error with a retry, so the user can act on it where they are. + // error with a retry, so the user can act on it where they are. Still + // log it - the inline state is deliberately identical for a malformed + // query and a 5xx, so this is the only trace a real backend failure + // leaves for the console and any error reporter's breadcrumbs. + // eslint-disable-next-line no-console + console.error('Error occurred while searching domains:', error); setHasSearchError(true); setTreeData([]); } finally { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx index 23a86ab54a83..74a698000bf0 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx @@ -476,9 +476,14 @@ const DomainSelectablTreeNew: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); - } catch { + } catch (error) { // Keep the failure inside the dropdown: the list renders an inline - // error with a retry, so the user can act on it where they are. + // error with a retry, so the user can act on it where they are. Still + // log it - the inline state is deliberately identical for a malformed + // query and a 5xx, so this is the only trace a real backend failure + // leaves for the console and any error reporter's breadcrumbs. + // eslint-disable-next-line no-console + console.error('Error occurred while searching domains:', error); setHasSearchError(true); setTreeData([]); } finally { From 8f49df3afb8fdcb56729ba1d9c809ffab30d18e4 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Thu, 20 Aug 2026 22:19:36 +0530 Subject: [PATCH 5/7] fix(ui): drop the console log from the domain search catch Co-Authored-By: Claude Opus 5 (1M context) --- .../DomainSelectableTree.test.tsx | 22 +++++-------------- .../DomainSelectableTree.tsx | 9 +------- .../DomainSelectableTreeNew.tsx | 9 +------- 3 files changed, 8 insertions(+), 32 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx index c2d3bb260aa6..533c12911440 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx @@ -438,9 +438,6 @@ describe('DomainSelectableTree', () => { it('should show an inline error inside the list when a search fails', async () => { const error = new AxiosError('Request failed with status code 400'); - const consoleError = jest - .spyOn(console, 'error') - .mockImplementation(() => undefined); jest.spyOn(domainAPI, 'searchDomains').mockRejectedValueOnce(error); renderComponent(); @@ -453,7 +450,6 @@ describe('DomainSelectableTree', () => { target: { value: 'a||||b' }, }); - // the failure stays in the dropdown - no toast, and the loader clears await waitFor(() => { expect(screen.getByTestId('domain-search-error')).toBeInTheDocument(); }); @@ -461,21 +457,14 @@ describe('DomainSelectableTree', () => { expect(screen.getByTestId('retry-domain-search')).toBeInTheDocument(); expect(screen.queryByText('Loader')).not.toBeInTheDocument(); expect(showErrorToast).not.toHaveBeenCalled(); - // the inline state cannot distinguish a bad query from a 5xx, so the error - // must still reach the console for anything collecting breadcrumbs - expect(consoleError).toHaveBeenCalledWith( - 'Error occurred while searching domains:', - error - ); - - consoleError.mockRestore(); }); it('should retry the search from the inline error and recover', async () => { - jest.spyOn(console, 'error').mockImplementation(() => undefined); jest .spyOn(domainAPI, 'searchDomains') - .mockRejectedValueOnce(new AxiosError('Request failed with status code 400')) + .mockRejectedValueOnce( + new AxiosError('Request failed with status code 400') + ) .mockResolvedValueOnce([mockDomains[0]]); renderComponent(); @@ -500,10 +489,11 @@ describe('DomainSelectableTree', () => { }); it('should clear the inline error when the search box is cleared', async () => { - jest.spyOn(console, 'error').mockImplementation(() => undefined); jest .spyOn(domainAPI, 'searchDomains') - .mockRejectedValueOnce(new AxiosError('Request failed with status code 400')); + .mockRejectedValueOnce( + new AxiosError('Request failed with status code 400') + ); renderComponent(); diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx index 1c08bfe9854e..8e91d791a174 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx @@ -478,14 +478,7 @@ const DomainSelectablTree: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); - } catch (error) { - // Keep the failure inside the dropdown: the list renders an inline - // error with a retry, so the user can act on it where they are. Still - // log it - the inline state is deliberately identical for a malformed - // query and a 5xx, so this is the only trace a real backend failure - // leaves for the console and any error reporter's breadcrumbs. - // eslint-disable-next-line no-console - console.error('Error occurred while searching domains:', error); + } catch { setHasSearchError(true); setTreeData([]); } finally { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx index 74a698000bf0..e9f4249b0da3 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTreeNew.tsx @@ -476,14 +476,7 @@ const DomainSelectablTreeNew: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); - } catch (error) { - // Keep the failure inside the dropdown: the list renders an inline - // error with a retry, so the user can act on it where they are. Still - // log it - the inline state is deliberately identical for a malformed - // query and a 5xx, so this is the only trace a real backend failure - // leaves for the console and any error reporter's breadcrumbs. - // eslint-disable-next-line no-console - console.error('Error occurred while searching domains:', error); + } catch { setHasSearchError(true); setTreeData([]); } finally { From ee14cb17fc0c85ac42fd6c3ece6b76cea9a3d041 Mon Sep 17 00:00:00 2001 From: Harsh Vador Date: Fri, 21 Aug 2026 11:00:23 +0530 Subject: [PATCH 6/7] fix(ui): show the server's message on a failed domain search The inline state rendered a static string, so every failure looked the same - a 400 "Invalid field name childrenCount", a 500 parse error, a 503 and a dropped connection were byte-identical on screen. That was a regression against the toast this replaced: showErrorToast runs the error through getErrorText, so the server's own message used to reach the user. Hold the AxiosError instead of a boolean and resolve the text in the render via the same getErrorText helper, keeping the static line as the fallback for errors that carry no message. Resolving it in the render rather than the catch keeps `t` out of the debounced callback's deps and lets the text follow a language change. Co-Authored-By: Claude Opus 5 (1M context) --- .../DomainSelectableTree.test.tsx | 32 +++++++++++++++++++ .../DomainSelectableTree.tsx | 28 +++++++++------- .../DomainSelectableTreeNew.tsx | 26 +++++++++------ 3 files changed, 65 insertions(+), 21 deletions(-) diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx index 533c12911440..0a9ec9491587 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.test.tsx @@ -110,6 +110,11 @@ jest.mock('../../../utils/EntityReferenceUtils', () => ({ jest.mock('../../../utils/StringUtils', () => ({ escapeESReservedCharacters: jest.fn().mockImplementation((value) => value), getEncodedFqn: jest.fn().mockImplementation((value) => value), + getErrorText: jest + .fn() + .mockImplementation( + (error, fallback) => error?.response?.data?.message ?? fallback + ), })); jest.mock('../../../utils/ToastUtils', () => ({ @@ -457,6 +462,33 @@ describe('DomainSelectableTree', () => { expect(screen.getByTestId('retry-domain-search')).toBeInTheDocument(); expect(screen.queryByText('Loader')).not.toBeInTheDocument(); expect(showErrorToast).not.toHaveBeenCalled(); + // no message on the error, so the static fallback is shown + expect(screen.getByText('server.entity-fetch-error')).toBeInTheDocument(); + }); + + it("should show the server's own message when the response carries one", async () => { + const error = new AxiosError('Request failed with status code 400'); + error.response = { + status: 400, + data: { code: 400, message: 'Invalid field name childrenCount' }, + } as never; + jest.spyOn(domainAPI, 'searchDomains').mockRejectedValueOnce(error); + + renderComponent(); + + fireEvent.change(screen.getByTestId('searchbar'), { + target: { value: 'eng' }, + }); + + await waitFor(() => { + expect( + screen.getByText('Invalid field name childrenCount') + ).toBeInTheDocument(); + }); + + expect( + screen.queryByText('server.entity-fetch-error') + ).not.toBeInTheDocument(); }); it('should retry the search from the inline error and recover', async () => { diff --git a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx index 8e91d791a174..de9331a9f093 100644 --- a/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx +++ b/openmetadata-ui/src/main/resources/ui/src/components/common/DomainSelectableTree/DomainSelectableTree.tsx @@ -52,6 +52,7 @@ import { getEntityReferenceFromEntity } from '../../../utils/EntityReferenceUtil import { escapeESReservedCharacters, getEncodedFqn, + getErrorText, } from '../../../utils/StringUtils'; import { showErrorToast } from '../../../utils/ToastUtils'; import Loader from '../Loader/Loader'; @@ -100,7 +101,7 @@ const DomainSelectablTree: FC = ({ > >({}); const [domainMapper, setDomainMapper] = useState>({}); - const [hasSearchError, setHasSearchError] = useState(false); + const [searchError, setSearchError] = useState(); const { activeDomain } = useDomainStore(); const pagingRef = useRef(INITIAL_PAGING_STATE); @@ -353,7 +354,7 @@ const DomainSelectablTree: FC = ({ async (isLoadMore = false) => { const setLoadingState = isLoadMore ? setIsLoadingMore : setIsLoading; setLoadingState(true); - setHasSearchError(false); + setSearchError(undefined); if (!isLoadMore) { setPaging(INITIAL_PAGING_STATE); @@ -451,7 +452,7 @@ const DomainSelectablTree: FC = ({ if (value) { try { setIsLoading(true); - setHasSearchError(false); + setSearchError(undefined); const encodedValue = getEncodedFqn(escapeESReservedCharacters(value)); const results: Domain[] = await searchDomains(encodedValue); const filteredResults = restrictedDomains?.length @@ -478,8 +479,8 @@ const DomainSelectablTree: FC = ({ ); setTreeData(updatedTreeData); setDomains(uniqueData); - } catch { - setHasSearchError(true); + } catch (error) { + setSearchError(error as AxiosError); setTreeData([]); } finally { setIsLoading(false); @@ -537,7 +538,7 @@ const DomainSelectablTree: FC = ({ const treeContent = useMemo(() => { if (isLoading) { return ; - } else if (hasSearchError) { + } else if (searchError) { return ( = ({ direction="col" gap={2} justify="center"> - - {t('server.entity-fetch-error', { - entity: t('label.domain-plural'), - })} + + {getErrorText( + searchError, + t('server.entity-fetch-error', { + entity: t('label.domain-plural'), + }) + )}