feat(catalog): Add support for server side text filtering.
Signed-off-by: Aramis <sennyeyaramis@gmail.com>
This commit is contained in:
@@ -177,7 +177,10 @@ const routes = (
|
||||
<Route path="/home" element={<HomepageCompositionRoot />}>
|
||||
{homePage}
|
||||
</Route>
|
||||
<Route path="/catalog" element={<CatalogIndexPage />} />
|
||||
<Route
|
||||
path="/catalog"
|
||||
element={<CatalogIndexPage pagination={{ limit: 3 }} />}
|
||||
/>
|
||||
<Route
|
||||
path="/catalog/:namespace/:kind/:name"
|
||||
element={<CatalogEntityPage />}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
import { RESOURCE_TYPE_CATALOG_ENTITY } from '@backstage/plugin-catalog-common/alpha';
|
||||
import { CatalogProcessingOrchestrator } from '../processing/types';
|
||||
import { z } from 'zod';
|
||||
import { encodeCursor } from './util';
|
||||
import { decodeCursor, encodeCursor } from './util';
|
||||
import { wrapInOpenApiTestServer } from '@backstage/backend-openapi-utils';
|
||||
import { Server } from 'http';
|
||||
|
||||
@@ -265,6 +265,47 @@ describe('createRouter readonly disabled', () => {
|
||||
});
|
||||
});
|
||||
|
||||
it('parses cursor request with fullTextFilter', async () => {
|
||||
const items: Entity[] = [
|
||||
{ apiVersion: 'a', kind: 'b', metadata: { name: 'n' } },
|
||||
];
|
||||
|
||||
entitiesCatalog.queryEntities.mockResolvedValueOnce({
|
||||
items,
|
||||
totalItems: 100,
|
||||
pageInfo: {
|
||||
nextCursor: mockCursor({ fullTextFilter: { term: 'mySearch' } }),
|
||||
},
|
||||
});
|
||||
|
||||
const cursor = mockCursor({
|
||||
totalItems: 100,
|
||||
isPrevious: false,
|
||||
fullTextFilter: { term: 'mySearch' },
|
||||
});
|
||||
|
||||
const response = await request(app).get(
|
||||
`/entities/by-query?cursor=${encodeCursor(cursor)}`,
|
||||
);
|
||||
expect(entitiesCatalog.queryEntities).toHaveBeenCalledTimes(1);
|
||||
expect(entitiesCatalog.queryEntities).toHaveBeenCalledWith({
|
||||
cursor,
|
||||
});
|
||||
expect(response.status).toEqual(200);
|
||||
expect(response.body).toEqual({
|
||||
items,
|
||||
totalItems: 100,
|
||||
pageInfo: { nextCursor: expect.any(String) },
|
||||
});
|
||||
const decodedCursor = decodeCursor(response.body.pageInfo.nextCursor);
|
||||
expect(decodedCursor).toMatchObject({
|
||||
isPrevious: false,
|
||||
fullTextFilter: {
|
||||
term: 'mySearch',
|
||||
},
|
||||
});
|
||||
});
|
||||
|
||||
it('should throw in case of malformed cursor', async () => {
|
||||
const items: Entity[] = [
|
||||
{ apiVersion: 'a', kind: 'b', metadata: { name: 'n' } },
|
||||
|
||||
@@ -106,6 +106,12 @@ export const cursorParser: z.ZodSchema<Cursor> = z.object({
|
||||
orderFields: z.array(
|
||||
z.object({ field: z.string(), order: z.enum(['asc', 'desc']) }),
|
||||
),
|
||||
fullTextFilter: z
|
||||
.object({
|
||||
term: z.string(),
|
||||
fields: z.array(z.string()).optional(),
|
||||
})
|
||||
.optional(),
|
||||
orderFieldValues: z.array(z.string().or(z.null())),
|
||||
filter: entityFilterParser.optional(),
|
||||
isPrevious: z.boolean(),
|
||||
|
||||
@@ -124,6 +124,7 @@ type OutputState<EntityFilters extends DefaultEntityFilters> = {
|
||||
entities: Entity[];
|
||||
backendEntities: Entity[];
|
||||
pageInfo?: QueryEntitiesResponse['pageInfo'];
|
||||
textSearch?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -232,9 +233,17 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
|
||||
compact(Object.values(outputState.appliedFilters)),
|
||||
);
|
||||
|
||||
if (!isEqual(previousBackendFilter, backendFilter)) {
|
||||
if (
|
||||
!isEqual(previousBackendFilter, backendFilter) ||
|
||||
requestedFilters.text?.value !== outputState.textSearch
|
||||
) {
|
||||
const response = await catalogApi.queryEntities({
|
||||
filter: backendFilter,
|
||||
fullTextFilter: requestedFilters.text
|
||||
? {
|
||||
term: requestedFilters.text.value,
|
||||
}
|
||||
: undefined,
|
||||
limit,
|
||||
orderFields: [{ field: 'metadata.name', order: 'asc' }],
|
||||
});
|
||||
@@ -243,6 +252,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
|
||||
backendEntities: response.items,
|
||||
entities: response.items.filter(entityFilter),
|
||||
pageInfo: response.pageInfo,
|
||||
textSearch: requestedFilters.text?.value,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -317,7 +327,7 @@ export const EntityListProvider = <EntityFilters extends DefaultEntityFilters>(
|
||||
// changing filters will affect pagination, so we need to reset
|
||||
// the cursor and start from the first page.
|
||||
// TODO(vinzscam): this is currently causing issues at page reload
|
||||
// where the state is not kept. Unfortunately we need to rething
|
||||
// where the state is not kept. Unfortunately we need to rethink
|
||||
// the way filters work in order to fix this.
|
||||
setCursor(undefined);
|
||||
setRequestedFilters(prevFilters => {
|
||||
|
||||
@@ -18,6 +18,10 @@ import React from 'react';
|
||||
|
||||
import { Table, TableProps } from '@backstage/core-components';
|
||||
import { CatalogTableRow } from './types';
|
||||
import {
|
||||
EntityTextFilter,
|
||||
useEntityList,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
type PaginatedCatalogTableProps = {
|
||||
prev?(): void;
|
||||
@@ -29,6 +33,7 @@ type PaginatedCatalogTableProps = {
|
||||
*/
|
||||
export function PaginatedCatalogTable(props: PaginatedCatalogTableProps) {
|
||||
const { columns, data, next, prev } = props;
|
||||
const { updateFilters } = useEntityList();
|
||||
|
||||
return (
|
||||
<Table
|
||||
@@ -41,6 +46,11 @@ export function PaginatedCatalogTable(props: PaginatedCatalogTableProps) {
|
||||
pageSize: Number.MAX_SAFE_INTEGER,
|
||||
emptyRowsWhenPaging: false,
|
||||
}}
|
||||
onSearchChange={(searchText: string) =>
|
||||
updateFilters({
|
||||
text: searchText ? new EntityTextFilter(searchText) : undefined,
|
||||
})
|
||||
}
|
||||
onPageChange={page => {
|
||||
if (page > 0) {
|
||||
next?.();
|
||||
|
||||
Reference in New Issue
Block a user