catalog: toggle pagination through props
Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
@@ -2,13 +2,17 @@
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Added experimental pagination support to `CatalogIndexPage`
|
||||
Added pagination support to `CatalogIndexPage`
|
||||
|
||||
CatalogIndexPage now offers an optional pagination feature, designed to accommodate adopters managing extensive catalogs. This new capability allows for better handling of large amounts of data.
|
||||
`CatalogIndexPage` now offers an optional pagination feature, designed to accommodate adopters managing extensive catalogs. This new capability allows for better handling of large amounts of data.
|
||||
|
||||
To activate the pagination mode, simply update your configuration as follows:
|
||||
To activate the pagination mode, simply update your `App.tsx` as follows:
|
||||
|
||||
```diff
|
||||
catalog:
|
||||
+ experimentalPagination: true
|
||||
const routes = (
|
||||
<FlatRoutes>
|
||||
...
|
||||
- <Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
+ <Route path="/catalog" element={<CatalogIndexPage pagination />} />
|
||||
...
|
||||
```
|
||||
|
||||
@@ -297,11 +297,7 @@ export const EntityListProvider: <EntityFilters extends DefaultEntityFilters>(
|
||||
|
||||
// @public (undocumented)
|
||||
export type EntityListProviderProps = PropsWithChildren<{
|
||||
enablePagination?:
|
||||
| boolean
|
||||
| {
|
||||
limit?: number;
|
||||
};
|
||||
pagination?: Pagination;
|
||||
}>;
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -657,6 +653,13 @@ export class MockStarredEntitiesApi implements StarredEntitiesApi {
|
||||
toggleStarred(entityRef: string): Promise<void>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export type Pagination =
|
||||
| boolean
|
||||
| {
|
||||
limit?: number;
|
||||
};
|
||||
|
||||
// @public
|
||||
export interface StarredEntitiesApi {
|
||||
starredEntitie$(): Observable<Set<string>>;
|
||||
|
||||
@@ -33,6 +33,7 @@ export type {
|
||||
DefaultEntityFilters,
|
||||
EntityListContextProps,
|
||||
EntityListProviderProps,
|
||||
Pagination,
|
||||
} from './useEntityListProvider';
|
||||
export { useEntityTypeFilter } from './useEntityTypeFilter';
|
||||
export { useRelatedEntities } from './useRelatedEntities';
|
||||
|
||||
@@ -87,7 +87,7 @@ const mockCatalogApi: Partial<jest.Mocked<CatalogApi>> = {
|
||||
};
|
||||
|
||||
const createWrapper =
|
||||
(options: { location?: string; enablePagination: boolean }) =>
|
||||
(options: { location?: string; pagination: boolean }) =>
|
||||
(props: PropsWithChildren) => {
|
||||
const InitialFiltersWrapper = ({ children }: PropsWithChildren) => {
|
||||
const { updateFilters } = useEntityList();
|
||||
@@ -111,7 +111,7 @@ const createWrapper =
|
||||
[alertApiRef, { post: jest.fn() }],
|
||||
]}
|
||||
>
|
||||
<EntityListProvider enablePagination={options.enablePagination}>
|
||||
<EntityListProvider pagination={options.pagination}>
|
||||
<InitialFiltersWrapper>{props.children}</InitialFiltersWrapper>
|
||||
</EntityListProvider>
|
||||
</TestApiProvider>
|
||||
@@ -121,7 +121,7 @@ const createWrapper =
|
||||
|
||||
describe('<EntityListProvider />', () => {
|
||||
const origReplaceState = window.history.replaceState;
|
||||
const enablePagination = false;
|
||||
const pagination = false;
|
||||
|
||||
beforeEach(() => {
|
||||
window.history.replaceState = jest.fn();
|
||||
@@ -136,7 +136,7 @@ describe('<EntityListProvider />', () => {
|
||||
|
||||
it('should send backend filters', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -152,7 +152,7 @@ describe('<EntityListProvider />', () => {
|
||||
|
||||
it('resolves frontend filters', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
initialProps: {
|
||||
userFilter: 'all',
|
||||
},
|
||||
@@ -178,7 +178,7 @@ describe('<EntityListProvider />', () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({
|
||||
location: `/catalog?${query}`,
|
||||
enablePagination,
|
||||
pagination,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -193,7 +193,7 @@ describe('<EntityListProvider />', () => {
|
||||
|
||||
it('does not fetch when only frontend filters change', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -220,7 +220,7 @@ describe('<EntityListProvider />', () => {
|
||||
|
||||
it('debounces multiple filter changes', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -243,7 +243,7 @@ describe('<EntityListProvider />', () => {
|
||||
|
||||
it('returns an error on catalogApi failure', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -262,7 +262,7 @@ describe('<EntityListProvider />', () => {
|
||||
|
||||
it('returns an empty pageInfo', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockCatalogApi.getEntities).toHaveBeenCalled();
|
||||
@@ -272,9 +272,9 @@ describe('<EntityListProvider />', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('<EntityListProvider enablePagination />', () => {
|
||||
describe('<EntityListProvider pagination />', () => {
|
||||
const origReplaceState = window.history.replaceState;
|
||||
const enablePagination = true;
|
||||
const pagination = true;
|
||||
const limit = 20;
|
||||
const orderFields = [{ field: 'metadata.name', order: 'asc' }];
|
||||
|
||||
@@ -291,7 +291,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
|
||||
it('should send backend filters', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -309,7 +309,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
|
||||
it('resolves frontend filters', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
initialProps: {
|
||||
userFilter: 'all',
|
||||
},
|
||||
@@ -335,7 +335,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({
|
||||
location: `/catalog?${query}`,
|
||||
enablePagination,
|
||||
pagination,
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -350,7 +350,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
|
||||
it('fetch when frontend filters change', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -375,7 +375,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
|
||||
it('debounces multiple filter changes', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -400,7 +400,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
|
||||
it('returns an error on catalogApi failure', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
@@ -425,7 +425,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
totalItems: 10,
|
||||
});
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalled();
|
||||
@@ -439,7 +439,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
|
||||
it('returns pageInfo with next function and properly fetch next batch', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalled();
|
||||
@@ -463,7 +463,7 @@ describe('<EntityListProvider enablePagination />', () => {
|
||||
|
||||
it('returns pageInfo with prev function and properly fetch prev batch', async () => {
|
||||
const { result } = renderHook(() => useEntityList(), {
|
||||
wrapper: createWrapper({ enablePagination }),
|
||||
wrapper: createWrapper({ pagination }),
|
||||
});
|
||||
await waitFor(() => {
|
||||
expect(mockCatalogApi.queryEntities).toHaveBeenCalled();
|
||||
|
||||
@@ -130,9 +130,14 @@ type OutputState<EntityFilters extends DefaultEntityFilters> = {
|
||||
* @public
|
||||
*/
|
||||
export type EntityListProviderProps = PropsWithChildren<{
|
||||
enablePagination?: boolean | { limit?: number };
|
||||
pagination?: Pagination;
|
||||
}>;
|
||||
|
||||
/**
|
||||
* @public
|
||||
*/
|
||||
export type Pagination = boolean | { limit?: number };
|
||||
|
||||
/**
|
||||
* Provides entities and filters for a catalog listing.
|
||||
* @public
|
||||
@@ -153,14 +158,13 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
|
||||
const location = useLocation();
|
||||
|
||||
const enablePagination =
|
||||
props.enablePagination === true ||
|
||||
typeof props.enablePagination === 'object';
|
||||
props.pagination === true || typeof props.pagination === 'object';
|
||||
|
||||
const limit =
|
||||
props.enablePagination &&
|
||||
typeof props.enablePagination === 'object' &&
|
||||
typeof props.enablePagination.limit === 'number'
|
||||
? props.enablePagination.limit
|
||||
props.pagination &&
|
||||
typeof props.pagination === 'object' &&
|
||||
typeof props.pagination.limit === 'number'
|
||||
? props.pagination.limit
|
||||
: 20;
|
||||
|
||||
const { queryParameters, cursor: initialCursor } = useMemo(() => {
|
||||
|
||||
@@ -158,9 +158,7 @@ export const CatalogTable: {
|
||||
createSystemColumn(): TableColumn<CatalogTableRow>;
|
||||
createOwnerColumn(): TableColumn<CatalogTableRow>;
|
||||
createSpecTargetsColumn(): TableColumn<CatalogTableRow>;
|
||||
createSpecTypeColumn({
|
||||
hidden,
|
||||
}?: {
|
||||
createSpecTypeColumn(options?: {
|
||||
hidden: boolean;
|
||||
}): TableColumn<CatalogTableRow>;
|
||||
createSpecLifecycleColumn(): TableColumn<CatalogTableRow>;
|
||||
@@ -238,6 +236,12 @@ export interface DefaultCatalogPageProps {
|
||||
// (undocumented)
|
||||
ownerPickerMode?: EntityOwnerPickerProps['mode'];
|
||||
// (undocumented)
|
||||
pagination?:
|
||||
| boolean
|
||||
| {
|
||||
limit?: number;
|
||||
};
|
||||
// (undocumented)
|
||||
tableOptions?: TableProps<CatalogTableRow>['options'];
|
||||
}
|
||||
|
||||
|
||||
@@ -37,6 +37,7 @@ import {
|
||||
EntityKindPicker,
|
||||
EntityNamespacePicker,
|
||||
EntityOwnerPickerProps,
|
||||
Pagination,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { createComponentRouteRef } from '../../routes';
|
||||
@@ -47,23 +48,20 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { CatalogTableColumnsFunc } from '../CatalogTable/types';
|
||||
|
||||
/** @internal */
|
||||
export interface BaseCatalogPageProps {
|
||||
export type BaseCatalogPageProps = {
|
||||
filters: ReactNode;
|
||||
content?: ReactNode;
|
||||
}
|
||||
pagination?: Pagination;
|
||||
};
|
||||
|
||||
/** @internal */
|
||||
export function BaseCatalogPage(props: BaseCatalogPageProps) {
|
||||
const { filters, content = <CatalogTable /> } = props;
|
||||
const { filters, content = <CatalogTable />, pagination } = props;
|
||||
const orgName =
|
||||
useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';
|
||||
const createComponentLink = useRouteRef(createComponentRouteRef);
|
||||
const { t } = useTranslationRef(catalogTranslationRef);
|
||||
|
||||
const experimentalPagination = useApi(configApiRef).getOptional(
|
||||
'catalog.experimentalPagination',
|
||||
) as boolean | { limit: number } | undefined;
|
||||
|
||||
return (
|
||||
<PageWithHeader title={t('catalog_page_title', { orgName })} themeId="home">
|
||||
<Content>
|
||||
@@ -74,7 +72,7 @@ export function BaseCatalogPage(props: BaseCatalogPageProps) {
|
||||
/>
|
||||
<SupportButton>All your software catalog entities</SupportButton>
|
||||
</ContentHeader>
|
||||
<EntityListProvider enablePagination={experimentalPagination}>
|
||||
<EntityListProvider pagination={pagination}>
|
||||
<CatalogFilterLayout>
|
||||
<CatalogFilterLayout.Filters>{filters}</CatalogFilterLayout.Filters>
|
||||
<CatalogFilterLayout.Content>{content}</CatalogFilterLayout.Content>
|
||||
@@ -98,6 +96,7 @@ export interface DefaultCatalogPageProps {
|
||||
tableOptions?: TableProps<CatalogTableRow>['options'];
|
||||
emptyContent?: ReactNode;
|
||||
ownerPickerMode?: EntityOwnerPickerProps['mode'];
|
||||
pagination?: boolean | { limit?: number };
|
||||
}
|
||||
|
||||
export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
|
||||
@@ -108,6 +107,7 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
|
||||
initialKind = 'component',
|
||||
tableOptions = {},
|
||||
emptyContent,
|
||||
pagination,
|
||||
ownerPickerMode,
|
||||
} = props;
|
||||
|
||||
@@ -133,6 +133,7 @@ export function DefaultCatalogPage(props: DefaultCatalogPageProps) {
|
||||
emptyContent={emptyContent}
|
||||
/>
|
||||
}
|
||||
pagination={pagination}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user