From d47aaa3250eee541e65848dd62a3c39a4ce9b738 Mon Sep 17 00:00:00 2001 From: Mahendra Patel Date: Thu, 20 Mar 2025 03:16:34 +0530 Subject: [PATCH] Added EntityOrderFilter to sort entities by different fields Signed-off-by: Mahendra Patel --- .changeset/sharp-numbers-doubt.md | 20 +++++++++++++++++++ .../UserListPicker/UserListPicker.test.tsx | 20 +++++++++++++++++++ .../useAllEntitiesCount.test.tsx | 6 ++++++ .../useOwnedEntitiesCount.test.tsx | 12 +++++++++++ .../useStarredEntitiesCount.test.tsx | 6 ++++++ plugins/catalog-react/src/filters.ts | 17 ++++++++++++++++ .../src/hooks/useEntityListProvider.tsx | 3 ++- plugins/catalog-react/src/utils/filters.ts | 16 ++++++++++++++- 8 files changed, 98 insertions(+), 2 deletions(-) create mode 100644 .changeset/sharp-numbers-doubt.md diff --git a/.changeset/sharp-numbers-doubt.md b/.changeset/sharp-numbers-doubt.md new file mode 100644 index 0000000000..93f1b5f233 --- /dev/null +++ b/.changeset/sharp-numbers-doubt.md @@ -0,0 +1,20 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Added EntityOrderFilter to sort entities by different fields/columns. This new filter allows users to specify the order in which entities are displayed in the catalog. + +Example usage: + +```ts +import { EntityOrderFilter } from '@backstage/plugin-catalog-react'; + +updateFilters({ + order: new EntityOrderFilter([ + { + field: 'metadata.name', + order: 'desc', + }, + ]), +}); +``` diff --git a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx index 2471ce5277..0952822f95 100644 --- a/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/UserListPicker.test.tsx @@ -60,6 +60,12 @@ const mockUser: UserEntity = { }; const ownershipEntityRefs = ['user:default/testuser']; +const orderFields = [ + { + field: 'metadata.name', + order: 'asc', + }, +]; const mockConfigApi = mockApis.config({ data: { organization: { name: 'Test Company' } }, @@ -190,6 +196,7 @@ describe('', () => { 'metadata.namespace': ['default'], }, limit: 0, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { @@ -197,6 +204,7 @@ describe('', () => { 'relations.ownedBy': ['user:default/testuser'], }, limit: 0, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { @@ -204,6 +212,7 @@ describe('', () => { 'metadata.name': ['e-1', 'e-2'], }, limit: 1000, + orderFields, }); }); @@ -229,10 +238,12 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { 'metadata.tags': ['tag1'] }, limit: 0, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { 'metadata.name': ['e-1', 'e-2'], 'metadata.tags': ['tag1'] }, limit: 1000, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { @@ -240,6 +251,7 @@ describe('', () => { 'metadata.tags': ['tag1'], }, limit: 0, + orderFields, }); }); @@ -272,10 +284,12 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { kind: 'component' }, limit: 0, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { kind: 'component', 'metadata.name': ['e-1', 'e-2'] }, limit: 1000, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { @@ -283,6 +297,7 @@ describe('', () => { 'relations.ownedBy': ['user:default/testuser'], }, limit: 0, + orderFields, }); }); @@ -308,10 +323,12 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { kind: 'component', 'metadata.name': ['e-1', 'e-2'] }, limit: 1000, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { kind: 'component' }, limit: 0, + orderFields, }); expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { @@ -319,6 +336,7 @@ describe('', () => { 'relations.ownedBy': ['user:default/testuser'], }, limit: 0, + orderFields, }); }); @@ -451,6 +469,7 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { 'metadata.name': ['e-1', 'e-2'] }, limit: 1000, + orderFields, }); }); expect(updateFilters).not.toHaveBeenCalledWith({ @@ -624,6 +643,7 @@ describe('', () => { expect(mockCatalogApi.queryEntities).toHaveBeenCalledWith({ filter: { 'metadata.name': ['e-1', 'e-2'] }, limit: 1000, + orderFields, }); }); expect(updateFilters).not.toHaveBeenCalledWith({ diff --git a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx index 2449d47a1c..718f7499a9 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useAllEntitiesCount.test.tsx @@ -74,6 +74,12 @@ describe('useAllEntitiesCount', () => { 'relations.ownedBy': ['user:default/owner'], }, limit: 0, + orderFields: [ + { + field: 'metadata.name', + order: 'asc', + }, + ], }), ); expect(result.current).toEqual({ count: 10, loading: false }); diff --git a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx index 8ea9f63472..7b4f89d70a 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useOwnedEntitiesCount.test.tsx @@ -115,6 +115,12 @@ describe('useOwnedEntitiesCount', () => { 'relations.ownedBy': ['user:default/spiderman', 'user:group/a-group'], }, limit: 0, + orderFields: [ + { + field: 'metadata.name', + order: 'asc', + }, + ], }), ); @@ -190,6 +196,12 @@ describe('useOwnedEntitiesCount', () => { 'relations.ownedBy': ['user:group/a-group'], }, limit: 0, + orderFields: [ + { + field: 'metadata.name', + order: 'asc', + }, + ], }), ); diff --git a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx index 096ad01ddd..8808d4a3b6 100644 --- a/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx +++ b/plugins/catalog-react/src/components/UserListPicker/useStarredEntitiesCount.test.tsx @@ -87,6 +87,12 @@ describe('useStarredEntitiesCount', () => { 'metadata.name': ['favourite1', 'favourite2'], }, limit: 1000, + orderFields: [ + { + field: 'metadata.name', + order: 'asc', + }, + ], }); expect(result.current).toEqual({ count: 2, diff --git a/plugins/catalog-react/src/filters.ts b/plugins/catalog-react/src/filters.ts index 3ac7bda73a..3bbae986fa 100644 --- a/plugins/catalog-react/src/filters.ts +++ b/plugins/catalog-react/src/filters.ts @@ -23,6 +23,7 @@ import { import { AlphaEntity } from '@backstage/catalog-model/alpha'; import { EntityFilter, UserListFilterKind } from './types'; import { getEntityRelations } from './utils/getEntityRelations'; +import { EntityOrderQuery } from '@backstage/catalog-client'; /** * Filter entities based on Kind. @@ -331,3 +332,19 @@ export class EntityErrorFilter implements EntityFilter { return error !== undefined && this.value === error; } } + +/** + * Sort entities by a given field/column. + * @public + */ +export class EntityOrderFilter implements EntityFilter { + constructor(readonly value: EntityOrderQuery) {} + + getOrderFilters(): EntityOrderQuery { + return this.value; + } + + filterEntity(_: Entity): boolean { + return true; + } +} diff --git a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx index fc2dfe6a73..3b09434c81 100644 --- a/plugins/catalog-react/src/hooks/useEntityListProvider.tsx +++ b/plugins/catalog-react/src/hooks/useEntityListProvider.tsx @@ -35,6 +35,7 @@ import { EntityKindFilter, EntityLifecycleFilter, EntityNamespaceFilter, + EntityOrderFilter, EntityOrphanFilter, EntityOwnerFilter, EntityTagFilter, @@ -64,6 +65,7 @@ export type DefaultEntityFilters = { orphan?: EntityOrphanFilter; error?: EntityErrorFilter; namespace?: EntityNamespaceFilter; + order?: EntityOrderFilter; }; /** @public */ @@ -287,7 +289,6 @@ export const EntityListProvider = ( ...backendFilter, limit, offset, - orderFields: [{ field: 'metadata.name', order: 'asc' }], }); setOutputState({ appliedFilters: requestedFilters, diff --git a/plugins/catalog-react/src/utils/filters.ts b/plugins/catalog-react/src/utils/filters.ts index 82d7358b91..1ff7334ffb 100644 --- a/plugins/catalog-react/src/utils/filters.ts +++ b/plugins/catalog-react/src/utils/filters.ts @@ -19,6 +19,7 @@ import { EntityFilter } from '../types'; import { EntityLifecycleFilter, EntityNamespaceFilter, + EntityOrderFilter, EntityOrphanFilter, EntityOwnerFilter, EntityTagFilter, @@ -26,18 +27,24 @@ import { EntityUserFilter, UserListFilter, } from '../filters'; +import { EntityOrderQuery } from '@backstage/catalog-client'; export interface CatalogFilters { filter: Record; fullTextFilter?: { term: string; }; + orderFields?: EntityOrderQuery; } function isEntityTextFilter(t: EntityFilter): t is EntityTextFilter { return !!(t as EntityTextFilter).getFullTextFilters; } +function isEntityOrderFilter(t: EntityFilter): t is EntityOrderFilter { + return !!(t as EntityOrderFilter).getOrderFilters; +} + export function reduceCatalogFilters(filters: EntityFilter[]): CatalogFilters { const condensedFilters = filters.reduce( (compoundFilter, filter) => { @@ -50,7 +57,14 @@ export function reduceCatalogFilters(filters: EntityFilter[]): CatalogFilters { ); const fullTextFilter = filters.find(isEntityTextFilter)?.getFullTextFilters(); - return { filter: condensedFilters, fullTextFilter }; + + const orderFields = filters.find(isEntityOrderFilter)?.getOrderFilters() || [ + { + field: 'metadata.name', + order: 'asc', + }, + ]; + return { filter: condensedFilters, fullTextFilter, orderFields }; } /**