From adde47f07648fe238dfa3939dd6e8c5df986c645 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:27:34 +0200 Subject: [PATCH 1/7] fix(plugin-catalog): add searchable column for displayName Signed-off-by: Julien --- .../catalog/src/components/CatalogTable/columns.tsx | 10 ++++++++++ .../CatalogTable/defaultCatalogTableColumnsFunc.tsx | 10 +++++++++- 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index d5362badda..fc804fc22f 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -180,6 +180,16 @@ export const columnFactories = Object.freeze({ searchable: true, }; }, + createDisplayNameColumn(options?: { + hidden?: boolean; + }): TableColumn { + return { + title: 'Display Name', + field: 'entity.spec.profile.displayName', + hidden: options?.hidden, + searchable: true, + }; + }, createLabelColumn( key: string, options?: { title?: string; defaultValue?: string }, diff --git a/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx b/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx index 54dd701770..f5f41c1e32 100644 --- a/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx +++ b/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx @@ -46,11 +46,19 @@ export const defaultCatalogTableColumnsFunc: CatalogTableColumnsFunc = ({ ]; switch (filters.kind?.value) { case 'user': - return [...descriptionTagColumns]; + return [ + columnFactories.createDisplayNameColumn({ hidden: true }), + ...descriptionTagColumns, + ]; case 'domain': case 'system': return [columnFactories.createOwnerColumn(), ...descriptionTagColumns]; case 'group': + return [ + columnFactories.createDisplayNameColumn({ hidden: true }), + columnFactories.createSpecTypeColumn({ hidden: !showTypeColumn }), + ...descriptionTagColumns, + ]; case 'template': return [ columnFactories.createSpecTypeColumn({ hidden: !showTypeColumn }), From b5c8fb4aafa35a9d9ea1efaeab88c645db946b4a Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:28:46 +0200 Subject: [PATCH 2/7] fix(plugin-catalog-react): add partial match text filter for displayName Signed-off-by: Julien --- plugins/catalog-react/src/filters.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 08be27a561..6c68dad9aa 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -94,6 +94,7 @@ export class EntityTextFilter implements EntityFilter { const partialMatch = this.toUpperArray([ entity.metadata.name, entity.metadata.title, + (entity.spec?.profile as { displayName?: string })?.displayName, ]); for (const word of words) { From e3f5e20e9493818d033773a67f58c32b7f738d8f Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:29:47 +0200 Subject: [PATCH 3/7] fix(plugin-catalog-backend): lowercase text filter fields to match database values Signed-off-by: Julien --- .../catalog-backend/src/service/DefaultEntitiesCatalog.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts index c71f242969..bb46691e76 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.ts @@ -422,7 +422,11 @@ export class DefaultEntitiesCatalog implements EntitiesCatalog { } else { const matchQuery = db('search') .select('search.entity_id') - .whereIn('key', textFilterFields) + // textFilterFields must be lowercased to match searchable keys in database, i.e. spec.profile.displayName -> spec.profile.displayname + .whereIn( + 'key', + textFilterFields.map(field => field.toLocaleLowerCase('en-US')), + ) .andWhere(function keyFilter() { this.andWhereRaw( 'value like ?', From db7ea24112afdb9d606a70f847f0b929b406dd89 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 11:59:50 +0200 Subject: [PATCH 4/7] test(catalog): add test for search by displayName Signed-off-by: Julien --- .../service/DefaultEntitiesCatalog.test.ts | 55 ++++++++++++++++++- plugins/catalog-react/src/filters.test.ts | 19 +++++++ 2 files changed, 73 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts index 92acd9e3ce..da84103e86 100644 --- a/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts +++ b/plugins/catalog-backend/src/service/DefaultEntitiesCatalog.test.ts @@ -1124,7 +1124,6 @@ describe('DefaultEntitiesCatalog', () => { const request: QueryEntitiesInitialRequest = { filter, limit: 100, - orderFields: [{ field: 'metadata.name', order: 'asc' }], fullTextFilter: { term: 'cAt ' }, credentials: mockCredentials.none(), @@ -1141,6 +1140,60 @@ describe('DefaultEntitiesCatalog', () => { }, ); + it.each(databases.eachSupportedId())( + 'should filter the results when query is provided with fullTextFilter for camelCase fields, %p', + async databaseId => { + await createDatabase(databaseId); + + const entities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { + name: 'camelCase', + }, + spec: { + shouldSearchCamelCase: 'searched', + }, + }, + ]; + + const notFoundEntities: Entity[] = [ + { + apiVersion: 'a', + kind: 'k', + metadata: { name: 'something' }, + spec: {}, + }, + ]; + + await Promise.all( + entities.concat(notFoundEntities).map(e => addEntityToSearch(e)), + ); + + const catalog = new DefaultEntitiesCatalog({ + database: knex, + logger: mockServices.logger.mock(), + stitcher, + }); + + const request: QueryEntitiesInitialRequest = { + limit: 100, + orderFields: [{ field: 'metadata.name', order: 'asc' }], + fullTextFilter: { + term: 'sear', + fields: ['spec.shouldSearchCamelCase'], + }, + credentials: mockCredentials.none(), + }; + const response = await catalog.queryEntities(request); + expect(response.items).toEqual(entities); + expect(response.pageInfo.nextCursor).toBeUndefined(); + expect(response.pageInfo.prevCursor).toBeUndefined(); + expect(response.totalItems).toBe(1); + }, + ); + it.each(databases.eachSupportedId())( 'should filter the text results when sortOrder is not provided, %p', async databaseId => { diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index 8ddac7e53b..332f4c22c1 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -41,6 +41,18 @@ const entities: Entity[] = [ tags: ['gRPC', 'java'], }, }, + { + apiVersion: '1', + kind: 'User', + metadata: { + name: 'user', + }, + spec: { + profile: { + displayName: 'John Doe', + }, + }, + }, ]; const templates: TemplateEntityV1beta3[] = [ @@ -79,6 +91,13 @@ describe('EntityTextFilter', () => { expect(filter.filterEntity(entities[1])).toBeFalsy(); }); + it('should search displayName', () => { + const filter = new EntityTextFilter('John D'); + expect(filter.filterEntity(entities[0])).toBeFalsy(); + expect(filter.filterEntity(entities[1])).toBeFalsy(); + expect(filter.filterEntity(entities[2])).toBeTruthy(); + }); + it('should search template title', () => { const filter = new EntityTextFilter('spring'); expect(filter.filterEntity(templates[0])).toBeFalsy(); From 1bf02cc6ababf619482dc7f42d26da1120f60c12 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 12:11:46 +0200 Subject: [PATCH 5/7] chore: add changeset Signed-off-by: Julien --- .changeset/sixty-sheep-drive.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changeset/sixty-sheep-drive.md diff --git a/.changeset/sixty-sheep-drive.md b/.changeset/sixty-sheep-drive.md new file mode 100644 index 0000000000..0b96248c93 --- /dev/null +++ b/.changeset/sixty-sheep-drive.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-catalog-backend': patch +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch +--- + +Fixed bug when searching an entity by `spec.profile.displayName` in the catalog on the frontend. Text filter fields were not applied correctly to the database query resulting in empty results. From 9f642dbdb8cd4052283c1e7adb421ce70dc176c0 Mon Sep 17 00:00:00 2001 From: Julien Date: Fri, 25 Oct 2024 13:17:13 +0200 Subject: [PATCH 6/7] chore: generate api-reports Signed-off-by: Julien --- plugins/catalog/report.api.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index d817118e3a..9eeae32fb2 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -181,6 +181,13 @@ export const CatalogTable: { } | undefined, ): TableColumn; + createDisplayNameColumn( + options?: + | { + hidden?: boolean | undefined; + } + | undefined, + ): TableColumn; createLabelColumn( key: string, options?: From d8ab481a2846e1090d5b26578d2b33fbcd99019a Mon Sep 17 00:00:00 2001 From: Julien Date: Mon, 18 Nov 2024 10:48:14 +0100 Subject: [PATCH 7/7] fix: rollback unnecessary changes Signed-off-by: Julien --- .changeset/sixty-sheep-drive.md | 2 -- plugins/catalog-react/src/filters.test.ts | 19 ------------------- plugins/catalog-react/src/filters.ts | 1 - plugins/catalog/report.api.md | 7 ------- .../src/components/CatalogTable/columns.tsx | 10 ---------- .../defaultCatalogTableColumnsFunc.tsx | 10 +--------- 6 files changed, 1 insertion(+), 48 deletions(-) diff --git a/.changeset/sixty-sheep-drive.md b/.changeset/sixty-sheep-drive.md index 0b96248c93..2e947b30f9 100644 --- a/.changeset/sixty-sheep-drive.md +++ b/.changeset/sixty-sheep-drive.md @@ -1,7 +1,5 @@ --- '@backstage/plugin-catalog-backend': patch -'@backstage/plugin-catalog-react': patch -'@backstage/plugin-catalog': patch --- Fixed bug when searching an entity by `spec.profile.displayName` in the catalog on the frontend. Text filter fields were not applied correctly to the database query resulting in empty results. diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts index 332f4c22c1..8ddac7e53b 100644 --- a/plugins/catalog-react/src/filters.test.ts +++ b/plugins/catalog-react/src/filters.test.ts @@ -41,18 +41,6 @@ const entities: Entity[] = [ tags: ['gRPC', 'java'], }, }, - { - apiVersion: '1', - kind: 'User', - metadata: { - name: 'user', - }, - spec: { - profile: { - displayName: 'John Doe', - }, - }, - }, ]; const templates: TemplateEntityV1beta3[] = [ @@ -91,13 +79,6 @@ describe('EntityTextFilter', () => { expect(filter.filterEntity(entities[1])).toBeFalsy(); }); - it('should search displayName', () => { - const filter = new EntityTextFilter('John D'); - expect(filter.filterEntity(entities[0])).toBeFalsy(); - expect(filter.filterEntity(entities[1])).toBeFalsy(); - expect(filter.filterEntity(entities[2])).toBeTruthy(); - }); - it('should search template title', () => { const filter = new EntityTextFilter('spring'); expect(filter.filterEntity(templates[0])).toBeFalsy(); diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 6c68dad9aa..08be27a561 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -94,7 +94,6 @@ export class EntityTextFilter implements EntityFilter { const partialMatch = this.toUpperArray([ entity.metadata.name, entity.metadata.title, - (entity.spec?.profile as { displayName?: string })?.displayName, ]); for (const word of words) { diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index fafdc6c9fa..ffb46afc6e 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -181,13 +181,6 @@ export const CatalogTable: { } | undefined, ): TableColumn; - createDisplayNameColumn( - options?: - | { - hidden?: boolean | undefined; - } - | undefined, - ): TableColumn; createLabelColumn( key: string, options?: diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index fc804fc22f..d5362badda 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -180,16 +180,6 @@ export const columnFactories = Object.freeze({ searchable: true, }; }, - createDisplayNameColumn(options?: { - hidden?: boolean; - }): TableColumn { - return { - title: 'Display Name', - field: 'entity.spec.profile.displayName', - hidden: options?.hidden, - searchable: true, - }; - }, createLabelColumn( key: string, options?: { title?: string; defaultValue?: string }, diff --git a/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx b/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx index f5f41c1e32..54dd701770 100644 --- a/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx +++ b/plugins/catalog/src/components/CatalogTable/defaultCatalogTableColumnsFunc.tsx @@ -46,19 +46,11 @@ export const defaultCatalogTableColumnsFunc: CatalogTableColumnsFunc = ({ ]; switch (filters.kind?.value) { case 'user': - return [ - columnFactories.createDisplayNameColumn({ hidden: true }), - ...descriptionTagColumns, - ]; + return [...descriptionTagColumns]; case 'domain': case 'system': return [columnFactories.createOwnerColumn(), ...descriptionTagColumns]; case 'group': - return [ - columnFactories.createDisplayNameColumn({ hidden: true }), - columnFactories.createSpecTypeColumn({ hidden: !showTypeColumn }), - ...descriptionTagColumns, - ]; case 'template': return [ columnFactories.createSpecTypeColumn({ hidden: !showTypeColumn }),