Implement AbortController request cancellation for search.
Signed-off-by: Sydney Achinger <sydneynicoleachinger@spotify.com>
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/plugin-search-react': patch
|
||||
'@backstage/plugin-search': patch
|
||||
---
|
||||
|
||||
Implement AbortController request cancellation in SearchBar to prevent overlapping search requests. This change ensures that when users type quickly, previous search requests are properly canceled before new ones start.
|
||||
@@ -93,13 +93,23 @@ export class MockSearchApi implements SearchApi {
|
||||
// (undocumented)
|
||||
mockedResults?: SearchResultSet | undefined;
|
||||
// (undocumented)
|
||||
query(): Promise<SearchResultSet>;
|
||||
query(
|
||||
_query: SearchQuery,
|
||||
_options?: {
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Promise<SearchResultSet>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface SearchApi {
|
||||
// (undocumented)
|
||||
query(query: SearchQuery): Promise<SearchResultSet>;
|
||||
query(
|
||||
query: SearchQuery,
|
||||
options?: {
|
||||
signal?: AbortSignal;
|
||||
},
|
||||
): Promise<SearchResultSet>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
|
||||
@@ -28,7 +28,10 @@ export const searchApiRef = createApiRef<SearchApi>({
|
||||
* @public
|
||||
*/
|
||||
export interface SearchApi {
|
||||
query(query: SearchQuery): Promise<SearchResultSet>;
|
||||
query(
|
||||
query: SearchQuery,
|
||||
options?: { signal?: AbortSignal },
|
||||
): Promise<SearchResultSet>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -39,7 +42,10 @@ export interface SearchApi {
|
||||
export class MockSearchApi implements SearchApi {
|
||||
constructor(public mockedResults?: SearchResultSet) {}
|
||||
|
||||
query(): Promise<SearchResultSet> {
|
||||
query(
|
||||
_query: SearchQuery,
|
||||
_options?: { signal?: AbortSignal },
|
||||
): Promise<SearchResultSet> {
|
||||
return Promise.resolve(this.mockedResults || { results: [] });
|
||||
}
|
||||
}
|
||||
|
||||
+44
-24
@@ -95,12 +95,17 @@ describe('SearchAutocomplete', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
types: [],
|
||||
});
|
||||
expect(query).toHaveBeenCalledWith(
|
||||
{
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
types: [],
|
||||
},
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -117,23 +122,33 @@ describe('SearchAutocomplete', () => {
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
types: [],
|
||||
});
|
||||
expect(query).toHaveBeenCalledWith(
|
||||
{
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
types: [],
|
||||
},
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Clear'));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: '',
|
||||
types: [],
|
||||
});
|
||||
expect(query).toHaveBeenCalledWith(
|
||||
{
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: '',
|
||||
types: [],
|
||||
},
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -154,12 +169,17 @@ describe('SearchAutocomplete', () => {
|
||||
await userEvent.click(screen.getByText(options[0]));
|
||||
|
||||
await waitFor(() => {
|
||||
expect(query).toHaveBeenCalledWith({
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
types: [],
|
||||
});
|
||||
expect(query).toHaveBeenCalledWith(
|
||||
{
|
||||
filters: {},
|
||||
pageCursor: undefined,
|
||||
term: options[0],
|
||||
types: [],
|
||||
},
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -171,6 +171,9 @@ describe('SearchBar', () => {
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: value }),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
jest.runAllTimers();
|
||||
@@ -203,6 +206,9 @@ describe('SearchBar', () => {
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: '' }),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -254,6 +260,9 @@ describe('SearchBar', () => {
|
||||
await waitFor(() =>
|
||||
expect(searchApiMock.query).not.toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: value }),
|
||||
expect.objectContaining({
|
||||
signal: expect.any(AbortSignal),
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
@@ -265,6 +274,9 @@ describe('SearchBar', () => {
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ term: value }),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
jest.runAllTimers();
|
||||
|
||||
@@ -178,6 +178,7 @@ describe('SearchFilter', () => {
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ filters: { field: [values[0]] } }),
|
||||
{ signal: expect.any(Object) },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -186,6 +187,7 @@ describe('SearchFilter', () => {
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ filters: {} }),
|
||||
{ signal: expect.any(Object) },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -217,6 +219,7 @@ describe('SearchFilter', () => {
|
||||
expect.objectContaining({
|
||||
filters: { ...filters, field: [values[0]] },
|
||||
}),
|
||||
{ signal: expect.any(Object) },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -225,6 +228,7 @@ describe('SearchFilter', () => {
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ filters }),
|
||||
{ signal: expect.any(Object) },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -421,6 +425,7 @@ describe('SearchFilter', () => {
|
||||
expect.objectContaining({
|
||||
filters: { [name]: values[0] },
|
||||
}),
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -437,6 +442,7 @@ describe('SearchFilter', () => {
|
||||
expect.objectContaining({
|
||||
filters: {},
|
||||
}),
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -479,6 +485,7 @@ describe('SearchFilter', () => {
|
||||
expect.objectContaining({
|
||||
filters: { ...filters, [name]: values[0] },
|
||||
}),
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
@@ -493,6 +500,7 @@ describe('SearchFilter', () => {
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining({ filters }),
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -197,6 +197,9 @@ describe('SearchPagination', () => {
|
||||
expect.objectContaining({
|
||||
pageLimit: 10,
|
||||
}),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -229,6 +232,9 @@ describe('SearchPagination', () => {
|
||||
expect.objectContaining({
|
||||
pageCursor: 'Mg==', // page: 2
|
||||
}),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
await userEvent.click(screen.getByLabelText('Previous page'));
|
||||
@@ -239,6 +245,9 @@ describe('SearchPagination', () => {
|
||||
expect.objectContaining({
|
||||
pageCursor: 'MQ==', // page: 1
|
||||
}),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -271,6 +280,9 @@ describe('SearchPagination', () => {
|
||||
pageCursor: undefined,
|
||||
pageLimit: 10,
|
||||
}),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -264,11 +264,14 @@ describe('SearchContext', () => {
|
||||
result.current.setTerm(term);
|
||||
});
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
term,
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
term,
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
it('When types is set', async () => {
|
||||
@@ -286,11 +289,14 @@ describe('SearchContext', () => {
|
||||
result.current.setTypes(types);
|
||||
});
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
types,
|
||||
term: '',
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
types,
|
||||
term: '',
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
it('When filters are set', async () => {
|
||||
@@ -308,11 +314,14 @@ describe('SearchContext', () => {
|
||||
result.current.setFilters(filters);
|
||||
});
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
filters,
|
||||
term: '',
|
||||
types: ['*'],
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
filters,
|
||||
term: '',
|
||||
types: ['*'],
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
it('When page limit is set', async () => {
|
||||
@@ -330,12 +339,15 @@ describe('SearchContext', () => {
|
||||
result.current.setPageLimit(pageLimit);
|
||||
});
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
pageLimit,
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
pageLimit,
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
it('When page cursor is set', async () => {
|
||||
@@ -353,12 +365,15 @@ describe('SearchContext', () => {
|
||||
result.current.setPageCursor(pageCursor);
|
||||
});
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
pageCursor,
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
pageCursor,
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
it('provides function for fetch the next page', async () => {
|
||||
@@ -382,12 +397,15 @@ describe('SearchContext', () => {
|
||||
result.current.fetchNextPage!();
|
||||
});
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
pageCursor: 'NEXT',
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
pageCursor: 'NEXT',
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
it('provides function for fetch the previous page', async () => {
|
||||
@@ -410,12 +428,15 @@ describe('SearchContext', () => {
|
||||
result.current.fetchPreviousPage!();
|
||||
});
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
pageCursor: 'PREVIOUS',
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
pageCursor: 'PREVIOUS',
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -456,11 +477,14 @@ describe('SearchContext', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith({
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
{
|
||||
term: '',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
expect(analyticsApiMock.captureEvent).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
@@ -470,11 +494,14 @@ describe('SearchContext', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith({
|
||||
term: 'eva',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
{
|
||||
term: 'eva',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
|
||||
action: 'search',
|
||||
subject: 'eva',
|
||||
@@ -493,11 +520,14 @@ describe('SearchContext', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith({
|
||||
term: 'eva.m',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
{
|
||||
term: 'eva.m',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
|
||||
action: 'search',
|
||||
subject: 'eva.m',
|
||||
@@ -531,11 +561,14 @@ describe('SearchContext', () => {
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith({
|
||||
term: 'term',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
{
|
||||
term: 'term',
|
||||
types: ['*'],
|
||||
filters: {},
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
expect(analyticsApiMock.captureEvent).toHaveBeenCalledWith({
|
||||
action: 'search',
|
||||
subject: 'term',
|
||||
|
||||
@@ -23,6 +23,7 @@ import {
|
||||
useCallback,
|
||||
useContext,
|
||||
useEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from 'react';
|
||||
import useAsync, { AsyncState } from 'react-use/esm/useAsync';
|
||||
@@ -132,15 +133,28 @@ const useSearchContextValue = (
|
||||
|
||||
const prevTerm = usePrevious(term);
|
||||
const prevFilters = usePrevious(filters);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const result = useAsync(async () => {
|
||||
const resultSet = await searchApi.query({
|
||||
term,
|
||||
types,
|
||||
filters,
|
||||
pageLimit,
|
||||
pageCursor,
|
||||
});
|
||||
// Here we cancel the previous request before making a new one
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
const resultSet = await searchApi.query(
|
||||
{
|
||||
term,
|
||||
types,
|
||||
filters,
|
||||
pageLimit,
|
||||
pageCursor,
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
if (term) {
|
||||
analytics.captureEvent('search', term, {
|
||||
value: resultSet.numberOfResults,
|
||||
@@ -162,6 +176,14 @@ const useSearchContextValue = (
|
||||
setPageCursor(result.value?.previousPageCursor);
|
||||
}, [result.value?.previousPageCursor]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
// Any time a term is reset, we want to start from page 0.
|
||||
// Only reset the term if it has been modified by the user at least once, the initial state must not reset the term.
|
||||
|
||||
@@ -52,10 +52,19 @@ describe('apis', () => {
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
await client.query(query);
|
||||
expect(getBaseUrl).toHaveBeenLastCalledWith('search');
|
||||
expect(mockFetch).toHaveBeenLastCalledWith(
|
||||
`${baseUrl}/query?term=`,
|
||||
undefined,
|
||||
);
|
||||
expect(mockFetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, {
|
||||
signal: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it('Fetch is called with abort signal when provided', async () => {
|
||||
identityApi.getCredentials.mockResolvedValue({});
|
||||
const abortController = new AbortController();
|
||||
await client.query(query, { signal: abortController.signal });
|
||||
expect(getBaseUrl).toHaveBeenLastCalledWith('search');
|
||||
expect(mockFetch).toHaveBeenLastCalledWith(`${baseUrl}/query?term=`, {
|
||||
signal: abortController.signal,
|
||||
});
|
||||
});
|
||||
|
||||
it('Resolves JSON from fetch response', async () => {
|
||||
|
||||
@@ -30,12 +30,17 @@ export class SearchClient implements SearchApi {
|
||||
this.fetchApi = options.fetchApi;
|
||||
}
|
||||
|
||||
async query(query: SearchQuery): Promise<SearchResultSet> {
|
||||
async query(
|
||||
query: SearchQuery,
|
||||
options?: { signal?: AbortSignal },
|
||||
): Promise<SearchResultSet> {
|
||||
const queryString = qs.stringify(query);
|
||||
const url = `${await this.discoveryApi.getBaseUrl(
|
||||
'search',
|
||||
)}/query?${queryString}`;
|
||||
const response = await this.fetchApi.fetch(url);
|
||||
const response = await this.fetchApi.fetch(url, {
|
||||
signal: options?.signal,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw await ResponseError.fromResponse(response);
|
||||
|
||||
@@ -48,6 +48,9 @@ describe('<HomePageSearchBar/>', () => {
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ term: '' }),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
|
||||
await userEvent.type(screen.getByLabelText('Search'), 'term{enter}');
|
||||
|
||||
@@ -88,7 +88,9 @@ describe('SearchModal', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(initialState);
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(initialState, {
|
||||
signal: expect.any(AbortSignal),
|
||||
});
|
||||
});
|
||||
|
||||
it('Should create a local search context if a parent is not defined', async () => {
|
||||
@@ -104,12 +106,15 @@ describe('SearchModal', () => {
|
||||
);
|
||||
|
||||
expect(screen.getByRole('dialog')).toBeInTheDocument();
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith({
|
||||
term: '',
|
||||
filters: {},
|
||||
types: [],
|
||||
pageCursor: undefined,
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
{
|
||||
term: '',
|
||||
filters: {},
|
||||
types: [],
|
||||
pageCursor: undefined,
|
||||
},
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
});
|
||||
|
||||
it('Should render a custom Modal correctly', async () => {
|
||||
@@ -200,6 +205,7 @@ describe('SearchModal', () => {
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ term: 'term' }),
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
|
||||
const input = screen.getByLabelText<HTMLInputElement>('Search');
|
||||
@@ -232,6 +238,7 @@ describe('SearchModal', () => {
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ term: 'term' }),
|
||||
{ signal: expect.any(AbortSignal) },
|
||||
);
|
||||
|
||||
const fullResultsBtn = screen.getByRole('button', {
|
||||
|
||||
@@ -157,18 +157,28 @@ describe('SearchType.Accordion', () => {
|
||||
</Wrapper>,
|
||||
);
|
||||
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith({
|
||||
term: 'abc',
|
||||
types: [],
|
||||
filters: { foo: 'bar' },
|
||||
pageLimit: 0,
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith({
|
||||
term: 'abc',
|
||||
types: [expectedType.value],
|
||||
filters: {},
|
||||
pageLimit: 0,
|
||||
});
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
{
|
||||
term: 'abc',
|
||||
types: [],
|
||||
filters: { foo: 'bar' },
|
||||
pageLimit: 0,
|
||||
},
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
expect(searchApiMock.query).toHaveBeenCalledWith(
|
||||
{
|
||||
term: 'abc',
|
||||
types: [expectedType.value],
|
||||
filters: {},
|
||||
pageLimit: 0,
|
||||
},
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
await waitFor(() => {
|
||||
const countLabels = getAllByText('1234 results');
|
||||
expect(countLabels.length).toEqual(2);
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { cloneElement, Fragment, useEffect, useState } from 'react';
|
||||
import { cloneElement, Fragment, useEffect, useRef, useState } from 'react';
|
||||
import { useApi } from '@backstage/core-plugin-api';
|
||||
import { searchApiRef, useSearch } from '@backstage/plugin-search-react';
|
||||
import Accordion from '@material-ui/core/Accordion';
|
||||
@@ -86,6 +86,7 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
|
||||
const [expanded, setExpanded] = useState(true);
|
||||
const { defaultValue, name, showCounts, types: givenTypes } = props;
|
||||
const { t } = useTranslationRef(searchTranslationRef);
|
||||
const abortControllerRef = useRef<AbortController | null>(null);
|
||||
|
||||
const toggleExpanded = () => setExpanded(prevState => !prevState);
|
||||
const handleClick = (type: string) => {
|
||||
@@ -117,18 +118,29 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
|
||||
if (!showCounts) {
|
||||
return {};
|
||||
}
|
||||
// Here we cancel the previous requests before making a new one
|
||||
// All requests are made with a new AbortController signal
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
|
||||
const controller = new AbortController();
|
||||
abortControllerRef.current = controller;
|
||||
|
||||
const counts = await Promise.all(
|
||||
definedTypes
|
||||
.map(type => type.value)
|
||||
.map(async type => {
|
||||
const { numberOfResults } = await searchApi.query({
|
||||
term,
|
||||
types: type ? [type] : [],
|
||||
filters:
|
||||
types.includes(type) || (!types.length && !type) ? filters : {},
|
||||
pageLimit: 0,
|
||||
});
|
||||
const { numberOfResults } = await searchApi.query(
|
||||
{
|
||||
term,
|
||||
types: type ? [type] : [],
|
||||
filters:
|
||||
types.includes(type) || (!types.length && !type) ? filters : {},
|
||||
pageLimit: 0,
|
||||
},
|
||||
{ signal: controller.signal },
|
||||
);
|
||||
|
||||
return [
|
||||
type,
|
||||
@@ -145,6 +157,14 @@ export const SearchTypeAccordion = (props: SearchTypeAccordionProps) => {
|
||||
return Object.fromEntries(counts);
|
||||
}, [filters, showCounts, term, types]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (abortControllerRef.current) {
|
||||
abortControllerRef.current.abort();
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box>
|
||||
<Typography variant="body2" component="h2">
|
||||
|
||||
@@ -192,6 +192,9 @@ describe('SearchType', () => {
|
||||
expect.objectContaining({
|
||||
types: [values[0]],
|
||||
}),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -240,6 +243,9 @@ describe('SearchType', () => {
|
||||
expect.objectContaining({
|
||||
types: [...typeValues, values[0]],
|
||||
}),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -253,7 +259,12 @@ describe('SearchType', () => {
|
||||
|
||||
await waitFor(() => {
|
||||
expect(searchApiMock.query).toHaveBeenLastCalledWith(
|
||||
expect.objectContaining([]),
|
||||
expect.objectContaining({
|
||||
types: typeValues,
|
||||
}),
|
||||
{
|
||||
signal: expect.any(AbortSignal),
|
||||
},
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user