From 969d028b93eb72dc70a707df8f945da0f18c9ac4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fredrik=20Adel=C3=B6w?= Date: Thu, 23 Nov 2023 16:36:16 +0100 Subject: [PATCH] split apart into two filter refs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Fredrik Adelöw --- plugins/catalog-react/api-report-alpha.md | 18 +++- plugins/catalog-react/src/alpha.tsx | 55 ++++++++++-- .../catalog/src/alpha/EntityOverviewPage.tsx | 88 ++++++++++++------- plugins/catalog/src/alpha/entityContents.tsx | 6 +- 4 files changed, 121 insertions(+), 46 deletions(-) diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index e284f80b8e..913055f8b0 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -24,7 +24,9 @@ export function createEntityCardExtension< }; disabled?: boolean; inputs?: TInputs; - filter?: typeof entityFilterExtensionDataRef.T; + filter?: + | typeof entityFilterFunctionExtensionDataRef.T + | typeof entityFilterExpressionExtensionDataRef.T; loader: (options: { inputs: Expand>; }) => Promise; @@ -46,7 +48,9 @@ export function createEntityContentExtension< routeRef?: RouteRef; defaultPath: string; defaultTitle: string; - filter?: typeof entityFilterExtensionDataRef.T; + filter?: + | typeof entityFilterFunctionExtensionDataRef.T + | typeof entityFilterExpressionExtensionDataRef.T; loader: (options: { inputs: Expand>; }) => Promise; @@ -63,8 +67,14 @@ export const entityContentTitleExtensionDataRef: ConfigurableExtensionDataRef< >; // @alpha (undocumented) -export const entityFilterExtensionDataRef: ConfigurableExtensionDataRef< - string | ((entity: Entity) => boolean), +export const entityFilterExpressionExtensionDataRef: ConfigurableExtensionDataRef< + string, + {} +>; + +// @alpha (undocumented) +export const entityFilterFunctionExtensionDataRef: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, {} >; diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha.tsx index 604c769fe5..43b7156b34 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha.tsx @@ -37,9 +37,13 @@ export const entityContentTitleExtensionDataRef = createExtensionDataRef('plugin.catalog.entity.content.title'); /** @alpha */ -export const entityFilterExtensionDataRef = createExtensionDataRef< - string | ((entity: Entity) => boolean) ->('plugin.catalog.entity.filter'); +export const entityFilterFunctionExtensionDataRef = createExtensionDataRef< + (entity: Entity) => boolean +>('plugin.catalog.entity.filter.fn'); + +/** @alpha */ +export const entityFilterExpressionExtensionDataRef = + createExtensionDataRef('plugin.catalog.entity.filter.expression'); // TODO: Figure out how to merge with provided config schema /** @alpha */ @@ -50,7 +54,9 @@ export function createEntityCardExtension< attachTo?: { id: string; input: string }; disabled?: boolean; inputs?: TInputs; - filter?: typeof entityFilterExtensionDataRef.T; + filter?: + | typeof entityFilterFunctionExtensionDataRef.T + | typeof entityFilterExpressionExtensionDataRef.T; loader: (options: { inputs: Expand>; }) => Promise; @@ -66,7 +72,8 @@ export function createEntityCardExtension< disabled: options.disabled ?? true, output: { element: coreExtensionData.reactElement, - filter: entityFilterExtensionDataRef.optional(), + filterFunction: entityFilterFunctionExtensionDataRef.optional(), + filterExpression: entityFilterExpressionExtensionDataRef.optional(), }, inputs: options.inputs, configSchema: createSchemaFromZod(z => @@ -87,7 +94,7 @@ export function createEntityCardExtension< ), - filter: config.filter ?? options.filter, + ...mergeFilters({ config, options }), }; }, }); @@ -104,7 +111,9 @@ export function createEntityContentExtension< routeRef?: RouteRef; defaultPath: string; defaultTitle: string; - filter?: typeof entityFilterExtensionDataRef.T; + filter?: + | typeof entityFilterFunctionExtensionDataRef.T + | typeof entityFilterExpressionExtensionDataRef.T; loader: (options: { inputs: Expand>; }) => Promise; @@ -123,7 +132,8 @@ export function createEntityContentExtension< path: coreExtensionData.routePath, routeRef: coreExtensionData.routeRef.optional(), title: entityContentTitleExtensionDataRef, - filter: entityFilterExtensionDataRef.optional(), + filterFunction: entityFilterFunctionExtensionDataRef.optional(), + filterExpression: entityFilterExpressionExtensionDataRef.optional(), }, inputs: options.inputs, configSchema: createSchemaFromZod(z => @@ -149,8 +159,35 @@ export function createEntityContentExtension< ), - filter: config.filter ?? options.filter, + ...mergeFilters({ config, options }), }; }, }); } + +/** + * Decides what filter outputs to produce, given some options and config + */ +function mergeFilters(inputs: { + options: { + filter?: + | typeof entityFilterFunctionExtensionDataRef.T + | typeof entityFilterExpressionExtensionDataRef.T; + }; + config: { + filter?: string; + }; +}): { + filterFunction?: typeof entityFilterFunctionExtensionDataRef.T; + filterExpression?: typeof entityFilterExpressionExtensionDataRef.T; +} { + const { options, config } = inputs; + if (config.filter) { + return { filterExpression: config.filter }; + } else if (typeof options.filter === 'string') { + return { filterExpression: options.filter }; + } else if (typeof options.filter === 'function') { + return { filterFunction: options.filter }; + } + return {}; +} diff --git a/plugins/catalog/src/alpha/EntityOverviewPage.tsx b/plugins/catalog/src/alpha/EntityOverviewPage.tsx index a71e789522..932e13d962 100644 --- a/plugins/catalog/src/alpha/EntityOverviewPage.tsx +++ b/plugins/catalog/src/alpha/EntityOverviewPage.tsx @@ -23,40 +23,71 @@ import { parseFilterExpression } from './filter/parseFilterExpression'; interface EntityOverviewPageProps { cards: Array<{ element: React.JSX.Element; - filter?: string | ((entity: Entity) => boolean); + filterFunction?: (entity: Entity) => boolean; + filterExpression?: string; }>; } -// Keeps track of what filter expression strings that we've emitted warnings for so far -const seenExpressionStrings = new Set(); +// Keeps track of what filter expression strings that we've seen duplicates of +// with functions, or which emitted parsing errors for so far +const seenParseErrorExpressionStrings = new Set(); +const seenDuplicateExpressionStrings = new Set(); +// Given an optional filter function and an optional filter expression, make +// sure that at most one of them was given, and return a filter function that +// does the right thing. +function buildFilterFn( + filterFunction?: (entity: Entity) => boolean, + filterExpression?: string, +): (entity: Entity) => boolean { + if ( + filterFunction && + filterExpression && + !seenDuplicateExpressionStrings.has(filterExpression) + ) { + // eslint-disable-next-line no-console + console.warn( + `Duplicate entity filter methods found, both '${filterExpression}' as well as a callback function, which is not permitted - using the callback`, + ); + seenDuplicateExpressionStrings.add(filterExpression); + } + + const filter = filterFunction || filterExpression; + if (!filter) { + return () => true; + } else if (typeof filter === 'function') { + return subject => filter(subject); + } + + const result = parseFilterExpression(filter); + if ( + result.expressionParseErrors.length && + !seenParseErrorExpressionStrings.has(filter) + ) { + // eslint-disable-next-line no-console + console.warn( + `Error(s) in entity filter expression '${filter}'`, + result.expressionParseErrors, + ); + seenParseErrorExpressionStrings.add(filter); + } + + return result.filterFn; +} + +// Handles the memoized parsing of filter expressions for each card function CardWrapper(props: { entity: Entity; element: React.JSX.Element; - filter?: string | ((entity: Entity) => boolean); + filterFunction?: (entity: Entity) => boolean; + filterExpression?: string; }) { - const { entity, element, filter } = props; + const { entity, element, filterFunction, filterExpression } = props; - const filterFn = useMemo<(subject: Entity) => boolean>(() => { - if (!filter) { - return () => true; - } else if (typeof filter === 'function') { - return subject => filter(subject); - } - const result = parseFilterExpression(filter); - if ( - result.expressionParseErrors.length && - !seenExpressionStrings.has(filter) - ) { - // eslint-disable-next-line no-console - console.warn( - `Error(s) in entity filter expression '${filter}'`, - result.expressionParseErrors, - ); - seenExpressionStrings.add(filter); - } - return result.filterFn; - }, [filter]); + const filterFn = useMemo( + () => buildFilterFn(filterFunction, filterExpression), + [filterFunction, filterExpression], + ); return filterFn(entity) ? ( @@ -70,12 +101,7 @@ export function EntityOverviewPage(props: EntityOverviewPageProps) { return ( {props.cards.map((card, index) => ( - + ))} ); diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx index 0bbee9bb7d..e280326b02 100644 --- a/plugins/catalog/src/alpha/entityContents.tsx +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -21,7 +21,8 @@ import { } from '@backstage/frontend-plugin-api'; import { createEntityContentExtension, - entityFilterExtensionDataRef, + entityFilterFunctionExtensionDataRef, + entityFilterExpressionExtensionDataRef, } from '@backstage/plugin-catalog-react/alpha'; export const OverviewEntityContent = createEntityContentExtension({ @@ -32,7 +33,8 @@ export const OverviewEntityContent = createEntityContentExtension({ inputs: { cards: createExtensionInput({ element: coreExtensionData.reactElement, - filter: entityFilterExtensionDataRef.optional(), + filterFunction: entityFilterFunctionExtensionDataRef.optional(), + filterExpression: entityFilterExpressionExtensionDataRef.optional(), }), }, loader: async ({ inputs }) =>