split apart into two filter refs

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2023-11-23 16:36:16 +01:00
parent 423e33d81f
commit 969d028b93
4 changed files with 121 additions and 46 deletions
+14 -4
View File
@@ -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<ExtensionInputValues<TInputs>>;
}) => Promise<JSX.Element>;
@@ -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<ExtensionInputValues<TInputs>>;
}) => Promise<JSX.Element>;
@@ -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,
{}
>;
+46 -9
View File
@@ -37,9 +37,13 @@ export const entityContentTitleExtensionDataRef =
createExtensionDataRef<string>('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<string>('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<ExtensionInputValues<TInputs>>;
}) => Promise<JSX.Element>;
@@ -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<
<ExtensionComponent />
</ExtensionBoundary>
),
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<ExtensionInputValues<TInputs>>;
}) => Promise<JSX.Element>;
@@ -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<
<ExtensionComponent />
</ExtensionBoundary>
),
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 {};
}
@@ -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<string>();
// 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<string>();
const seenDuplicateExpressionStrings = new Set<string>();
// 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) ? (
<Grid item md={6} xs={12}>
@@ -70,12 +101,7 @@ export function EntityOverviewPage(props: EntityOverviewPageProps) {
return (
<Grid container spacing={3} alignItems="stretch">
{props.cards.map((card, index) => (
<CardWrapper
key={index}
entity={entity}
element={card.element}
filter={card.filter}
/>
<CardWrapper key={index} entity={entity} {...card} />
))}
</Grid>
);
+4 -2
View File
@@ -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 }) =>