Merge pull request #6946 from backstage/freben/more-entity-opts
Use the history API directly in `useEntityListProvider`
This commit is contained in:
@@ -0,0 +1,12 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
---
|
||||
|
||||
Use the history API directly in `useEntityListProvider`.
|
||||
|
||||
This replaces `useSearchParams`/`useNavigate`, since they cause at least one additional re-render compared to using this method.
|
||||
|
||||
Table re-render count is down additionally:
|
||||
|
||||
- Initial render of catalog page: 12 -> 9
|
||||
- Full `CatalogPage` test: 57 -> 48
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import React, { PropsWithChildren } from 'react';
|
||||
import qs from 'qs';
|
||||
import { MemoryRouter as Router } from 'react-router-dom';
|
||||
import { act, renderHook } from '@testing-library/react-hooks';
|
||||
import { MockStorageApi } from '@backstage/test-utils';
|
||||
import { CatalogApi } from '@backstage/catalog-client';
|
||||
@@ -86,26 +85,30 @@ const apis = ApiRegistry.from([
|
||||
|
||||
const wrapper = ({
|
||||
userFilter,
|
||||
queryParams,
|
||||
children,
|
||||
}: PropsWithChildren<{
|
||||
userFilter?: UserListFilterKind;
|
||||
queryParams?: string;
|
||||
}>) => {
|
||||
return (
|
||||
<Router initialEntries={[`/?${queryParams ?? ''}`]}>
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityListProvider>
|
||||
<EntityKindPicker initialFilter="component" hidden />
|
||||
<UserListPicker initialFilter={userFilter} />
|
||||
{children}
|
||||
</EntityListProvider>
|
||||
</ApiProvider>
|
||||
</Router>
|
||||
<ApiProvider apis={apis}>
|
||||
<EntityListProvider>
|
||||
<EntityKindPicker initialFilter="component" hidden />
|
||||
<UserListPicker initialFilter={userFilter} />
|
||||
{children}
|
||||
</EntityListProvider>
|
||||
</ApiProvider>
|
||||
);
|
||||
};
|
||||
|
||||
describe('<EntityListProvider />', () => {
|
||||
const origReplaceState = window.history.replaceState;
|
||||
beforeEach(() => {
|
||||
window.history.replaceState = jest.fn();
|
||||
});
|
||||
afterEach(() => {
|
||||
window.history.replaceState = origReplaceState;
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
jest.clearAllMocks();
|
||||
});
|
||||
@@ -137,16 +140,13 @@ describe('<EntityListProvider />', () => {
|
||||
});
|
||||
|
||||
it('resolves query param filter values', async () => {
|
||||
const query = qs.stringify({
|
||||
filters: { kind: 'component', type: 'service' },
|
||||
});
|
||||
delete (window as any).location;
|
||||
(window as any).location = new URL(`http://localhost/catalog?${query}`);
|
||||
const { result, waitFor } = renderHook(() => useEntityListProvider(), {
|
||||
wrapper,
|
||||
initialProps: {
|
||||
queryParams: qs.stringify({
|
||||
filters: {
|
||||
kind: 'component',
|
||||
type: 'service',
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
await waitFor(() => !!result.current.queryParameters);
|
||||
expect(result.current.queryParameters).toEqual({
|
||||
|
||||
@@ -25,7 +25,6 @@ import React, {
|
||||
useMemo,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { useSearchParams } from 'react-router-dom';
|
||||
import { useAsyncFn, useDebounce, useMountedState } from 'react-use';
|
||||
import { catalogApiRef } from '../api';
|
||||
import {
|
||||
@@ -105,18 +104,25 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
}: PropsWithChildren<{}>) => {
|
||||
const isMounted = useMountedState();
|
||||
const catalogApi = useApi(catalogApiRef);
|
||||
const [searchParams, setSearchParams] = useSearchParams();
|
||||
const allQueryParams = qs.parse(searchParams.toString());
|
||||
const [requestedFilters, setRequestedFilters] = useState<EntityFilters>(
|
||||
{} as EntityFilters,
|
||||
);
|
||||
const [outputState, setOutputState] = useState<OutputState<EntityFilters>>({
|
||||
appliedFilters: {} as EntityFilters,
|
||||
entities: [],
|
||||
backendEntities: [],
|
||||
queryParameters:
|
||||
(allQueryParams.filters as Record<string, string | string[]>) ?? {},
|
||||
});
|
||||
const [outputState, setOutputState] = useState<OutputState<EntityFilters>>(
|
||||
() => {
|
||||
const query = qs.parse(window.location.search, {
|
||||
ignoreQueryPrefix: true,
|
||||
});
|
||||
return {
|
||||
appliedFilters: {} as EntityFilters,
|
||||
entities: [],
|
||||
backendEntities: [],
|
||||
queryParameters: (query.filters ?? {}) as Record<
|
||||
string,
|
||||
string | string[]
|
||||
>,
|
||||
};
|
||||
},
|
||||
);
|
||||
|
||||
// The main async filter worker. Note that while it has a lot of dependencies
|
||||
// in terms of its implementation, the triggering only happens (debounced)
|
||||
@@ -167,12 +173,20 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
}
|
||||
|
||||
if (isMounted()) {
|
||||
setSearchParams(
|
||||
qs.stringify({ ...allQueryParams, filters: queryParams }),
|
||||
{
|
||||
replace: true,
|
||||
},
|
||||
const oldParams = qs.parse(window.location.search, {
|
||||
ignoreQueryPrefix: true,
|
||||
});
|
||||
const newParams = qs.stringify(
|
||||
{ ...oldParams, filters: queryParams },
|
||||
{ addQueryPrefix: true },
|
||||
);
|
||||
const newUrl = `${window.location.pathname}${newParams}`;
|
||||
// We use direct history manipulation since useSearchParams and
|
||||
// useNavigate in react-router-dom cause unnecessary extra rerenders.
|
||||
// Also make sure to replace the state rather than pushing, since we
|
||||
// don't want there to be back/forward slots for every single filter
|
||||
// change.
|
||||
window.history?.replaceState(null, document.title, newUrl);
|
||||
}
|
||||
},
|
||||
[catalogApi, requestedFilters, outputState],
|
||||
@@ -208,15 +222,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>({
|
||||
loading,
|
||||
error,
|
||||
}),
|
||||
[
|
||||
outputState.appliedFilters,
|
||||
outputState.entities,
|
||||
outputState.backendEntities,
|
||||
updateFilters,
|
||||
outputState.queryParameters,
|
||||
loading,
|
||||
error,
|
||||
],
|
||||
[outputState, updateFilters, loading, error],
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -44,6 +44,14 @@ import {
|
||||
} from '@backstage/core-plugin-api';
|
||||
|
||||
describe('CatalogPage', () => {
|
||||
const origReplaceState = window.history.replaceState;
|
||||
beforeEach(() => {
|
||||
window.history.replaceState = jest.fn();
|
||||
});
|
||||
afterEach(() => {
|
||||
window.history.replaceState = origReplaceState;
|
||||
});
|
||||
|
||||
const catalogApi: Partial<CatalogApi> = {
|
||||
getEntities: () =>
|
||||
Promise.resolve({
|
||||
|
||||
Reference in New Issue
Block a user