diff --git a/.changeset/search-breezy-oranges-collect.md b/.changeset/search-breezy-oranges-collect.md new file mode 100644 index 0000000000..f9d4e8c076 --- /dev/null +++ b/.changeset/search-breezy-oranges-collect.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-search': patch +--- + +Handle request errors properly and display them in the results list. diff --git a/plugins/search/package.json b/plugins/search/package.json index c40885e357..29541f1ed2 100644 --- a/plugins/search/package.json +++ b/plugins/search/package.json @@ -33,6 +33,7 @@ "@backstage/config": "^0.1.5", "@backstage/core-components": "^0.1.5", "@backstage/core-plugin-api": "^0.1.3", + "@backstage/errors": "^0.1.1", "@backstage/plugin-catalog-react": "^0.2.6", "@backstage/search-common": "^0.1.2", "@backstage/theme": "^0.2.8", diff --git a/plugins/search/src/apis.test.ts b/plugins/search/src/apis.test.ts index 43b77e4927..cc59ca1a77 100644 --- a/plugins/search/src/apis.test.ts +++ b/plugins/search/src/apis.test.ts @@ -44,7 +44,7 @@ describe('apis', () => { const json = jest.fn(); const originalFetch = window.fetch; - window.fetch = jest.fn().mockResolvedValue({ json }); + window.fetch = jest.fn().mockResolvedValue({ json, ok: true }); afterAll(() => { window.fetch = originalFetch; diff --git a/plugins/search/src/apis.ts b/plugins/search/src/apis.ts index da8332bd44..fa6d9ce908 100644 --- a/plugins/search/src/apis.ts +++ b/plugins/search/src/apis.ts @@ -14,13 +14,14 @@ * limitations under the License. */ -import { SearchQuery, SearchResultSet } from '@backstage/search-common'; -import qs from 'qs'; import { createApiRef, DiscoveryApi, IdentityApi, } from '@backstage/core-plugin-api'; +import { ResponseError } from '@backstage/errors'; +import { SearchQuery, SearchResultSet } from '@backstage/search-common'; +import qs from 'qs'; export const searchApiRef = createApiRef({ id: 'plugin.search.queryservice', @@ -52,6 +53,11 @@ export class SearchClient implements SearchApi { const response = await fetch(url, { headers: token ? { Authorization: `Bearer ${token}` } : {}, }); + + if (!response.ok) { + throw await ResponseError.fromResponse(response); + } + return response.json(); } } diff --git a/plugins/search/src/components/SearchResult/SearchResult.test.tsx b/plugins/search/src/components/SearchResult/SearchResult.test.tsx index 9508eec259..bb8f393d67 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.test.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.test.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ +import { renderInTestApp } from '@backstage/test-utils'; +import { waitFor } from '@testing-library/react'; import React from 'react'; -import { render, waitFor } from '@testing-library/react'; - -import { SearchResult } from './SearchResult'; import { useSearch } from '../SearchContext'; +import { SearchResult } from './SearchResult'; jest.mock('../SearchContext', () => ({ ...jest.requireActual('../SearchContext'), @@ -33,7 +33,9 @@ describe('SearchResult', () => { result: { loading: true }, }); - const { getByRole } = render({() => <>}); + const { getByRole } = await renderInTestApp( + {() => <>}, + ); await waitFor(() => { expect(getByRole('progressbar')).toBeInTheDocument(); @@ -41,16 +43,18 @@ describe('SearchResult', () => { }); it('Alert rendered on Error state', async () => { - const error = 'error'; + const error = new Error('some error'); (useSearch as jest.Mock).mockReturnValueOnce({ result: { loading: false, error }, }); - const { getByRole } = render({() => <>}); + const { getByRole } = await renderInTestApp( + {() => <>}, + ); await waitFor(() => { expect(getByRole('alert')).toHaveTextContent( - `Error encountered while fetching search results. ${error}`, + new RegExp(`Error encountered while fetching search results.*${error}`), ); }); }); @@ -60,7 +64,9 @@ describe('SearchResult', () => { result: { loading: false, error: '', value: undefined }, }); - const { getByRole } = render({() => <>}); + const { getByRole } = await renderInTestApp( + {() => <>}, + ); await waitFor(() => { expect( @@ -74,7 +80,9 @@ describe('SearchResult', () => { result: { loading: false, error: '', value: { results: [] } }, }); - const { getByRole } = render({() => <>}); + const { getByRole } = await renderInTestApp( + {() => <>}, + ); await waitFor(() => { expect( @@ -83,12 +91,12 @@ describe('SearchResult', () => { }); }); - it('Calls children with results set to result.value', () => { + it('Calls children with results set to result.value', async () => { (useSearch as jest.Mock).mockReturnValueOnce({ result: { loading: false, error: '', value: { results: [] } }, }); - render( + await renderInTestApp( {({ results }) => { expect(results).toEqual([]); diff --git a/plugins/search/src/components/SearchResult/SearchResult.tsx b/plugins/search/src/components/SearchResult/SearchResult.tsx index 7ca5964d21..26a2484a86 100644 --- a/plugins/search/src/components/SearchResult/SearchResult.tsx +++ b/plugins/search/src/components/SearchResult/SearchResult.tsx @@ -14,13 +14,15 @@ * limitations under the License. */ -import React from 'react'; +import { + EmptyState, + Progress, + ResponseErrorPanel, +} from '@backstage/core-components'; import { SearchResult } from '@backstage/search-common'; -import { Alert } from '@material-ui/lab'; +import React from 'react'; import { useSearch } from '../SearchContext'; -import { EmptyState, Progress } from '@backstage/core-components'; - type Props = { children: (results: { results: SearchResult[] }) => JSX.Element; }; @@ -35,9 +37,10 @@ const SearchResultComponent = ({ children }: Props) => { } if (error) { return ( - - Error encountered while fetching search results. {error.toString()} - + ); }