split apart into two filter refs
Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
@@ -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,
|
||||
{}
|
||||
>;
|
||||
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -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 }) =>
|
||||
|
||||
Reference in New Issue
Block a user