feat(catalog): Add support for server side text filtering.

Signed-off-by: Aramis <sennyeyaramis@gmail.com>
This commit is contained in:
Aramis
2024-01-08 00:35:51 -05:00
parent aec45d056d
commit 4e759dd9b1
5 changed files with 74 additions and 4 deletions
+4 -1
View File
@@ -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?.();