Use a versioned context for useEntityList

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2025-12-04 17:00:12 +01:00
parent 8fd193f5b8
commit b3c0594c8e
7 changed files with 129 additions and 22 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': patch
---
Use a versioned context for `useEntityList`, to better work with mixed `@backstage/plugin-catalog-react` versions.
+1 -1
View File
@@ -322,7 +322,7 @@ export const EntityLifecyclePicker: (props: {
initialFilter?: string[];
}) => JSX_2.Element;
// @public
// @public @deprecated
export const EntityListContext: Context<
EntityListContextProps<any> | undefined
>;
+15 -3
View File
@@ -15,10 +15,12 @@
*/
import { PropsWithChildren, useCallback, useMemo, useState } from 'react';
import { createVersionedValueMap } from '@backstage/version-bridge';
import {
DefaultEntityFilters,
EntityListContext,
EntityListContextProps,
NewEntityListContext,
OldEntityListContext,
} from './hooks/useEntityListProvider';
/**
@@ -81,8 +83,18 @@ export function MockEntityListContextProvider<
);
return (
<EntityListContext.Provider value={resolvedValue}>
<NewEntityListContext.Provider
value={createVersionedValueMap({ 1: resolvedValue })}
>
{children}
</EntityListContext.Provider>
</NewEntityListContext.Provider>
);
}
/**
* Creates new context for entity listing and filtering.
*
* @public
* @deprecated Please use `EntityListProvider` and `EntityListProvider` instead.
*/
export const EntityListContext = OldEntityListContext;
+1 -5
View File
@@ -24,11 +24,7 @@ export type {
EntityProviderProps,
AsyncEntityProviderProps,
} from './useEntity';
export {
EntityListContext,
EntityListProvider,
useEntityList,
} from './useEntityListProvider';
export { EntityListProvider, useEntityList } from './useEntityListProvider';
export type {
DefaultEntityFilters,
EntityListContextProps,
@@ -42,7 +42,13 @@ import {
} from '../filters';
import { createDeferred } from '@backstage/types';
import { EntityListPagination } from '../types';
import { EntityListProvider, useEntityList } from './useEntityListProvider';
import {
EntityListContextProps,
EntityListProvider,
NewEntityListContext,
useEntityList,
} from './useEntityListProvider';
import { createVersionedValueMap } from '@backstage/version-bridge';
const entities: Entity[] = [
{
@@ -1048,3 +1054,59 @@ describe(`<EntityListProvider pagination={{ mode: 'offset' }} />`, () => {
);
});
});
describe('versioned context', () => {
it('should work explicitly with new versioned contexts', () => {
const value: EntityListContextProps<any> = {
filters: {},
entities: [],
backendEntities: [],
updateFilters: jest.fn(),
queryParameters: {},
loading: true,
limit: 277,
setLimit: jest.fn(),
setOffset: jest.fn(),
paginationMode: 'none',
};
const { result } = renderHook(() => useEntityList(), {
wrapper: ({ children }) => {
const InitialFiltersWrapper = (f: PropsWithChildren<{}>) => {
const { updateFilters } = useEntityList();
useMountEffect(() => {
updateFilters({
kind: new EntityKindFilter('component', 'Component'),
});
});
return <>{f.children}</>;
};
return (
<MemoryRouter initialEntries={['/catalog']}>
<TestApiProvider
apis={[
[configApiRef, mockApis.config()],
[catalogApiRef, mockCatalogApi],
[identityApiRef, mockIdentityApi],
[storageApiRef, mockApis.storage()],
[starredEntitiesApiRef, new MockStarredEntitiesApi()],
[alertApiRef, { post: jest.fn() }],
[translationApiRef, mockApis.translation()],
[errorApiRef, { error$: jest.fn(), post: jest.fn() }],
]}
>
<NewEntityListContext.Provider
value={createVersionedValueMap({ 1: value })}
>
<InitialFiltersWrapper>{children}</InitialFiltersWrapper>
</NewEntityListContext.Provider>
</TestApiProvider>
</MemoryRouter>
);
},
});
expect(result.current.limit).toBe(277);
});
});
@@ -17,6 +17,11 @@
import { QueryEntitiesResponse } from '@backstage/catalog-client';
import { Entity } from '@backstage/catalog-model';
import { useApi } from '@backstage/core-plugin-api';
import {
createVersionedContext,
createVersionedValueMap,
useVersionedContext,
} from '@backstage/version-bridge';
import { compact, isEqual } from 'lodash';
import qs from 'qs';
import {
@@ -122,11 +127,17 @@ export type EntityListContextProps<
paginationMode: PaginationMode;
};
// This context has support for multiple concurrent versions of this package.
// It is currently used in parallel with the old context in order to provide
// a smooth transition, but will eventually be the only context we use.
export const NewEntityListContext = createVersionedContext<{
1: EntityListContextProps<any>;
}>('entity-list-context');
/**
* Creates new context for entity listing and filtering.
* @public
*/
export const EntityListContext = createContext<
export const OldEntityListContext = createContext<
EntityListContextProps<any> | undefined
>(undefined);
@@ -487,9 +498,13 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
);
return (
<EntityListContext.Provider value={value}>
{props.children}
</EntityListContext.Provider>
<OldEntityListContext.Provider value={value}>
<NewEntityListContext.Provider
value={createVersionedValueMap({ 1: value })}
>
{props.children}
</NewEntityListContext.Provider>
</OldEntityListContext.Provider>
);
};
@@ -500,8 +515,22 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
export function useEntityList<
EntityFilters extends DefaultEntityFilters = DefaultEntityFilters,
>(): EntityListContextProps<EntityFilters> {
const context = useContext(EntityListContext);
if (!context)
throw new Error('useEntityList must be used within EntityListProvider');
return context;
const versionedHolder = useVersionedContext<{
1: EntityListContextProps<any>;
}>('entity-list-context');
const oldContext = useContext(OldEntityListContext);
if (versionedHolder) {
const value = versionedHolder.atVersion(1);
if (!value) {
throw new Error('EntityListContext v1 not available');
}
return value;
}
if (oldContext) {
return oldContext;
}
throw new Error('useEntityList must be used within EntityListProvider');
}
@@ -17,9 +17,10 @@
import { PropsWithChildren, useCallback, useMemo, useState } from 'react';
import {
DefaultEntityFilters,
EntityListContext,
EntityListContextProps,
} from '@backstage/plugin-catalog-react';
import { createVersionedValueMap } from '@backstage/version-bridge';
import { NewEntityListContext } from '../hooks/useEntityListProvider';
/**
* Simplifies testing of code that uses the entity list hooks.
@@ -82,8 +83,10 @@ export function MockEntityListContextProvider<
);
return (
<EntityListContext.Provider value={resolvedValue}>
<NewEntityListContext.Provider
value={createVersionedValueMap({ 1: resolvedValue })}
>
{children}
</EntityListContext.Provider>
</NewEntityListContext.Provider>
);
}