diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx
index 81272de8b4..694e79bb8c 100644
--- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx
+++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx
@@ -20,6 +20,7 @@ import {
import { EntityContextMenuItemBlueprint } from './EntityContextMenuItemBlueprint';
import { screen, waitFor } from '@testing-library/react';
import { EntityProvider } from '@backstage/plugin-catalog-react';
+import { Entity } from '@backstage/catalog-model';
jest.mock('../../hooks/useEntityContextMenu', () => ({
useEntityContextMenu: () => ({
@@ -47,6 +48,25 @@ describe('EntityContextMenuItemBlueprint', () => {
},
];
+ const filterTestCases = [
+ {
+ params: { filter: 'kind:component' },
+ config: undefined,
+ },
+ {
+ params: { filter: (e: Entity) => e.kind.toLowerCase() === 'component' },
+ config: undefined,
+ },
+ {
+ params: {},
+ config: { filter: 'kind:component' },
+ },
+ {
+ params: {},
+ config: { filter: { kind: 'component' } },
+ },
+ ];
+
it.each(data)('should return an extension with sane defaults', params => {
const extension = EntityContextMenuItemBlueprint.make({
name: 'test',
@@ -61,7 +81,138 @@ describe('EntityContextMenuItemBlueprint', () => {
"id": "page:catalog/entity",
"input": "contextMenuItems",
},
- "configSchema": undefined,
+ "configSchema": {
+ "parse": [Function],
+ "schema": {
+ "$schema": "http://json-schema.org/draft-07/schema#",
+ "additionalProperties": false,
+ "properties": {
+ "filter": {
+ "anyOf": [
+ {
+ "type": "string",
+ },
+ {
+ "anyOf": [
+ {
+ "anyOf": [
+ {
+ "type": [
+ "string",
+ "number",
+ "boolean",
+ ],
+ },
+ {
+ "items": {
+ "$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0",
+ },
+ "type": "array",
+ },
+ ],
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "$all": {
+ "items": {
+ "$ref": "#/properties/filter/anyOf/1",
+ },
+ "type": "array",
+ },
+ },
+ "required": [
+ "$all",
+ ],
+ "type": "object",
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "$any": {
+ "items": {
+ "$ref": "#/properties/filter/anyOf/1",
+ },
+ "type": "array",
+ },
+ },
+ "required": [
+ "$any",
+ ],
+ "type": "object",
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "$not": {
+ "$ref": "#/properties/filter/anyOf/1",
+ },
+ },
+ "required": [
+ "$not",
+ ],
+ "type": "object",
+ },
+ {
+ "additionalProperties": {
+ "anyOf": [
+ {
+ "$ref": "#/properties/filter/anyOf/1/anyOf/0",
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "$exists": {
+ "type": "boolean",
+ },
+ },
+ "required": [
+ "$exists",
+ ],
+ "type": "object",
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "$in": {
+ "items": {
+ "$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0",
+ },
+ "type": "array",
+ },
+ },
+ "required": [
+ "$in",
+ ],
+ "type": "object",
+ },
+ {
+ "additionalProperties": false,
+ "properties": {
+ "$contains": {
+ "$ref": "#/properties/filter/anyOf/1",
+ },
+ },
+ "required": [
+ "$contains",
+ ],
+ "type": "object",
+ },
+ ],
+ },
+ "propertyNames": {
+ "pattern": "^(?!\\$).*$",
+ },
+ "type": "object",
+ },
+ ],
+ },
+ ],
+ },
+ },
+ "type": "object",
+ },
+ },
"disabled": false,
"factory": [Function],
"inputs": {},
@@ -77,65 +228,71 @@ describe('EntityContextMenuItemBlueprint', () => {
`);
});
- it('should exclude items based on the filter function', async () => {
- const extension = EntityContextMenuItemBlueprint.make({
- name: 'test',
- params: {
- icon: Icon,
- filter: e => e.kind.toLowerCase() === 'component',
- useProps: () => ({
- title: 'Test',
- onClick: () => {},
- }),
- },
- });
+ it.each(filterTestCases)(
+ 'should exclude items based on the filter',
+ async ({ params, config }) => {
+ const extension = EntityContextMenuItemBlueprint.make({
+ name: 'test',
+ params: {
+ icon: Icon,
+ ...params,
+ useProps: () => ({
+ title: 'Test',
+ onClick: () => {},
+ }),
+ },
+ });
- renderInTestApp(
-
- {createExtensionTester(extension).reactElement()}
- ,
- );
+ renderInTestApp(
+
+ {createExtensionTester(extension, { config }).reactElement()}
+ ,
+ );
- await waitFor(() => {
- expect(screen.queryByText('Test')).not.toBeInTheDocument();
- });
- });
+ await waitFor(() => {
+ expect(screen.queryByText('Test')).not.toBeInTheDocument();
+ });
+ },
+ );
- it('should include items based on the filter function', async () => {
- const extension = EntityContextMenuItemBlueprint.make({
- name: 'test',
- params: {
- icon: Icon,
- filter: e => e.kind.toLowerCase() === 'component',
- useProps: () => ({
- title: 'Test',
- onClick: () => {},
- }),
- },
- });
+ it.each(filterTestCases)(
+ 'should include items based on the filter',
+ async ({ params, config }) => {
+ const extension = EntityContextMenuItemBlueprint.make({
+ name: 'test',
+ params: {
+ icon: Icon,
+ ...params,
+ useProps: () => ({
+ title: 'Test',
+ onClick: () => {},
+ }),
+ },
+ });
- renderInTestApp(
-
- {createExtensionTester(extension).reactElement()}
- ,
- );
+ renderInTestApp(
+
+ {createExtensionTester(extension, { config }).reactElement()}
+ ,
+ );
- await waitFor(() => {
- expect(screen.getByText('Test')).toBeInTheDocument();
- });
- });
+ await waitFor(() => {
+ expect(screen.getByText('Test')).toBeInTheDocument();
+ });
+ },
+ );
it('should render a menu item', async () => {
const extension = EntityContextMenuItemBlueprint.make({
diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx
index d2ce2b3cce..acc3ddbc48 100644
--- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx
+++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx
@@ -24,9 +24,16 @@ import MenuItem from '@material-ui/core/MenuItem';
import ListItemIcon from '@material-ui/core/ListItemIcon';
import ListItemText from '@material-ui/core/ListItemText';
import { useEntityContextMenu } from '../../hooks/useEntityContextMenu';
+import { EntityPredicate } from '../predicates';
import type { Entity } from '@backstage/catalog-model';
import { useEntity } from '../../hooks/useEntity';
-
+import {
+ entityFilterExpressionDataRef,
+ entityFilterFunctionDataRef,
+} from './extensionData';
+import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema';
+import { resolveEntityFilterData } from './resolveEntityFilterData';
+import { buildFilterFn } from '../filter/FilterWrapper';
/** @alpha */
export type UseProps = () =>
| {
@@ -44,7 +51,7 @@ export type UseProps = () =>
export type EntityContextMenuItemParams = {
useProps: UseProps;
icon: JSX.Element;
- filter?: (entity: Entity) => boolean;
+ filter?: string | EntityPredicate | ((entity: Entity) => boolean);
};
/** @alpha */
@@ -52,7 +59,33 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({
kind: 'entity-context-menu-item',
attachTo: { id: 'page:catalog/entity', input: 'contextMenuItems' },
output: [coreExtensionData.reactElement],
- *factory(params: EntityContextMenuItemParams, { node }) {
+ config: {
+ schema: {
+ filter: z =>
+ z.union([z.string(), createEntityPredicateSchema(z)]).optional(),
+ },
+ },
+ *factory(params: EntityContextMenuItemParams, { node, config }) {
+ const resolvedFilterData = [];
+
+ for (const resolved of resolveEntityFilterData(
+ params.filter,
+ config,
+ node,
+ )) {
+ resolvedFilterData.push(resolved);
+ }
+
+ const resolvedFilter = resolvedFilterData.pop();
+ const filter = buildFilterFn(
+ resolvedFilter?.id === entityFilterFunctionDataRef.id
+ ? resolvedFilter.value
+ : undefined,
+ resolvedFilter?.id === entityFilterExpressionDataRef.id
+ ? resolvedFilter.value
+ : undefined,
+ );
+
const loader = async () => {
const Component = () => {
const { onMenuClose } = useEntityContextMenu();
@@ -71,7 +104,7 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({
};
}
- if (entity && params.filter && !params.filter(entity)) {
+ if (entity && !filter(entity)) {
return null;
}