catalog: toggle pagination through props

Signed-off-by: Vincenzo Scamporlino <vincenzos@spotify.com>
This commit is contained in:
Vincenzo Scamporlino
2023-11-21 12:13:03 +01:00
parent 9cc670cf68
commit 992671e2c8
7 changed files with 66 additions and 49 deletions
+9 -5
View File
@@ -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 />} />
...
```
+8 -5
View File
@@ -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>>;
+1
View File
@@ -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(() => {
+7 -3
View File
@@ -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}
/>
);
}