Fix the race condition in EntityListProvider

Signed-off-by: Jacob Raihle kdm951 <jacob.raihle@teliacompany.com>
This commit is contained in:
Jacob Raihle kdm951
2025-07-15 16:29:14 +03:00
parent 097a334228
commit 4c98f05f0a
@@ -14,7 +14,9 @@
* limitations under the License.
*/
import { QueryEntitiesResponse } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
import { compact, isEqual } from 'lodash';
import qs from 'qs';
import {
@@ -22,6 +24,7 @@ import {
PropsWithChildren,
useCallback,
useContext,
useEffect,
useMemo,
useState,
} from 'react';
@@ -50,8 +53,6 @@ import {
reduceCatalogFilters,
reduceEntityFilters,
} from '../utils/filters';
import { useApi } from '@backstage/core-plugin-api';
import { QueryEntitiesResponse } from '@backstage/catalog-client';
/** @public */
export type DefaultEntityFilters = {
@@ -239,7 +240,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
// The main async filter worker. Note that while it has a lot of dependencies
// in terms of its implementation, the triggering only happens (debounced)
// based on the requested filters changing.
const [{ loading, error }, refresh] = useAsyncFn(
const [{ value: resolvedValue, loading, error }, refresh] = useAsyncFn(
async () => {
const kindValue =
requestedFilters.kind?.value?.toLocaleLowerCase('en-US');
@@ -249,19 +250,6 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
: requestedFilters;
const compacted = compact(Object.values(adjustedFilters));
const queryParams = Object.keys(requestedFilters).reduce(
(params, key) => {
const filter = requestedFilters[key as keyof EntityFilters] as
| EntityFilter
| undefined;
if (filter?.toQueryValue) {
params[key] = filter.toQueryValue();
}
return params;
},
{} as Record<string, string | string[]>,
);
if (paginationMode !== 'none') {
if (cursor) {
if (cursor !== outputState.appliedCursor) {
@@ -270,14 +258,14 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
cursor,
limit,
});
setOutputState({
return {
appliedFilters: requestedFilters,
appliedCursor: cursor,
backendEntities: response.items,
entities: response.items.filter(entityFilter),
pageInfo: response.pageInfo,
totalItems: response.totalItems,
});
};
}
} else {
const entityFilter = reduceEntityFilters(compacted);
@@ -296,7 +284,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
limit,
offset,
});
setOutputState({
return {
appliedFilters: requestedFilters,
backendEntities: response.items,
entities: response.items.filter(entityFilter),
@@ -304,7 +292,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
totalItems: response.totalItems,
limit,
offset,
});
};
}
}
} else {
@@ -324,43 +312,22 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
filter: backendFilter,
});
const entities = response.items.filter(entityFilter);
setOutputState({
return {
appliedFilters: requestedFilters,
backendEntities: response.items,
entities,
totalItems: entities.length,
});
} else {
const entities = outputState.backendEntities.filter(entityFilter);
setOutputState({
appliedFilters: requestedFilters,
backendEntities: outputState.backendEntities,
entities,
totalItems: entities.length,
});
};
}
const entities = outputState.backendEntities.filter(entityFilter);
return {
appliedFilters: requestedFilters,
backendEntities: outputState.backendEntities,
entities,
totalItems: entities.length,
};
}
if (isMounted()) {
const oldParams = qs.parse(location.search, {
ignoreQueryPrefix: true,
});
const newParams = qs.stringify(
{
...oldParams,
filters: queryParams,
...(paginationMode === 'none' ? {} : { cursor, limit, offset }),
},
{ addQueryPrefix: true, arrayFormat: 'repeat' },
);
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);
}
return undefined;
},
[
catalogApi,
@@ -379,6 +346,55 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
// filters will be calling this in rapid succession.
useDebounce(refresh, 10, [requestedFilters, cursor, limit, offset]);
useEffect(() => {
if (resolvedValue === undefined) {
return;
}
setOutputState(resolvedValue);
if (isMounted()) {
const queryParams = Object.keys(requestedFilters).reduce(
(params, key) => {
const filter = requestedFilters[key as keyof EntityFilters] as
| EntityFilter
| undefined;
if (filter?.toQueryValue) {
params[key] = filter.toQueryValue();
}
return params;
},
{} as Record<string, string | string[]>,
);
const oldParams = qs.parse(location.search, {
ignoreQueryPrefix: true,
});
const newParams = qs.stringify(
{
...oldParams,
filters: queryParams,
...(paginationMode === 'none' ? {} : { cursor, limit, offset }),
},
{ addQueryPrefix: true, arrayFormat: 'repeat' },
);
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);
}
}, [
cursor,
isMounted,
limit,
location.search,
offset,
requestedFilters,
resolvedValue,
paginationMode,
]);
const updateFilters = useCallback(
(
update: