diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx
index d2dde95f68..2ea0fefdea 100644
--- a/packages/app/src/App.tsx
+++ b/packages/app/src/App.tsx
@@ -177,7 +177,10 @@ const routes = (
}>
{homePage}
- } />
+ }
+ />
}
diff --git a/plugins/catalog-backend/src/service/createRouter.test.ts b/plugins/catalog-backend/src/service/createRouter.test.ts
index 05ebf6e1a9..809d970612 100644
--- a/plugins/catalog-backend/src/service/createRouter.test.ts
+++ b/plugins/catalog-backend/src/service/createRouter.test.ts
@@ -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' } },
diff --git a/plugins/catalog-backend/src/service/util.ts b/plugins/catalog-backend/src/service/util.ts
index af1279a59a..4b041d1a70 100644
--- a/plugins/catalog-backend/src/service/util.ts
+++ b/plugins/catalog-backend/src/service/util.ts
@@ -106,6 +106,12 @@ export const cursorParser: z.ZodSchema = 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(),
diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx
index 1ce87c262e..81bf8fb9e3 100644
--- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx
+++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx
@@ -124,6 +124,7 @@ type OutputState = {
entities: Entity[];
backendEntities: Entity[];
pageInfo?: QueryEntitiesResponse['pageInfo'];
+ textSearch?: string;
};
/**
@@ -232,9 +233,17 @@ export const EntityListProvider = (
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 = (
backendEntities: response.items,
entities: response.items.filter(entityFilter),
pageInfo: response.pageInfo,
+ textSearch: requestedFilters.text?.value,
});
}
}
@@ -317,7 +327,7 @@ export const EntityListProvider = (
// 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 => {
diff --git a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx
index 771878b65c..0311cd22ca 100644
--- a/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx
+++ b/plugins/catalog/src/components/CatalogTable/PaginatedCatalogTable.tsx
@@ -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 (
+ updateFilters({
+ text: searchText ? new EntityTextFilter(searchText) : undefined,
+ })
+ }
onPageChange={page => {
if (page > 0) {
next?.();