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 };
}
/**