feature(catalog export): process copilot feedback
Signed-off-by: the-serious-programmer <19777147+the-serious-programmer@users.noreply.github.com>
This commit is contained in:
@@ -319,6 +319,8 @@ periodically.
|
||||
For more details on extension overrides and the different override patterns
|
||||
available, see the [extension overrides](../../frontend-system/architecture/25-extension-overrides.md) documentation.
|
||||
|
||||
## Entity page
|
||||
|
||||
### Entity filters
|
||||
|
||||
Many extensions that attach within the catalog entity pages accept a `filter` configuration. The purpose of the `filter` configuration is to select what entities the extension should be applied to or be present on. Many of these extension will have a default filter defined, but you can override it by providing your own. When defining filters in code you can use either a predicate function or a entity predicate query, while in configuration you can only use an entity predicate query.
|
||||
|
||||
@@ -15,4 +15,4 @@
|
||||
*/
|
||||
export * from './useStreamingExport';
|
||||
export * from './serializeEntities';
|
||||
export * from './filtersToStreamRequest';
|
||||
export * from './toStreamRequest';
|
||||
|
||||
+112
-13
@@ -13,14 +13,18 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { filtersToStreamRequest } from './filtersToStreamRequest';
|
||||
import { DefaultEntityFilters } from '@backstage/plugin-catalog-react';
|
||||
import { toStreamRequest } from './toStreamRequest';
|
||||
import {
|
||||
DefaultEntityFilters,
|
||||
EntityTextFilter,
|
||||
EntityOrderFilter,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
describe('filtersToStreamRequest', () => {
|
||||
describe('toStreamRequest', () => {
|
||||
describe('with no filters', () => {
|
||||
it('returns undefined for empty filters object', () => {
|
||||
const filters: DefaultEntityFilters = {};
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
@@ -38,7 +42,7 @@ describe('filtersToStreamRequest', () => {
|
||||
kind: mockFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -65,7 +69,7 @@ describe('filtersToStreamRequest', () => {
|
||||
type: mockTypeFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -86,7 +90,7 @@ describe('filtersToStreamRequest', () => {
|
||||
kind: mockFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -111,7 +115,7 @@ describe('filtersToStreamRequest', () => {
|
||||
text: nonBackendFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -136,7 +140,7 @@ describe('filtersToStreamRequest', () => {
|
||||
owners: mockFilterWithNull as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -157,7 +161,7 @@ describe('filtersToStreamRequest', () => {
|
||||
kind: mockFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -179,7 +183,7 @@ describe('filtersToStreamRequest', () => {
|
||||
kind: mockFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -198,7 +202,7 @@ describe('filtersToStreamRequest', () => {
|
||||
type: mockEmptyFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
@@ -215,7 +219,7 @@ describe('filtersToStreamRequest', () => {
|
||||
type: mockFilter as any,
|
||||
};
|
||||
|
||||
const result = filtersToStreamRequest(filters);
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
@@ -225,4 +229,99 @@ describe('filtersToStreamRequest', () => {
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with text filters', () => {
|
||||
it('extracts full text filter and returns StreamEntitiesRequest', () => {
|
||||
const textFilter = new EntityTextFilter('search term');
|
||||
|
||||
const filters: DefaultEntityFilters = {
|
||||
text: textFilter,
|
||||
};
|
||||
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
fullTextFilter: {
|
||||
term: 'search term',
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.title',
|
||||
'spec.profile.displayName',
|
||||
],
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with order filters', () => {
|
||||
it('extracts order fields and returns StreamEntitiesRequest', () => {
|
||||
const orderFilter = new EntityOrderFilter([['metadata.name', 'asc']]);
|
||||
|
||||
const filters: DefaultEntityFilters = {
|
||||
order: orderFilter,
|
||||
};
|
||||
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
orderFields: [{ field: 'metadata.name', order: 'asc' }],
|
||||
});
|
||||
});
|
||||
|
||||
it('handles multiple order fields', () => {
|
||||
const orderFilter = new EntityOrderFilter([
|
||||
['metadata.name', 'asc'],
|
||||
['spec.type', 'desc'],
|
||||
]);
|
||||
|
||||
const filters: DefaultEntityFilters = {
|
||||
order: orderFilter,
|
||||
};
|
||||
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
orderFields: [
|
||||
{ field: 'metadata.name', order: 'asc' },
|
||||
{ field: 'spec.type', order: 'desc' },
|
||||
],
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('with combined filters', () => {
|
||||
it('combines backend, text, and order filters', () => {
|
||||
const mockBackendFilter = {
|
||||
getCatalogFilters: () => ({
|
||||
kind: ['Component'],
|
||||
}),
|
||||
};
|
||||
|
||||
const textFilter = new EntityTextFilter('search term');
|
||||
const orderFilter = new EntityOrderFilter([['metadata.name', 'asc']]);
|
||||
|
||||
const filters: DefaultEntityFilters = {
|
||||
kind: mockBackendFilter as any,
|
||||
text: textFilter,
|
||||
order: orderFilter,
|
||||
};
|
||||
|
||||
const result = toStreamRequest(filters);
|
||||
|
||||
expect(result).toEqual({
|
||||
filter: {
|
||||
kind: ['Component'],
|
||||
},
|
||||
fullTextFilter: {
|
||||
term: 'search term',
|
||||
fields: [
|
||||
'metadata.name',
|
||||
'metadata.title',
|
||||
'spec.profile.displayName',
|
||||
],
|
||||
},
|
||||
orderFields: [{ field: 'metadata.name', order: 'asc' }],
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
+45
-9
@@ -13,13 +13,28 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { DefaultEntityFilters } from '@backstage/plugin-catalog-react';
|
||||
import type { StreamEntitiesRequest } from '@backstage/catalog-client';
|
||||
import {
|
||||
DefaultEntityFilters,
|
||||
EntityTextFilter,
|
||||
EntityOrderFilter,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import type {
|
||||
StreamEntitiesRequest,
|
||||
EntityOrderQuery,
|
||||
} from '@backstage/catalog-client';
|
||||
|
||||
function isBackendFilter(f: any): f is { getCatalogFilters: () => any } {
|
||||
return typeof f?.getCatalogFilters === 'function';
|
||||
}
|
||||
|
||||
function isEntityTextFilter(f: any): f is EntityTextFilter {
|
||||
return !!(f as EntityTextFilter).getFullTextFilters;
|
||||
}
|
||||
|
||||
function isEntityOrderFilter(f: any): f is EntityOrderFilter {
|
||||
return !!(f as EntityOrderFilter).getOrderFilters;
|
||||
}
|
||||
|
||||
function getBackendFilterObject(
|
||||
backendFilter: Record<string, any>,
|
||||
): Record<string, string | string[]> {
|
||||
@@ -47,13 +62,13 @@ function getBackendFilterObject(
|
||||
* Converts entity filters to a StreamEntitiesRequest that can be used
|
||||
* with the catalogApi.streamEntities method.
|
||||
*
|
||||
* This extracts all enabled backend filters and converts them to the
|
||||
* appropriate format for streaming.
|
||||
* This extracts all enabled backend filters, full text filters, and order filters
|
||||
* and converts them to the appropriate format for streaming.
|
||||
*
|
||||
* @param filters - The entity filters from useEntityList
|
||||
* @returns A StreamEntitiesRequest object, or undefined if no backend filters are enabled
|
||||
* @returns A StreamEntitiesRequest object, or undefined if no filters are enabled
|
||||
*/
|
||||
export const filtersToStreamRequest = (
|
||||
export const toStreamRequest = (
|
||||
filters: DefaultEntityFilters,
|
||||
): StreamEntitiesRequest | undefined => {
|
||||
const backendFilters = Object.values(filters)
|
||||
@@ -68,8 +83,29 @@ export const filtersToStreamRequest = (
|
||||
return { ...acc, ...getBackendFilterObject(f) };
|
||||
}, {} as Record<string, string | string[]>);
|
||||
|
||||
// Return undefined if no filters, which means stream all entities
|
||||
return Object.keys(backendFilters).length > 0
|
||||
? { filter: backendFilters }
|
||||
const fullTextFilter = Object.values(filters)
|
||||
.find(isEntityTextFilter)
|
||||
?.getFullTextFilters();
|
||||
|
||||
const orderFieldsFilter = Object.values(filters).find(isEntityOrderFilter);
|
||||
const orderFields = orderFieldsFilter
|
||||
? (orderFieldsFilter.getOrderFilters() as EntityOrderQuery)
|
||||
: undefined;
|
||||
|
||||
const request: StreamEntitiesRequest = {};
|
||||
|
||||
if (Object.keys(backendFilters).length > 0) {
|
||||
request.filter = backendFilters;
|
||||
}
|
||||
|
||||
if (fullTextFilter) {
|
||||
request.fullTextFilter = fullTextFilter;
|
||||
}
|
||||
|
||||
if (orderFields) {
|
||||
request.orderFields = orderFields;
|
||||
}
|
||||
|
||||
// Return undefined if no filters, which means stream all entities
|
||||
return Object.keys(request).length > 0 ? request : undefined;
|
||||
};
|
||||
+5
-6
@@ -27,7 +27,7 @@ import type {
|
||||
CatalogApi,
|
||||
StreamEntitiesRequest,
|
||||
} from '@backstage/catalog-client';
|
||||
import { filtersToStreamRequest } from './filtersToStreamRequest';
|
||||
import { toStreamRequest } from './toStreamRequest';
|
||||
|
||||
/**
|
||||
* A custom exporter function that returns an async generator for streaming exports.
|
||||
@@ -145,7 +145,7 @@ export const useStreamingExport = (): {
|
||||
streamRequest,
|
||||
onSuccess,
|
||||
onError,
|
||||
exporter: exporterFn,
|
||||
exporter,
|
||||
}: StreamingExportOptions) => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
@@ -153,11 +153,10 @@ export const useStreamingExport = (): {
|
||||
try {
|
||||
// If caller didn't provide a streamRequest, derive it from the
|
||||
// current EntityList filters so exports reflect the user's view.
|
||||
const resolvedStreamRequest =
|
||||
streamRequest ?? filtersToStreamRequest(filters);
|
||||
const resolvedStreamRequest = streamRequest ?? toStreamRequest(filters);
|
||||
|
||||
if (exporterFn) {
|
||||
const { generator, contentType } = exporterFn({
|
||||
if (exporter) {
|
||||
const { generator, contentType } = exporter({
|
||||
apis,
|
||||
columns,
|
||||
streamRequest: resolvedStreamRequest,
|
||||
|
||||
Reference in New Issue
Block a user