From 2a292db5b22b99079fafdf8ea7526d42067927fb Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 1 May 2025 11:42:10 -0400 Subject: [PATCH 01/16] add filter support for EntityContextMenuItemBlueprint Signed-off-by: Mark Dunphy --- .../alpha/blueprints/EntityContextMenuItemBlueprint.tsx | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index f43dd6a6d9..cf3a640e37 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -24,7 +24,8 @@ 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 type { Entity } from '@backstage/catalog-model'; +import { useEntity } from '../../hooks/useEntity'; /** @alpha */ export type UseProps = () => | { @@ -42,6 +43,7 @@ export type UseProps = () => export type EntityContextMenuItemParams = { useProps: UseProps; icon: JSX.Element; + filter?: (entity: Entity) => boolean; }; /** @alpha */ @@ -53,6 +55,7 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ const loader = async () => { const Component = () => { const { onMenuClose } = useEntityContextMenu(); + const { entity } = useEntity(); const { title, ...menuItemProps } = params.useProps(); let handleClick = undefined; @@ -67,6 +70,10 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ }; } + if (entity && params.filter && !params.filter(entity)) { + return null; + } + return ( {params.icon} From b04f874aba3c0111792635e7c66ce18e19dd610a Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 1 May 2025 12:10:09 -0400 Subject: [PATCH 02/16] add tests for filtering Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.test.tsx | 72 +++++++++++++++++++ .../EntityContextMenuItemBlueprint.tsx | 1 + 2 files changed, 73 insertions(+) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index 3bdfc00db7..b6e0f6d8ef 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -13,7 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { + createExtensionTester, + renderInTestApp, + TestApiProvider, +} from '@backstage/frontend-test-utils'; import { EntityContextMenuItemBlueprint } from './EntityContextMenuItemBlueprint'; +import { screen, waitFor } from '@testing-library/react'; +import { EntityProvider } from '@backstage/plugin-catalog-react'; + +jest.mock('../../hooks/useEntityContextMenu', () => ({ + useEntityContextMenu: () => ({ + onMenuClose: jest.fn(), + }), +})); describe('EntityContextMenuItemBlueprint', () => { const data = [ @@ -64,4 +77,63 @@ describe('EntityContextMenuItemBlueprint', () => { } `); }); + + it('should filter 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: () => {}, + }), + }, + }); + + renderInTestApp( + +
    {createExtensionTester(extension).reactElement()}
+
, + ); + + await waitFor(() => { + expect(screen.queryByText('Test')).not.toBeInTheDocument(); + }); + }); + + it('should render a menu item', async () => { + const extension = EntityContextMenuItemBlueprint.make({ + name: 'test', + params: { + icon: Icon, + useProps: () => ({ + title: 'Test', + onClick: () => {}, + }), + }, + }); + + renderInTestApp( + +
    {createExtensionTester(extension).reactElement()}
+
, + ); + + await waitFor(() => { + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + }); }); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index cf3a640e37..d2ce2b3cce 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -26,6 +26,7 @@ import ListItemText from '@material-ui/core/ListItemText'; import { useEntityContextMenu } from '../../hooks/useEntityContextMenu'; import type { Entity } from '@backstage/catalog-model'; import { useEntity } from '../../hooks/useEntity'; + /** @alpha */ export type UseProps = () => | { From b1edfe2d60070631e2c9e3539cb1ae62c7f48162 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 1 May 2025 13:33:58 -0400 Subject: [PATCH 03/16] add api reports Signed-off-by: Mark Dunphy --- plugins/catalog-react/report-alpha.api.md | 1 + .../src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index d5f85b7836..f69bbe476d 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -343,6 +343,7 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ export type EntityContextMenuItemParams = { useProps: UseProps; icon: JSX_2.Element; + filter?: (entity: Entity) => boolean; }; // @alpha (undocumented) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index b6e0f6d8ef..5a272c33ca 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -16,7 +16,6 @@ import { createExtensionTester, renderInTestApp, - TestApiProvider, } from '@backstage/frontend-test-utils'; import { EntityContextMenuItemBlueprint } from './EntityContextMenuItemBlueprint'; import { screen, waitFor } from '@testing-library/react'; From 2ddbc50dbc26dd10fa361bb76746d85b8d8b83a2 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 1 May 2025 13:36:21 -0400 Subject: [PATCH 04/16] add changeset Signed-off-by: Mark Dunphy --- .changeset/orange-women-fry.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/orange-women-fry.md diff --git a/.changeset/orange-women-fry.md b/.changeset/orange-women-fry.md new file mode 100644 index 0000000000..14474fd23e --- /dev/null +++ b/.changeset/orange-women-fry.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +A new `filter` param has been added to the EntityContextMenuItemBlueprint to make it easier to determine if a menu item should appear for a given entity. The `filter` param is a function which accepts an entity and returns a boolean. From 2768e8a8e36b62b2f4a6af4fe215dd38e8efd80c Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 1 May 2025 13:37:17 -0400 Subject: [PATCH 05/16] changeset update Signed-off-by: Mark Dunphy --- .changeset/orange-women-fry.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.changeset/orange-women-fry.md b/.changeset/orange-women-fry.md index 14474fd23e..681d75c875 100644 --- a/.changeset/orange-women-fry.md +++ b/.changeset/orange-women-fry.md @@ -2,4 +2,4 @@ '@backstage/plugin-catalog-react': patch --- -A new `filter` param has been added to the EntityContextMenuItemBlueprint to make it easier to determine if a menu item should appear for a given entity. The `filter` param is a function which accepts an entity and returns a boolean. +A new `filter` parameter has been added to `EntityContextMenuItemBlueprint` to make it easier to configure which entities a menu item should appear for. The `filter` parameter is a function which accepts an entity and returns a boolean. From af9f453c5f9b7456d45ca23f8234e8616581fcf4 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Thu, 1 May 2025 13:48:54 -0400 Subject: [PATCH 06/16] add extra test case Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.test.tsx | 32 ++++++++++++++++++- 1 file changed, 31 insertions(+), 1 deletion(-) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index 5a272c33ca..81272de8b4 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -77,7 +77,7 @@ describe('EntityContextMenuItemBlueprint', () => { `); }); - it('should filter items based on the filter function', async () => { + it('should exclude items based on the filter function', async () => { const extension = EntityContextMenuItemBlueprint.make({ name: 'test', params: { @@ -107,6 +107,36 @@ describe('EntityContextMenuItemBlueprint', () => { }); }); + 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: () => {}, + }), + }, + }); + + renderInTestApp( + +
    {createExtensionTester(extension).reactElement()}
+
, + ); + + await waitFor(() => { + expect(screen.getByText('Test')).toBeInTheDocument(); + }); + }); + it('should render a menu item', async () => { const extension = EntityContextMenuItemBlueprint.make({ name: 'test', From af0ab1cb788287cf645a5874796e2b375ace6b9d Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Fri, 2 May 2025 10:32:24 -0400 Subject: [PATCH 07/16] relocate buildFilterFn and FilterWrapper to catalog-react from catalog Signed-off-by: Mark Dunphy --- .../src/alpha/filter/FilterWrapper.tsx | 10 +++++++--- .../src/alpha/filter/matchers/createHasMatcher.test.ts | 0 .../src/alpha/filter/matchers/createHasMatcher.ts | 0 .../src/alpha/filter/matchers/createIsMatcher.test.ts | 0 .../src/alpha/filter/matchers/createIsMatcher.ts | 0 .../alpha/filter/matchers/createKindMatcher.test.ts | 0 .../src/alpha/filter/matchers/createKindMatcher.ts | 0 .../alpha/filter/matchers/createTypeMatcher.test.ts | 0 .../src/alpha/filter/matchers/createTypeMatcher.ts | 0 .../src/alpha/filter/matchers/types.ts | 0 .../src/alpha/filter/parseFilterExpression.test.ts | 0 .../src/alpha/filter/parseFilterExpression.ts | 0 plugins/catalog-react/src/alpha/index.ts | 1 + plugins/catalog/src/alpha/entityContents.tsx | 2 +- plugins/catalog/src/alpha/pages.tsx | 2 +- 15 files changed, 10 insertions(+), 5 deletions(-) rename plugins/{catalog => catalog-react}/src/alpha/filter/FilterWrapper.tsx (94%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createHasMatcher.test.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createHasMatcher.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createIsMatcher.test.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createIsMatcher.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createKindMatcher.test.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createKindMatcher.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createTypeMatcher.test.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/createTypeMatcher.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/matchers/types.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/parseFilterExpression.test.ts (100%) rename plugins/{catalog => catalog-react}/src/alpha/filter/parseFilterExpression.ts (100%) diff --git a/plugins/catalog/src/alpha/filter/FilterWrapper.tsx b/plugins/catalog-react/src/alpha/filter/FilterWrapper.tsx similarity index 94% rename from plugins/catalog/src/alpha/filter/FilterWrapper.tsx rename to plugins/catalog-react/src/alpha/filter/FilterWrapper.tsx index 33af9c2c3f..76ee69b087 100644 --- a/plugins/catalog/src/alpha/filter/FilterWrapper.tsx +++ b/plugins/catalog-react/src/alpha/filter/FilterWrapper.tsx @@ -24,9 +24,13 @@ import { parseFilterExpression } from './parseFilterExpression'; 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. +/** + * 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. + * + * @alpha + */ export function buildFilterFn( filterFunction?: (entity: Entity) => boolean, filterExpression?: string, diff --git a/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.test.ts b/plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createHasMatcher.test.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts b/plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.test.ts b/plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createIsMatcher.test.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts b/plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.test.ts b/plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createKindMatcher.test.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts b/plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.test.ts b/plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.test.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.test.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts b/plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts rename to plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.ts diff --git a/plugins/catalog/src/alpha/filter/matchers/types.ts b/plugins/catalog-react/src/alpha/filter/matchers/types.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/matchers/types.ts rename to plugins/catalog-react/src/alpha/filter/matchers/types.ts diff --git a/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts b/plugins/catalog-react/src/alpha/filter/parseFilterExpression.test.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts rename to plugins/catalog-react/src/alpha/filter/parseFilterExpression.test.ts diff --git a/plugins/catalog/src/alpha/filter/parseFilterExpression.ts b/plugins/catalog-react/src/alpha/filter/parseFilterExpression.ts similarity index 100% rename from plugins/catalog/src/alpha/filter/parseFilterExpression.ts rename to plugins/catalog-react/src/alpha/filter/parseFilterExpression.ts diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index 4ff4dbf0dd..f1b8b05c62 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -20,3 +20,4 @@ export * from './predicates'; export { catalogReactTranslationRef } from '../translation'; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; +export { buildFilterFn } from './filter/FilterWrapper'; diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx index 165ed33e04..224557519e 100644 --- a/plugins/catalog/src/alpha/entityContents.tsx +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -26,7 +26,7 @@ import { EntityContentLayoutBlueprint, EntityContentLayoutProps, } from '@backstage/plugin-catalog-react/alpha'; -import { buildFilterFn } from './filter/FilterWrapper'; +import { buildFilterFn } from '@backstage/plugin-catalog-react/alpha'; import { useEntity } from '@backstage/plugin-catalog-react'; export const catalogOverviewEntityContent = diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index b39fe510e6..f822cb0fed 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -31,10 +31,10 @@ import { EntityHeaderBlueprint, EntityContentBlueprint, defaultEntityContentGroups, + buildFilterFn, } from '@backstage/plugin-catalog-react/alpha'; import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; -import { buildFilterFn } from './filter/FilterWrapper'; import { EntityHeader } from './components/EntityHeader'; export const catalogPage = PageBlueprint.makeWithOverrides({ From f001924d8487cbeb0186f396f5e19faa731b73e4 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Fri, 2 May 2025 12:11:12 -0400 Subject: [PATCH 08/16] filter menu items by entity predicate, filter string, or function Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.test.tsx | 267 ++++++++++++++---- .../EntityContextMenuItemBlueprint.tsx | 41 ++- 2 files changed, 249 insertions(+), 59 deletions(-) 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; } From 5fe55c90d3c724c9f6edd27abed3194fe76877a7 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Fri, 2 May 2025 12:27:35 -0400 Subject: [PATCH 09/16] api reports + changeset Signed-off-by: Mark Dunphy --- .changeset/orange-women-fry.md | 1 + plugins/catalog-react/report-alpha.api.md | 16 ++++++++++++--- plugins/catalog/report-alpha.api.md | 24 +++++++++++++++++------ 3 files changed, 32 insertions(+), 9 deletions(-) diff --git a/.changeset/orange-women-fry.md b/.changeset/orange-women-fry.md index 681d75c875..493a1aed57 100644 --- a/.changeset/orange-women-fry.md +++ b/.changeset/orange-women-fry.md @@ -1,5 +1,6 @@ --- '@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch --- A new `filter` parameter has been added to `EntityContextMenuItemBlueprint` to make it easier to configure which entities a menu item should appear for. The `filter` parameter is a function which accepts an entity and returns a boolean. diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index f69bbe476d..595741ab67 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -16,6 +16,12 @@ import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; +// @alpha +export function buildFilterFn( + filterFunction?: (entity: Entity) => boolean, + filterExpression?: string, +): (entity: Entity) => boolean; + // @alpha export const CatalogFilterBlueprint: ExtensionBlueprint<{ kind: 'catalog-filter'; @@ -334,8 +340,12 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ params: EntityContextMenuItemParams; output: ConfigurableExtensionDataRef; inputs: {}; - config: {}; - configInput: {}; + config: { + filter: EntityPredicate | undefined; + }; + configInput: { + filter?: EntityPredicate | undefined; + }; dataRefs: never; }>; @@ -343,7 +353,7 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ export type EntityContextMenuItemParams = { useProps: UseProps; icon: JSX_2.Element; - filter?: (entity: Entity) => boolean; + filter?: string | EntityPredicate | ((entity: Entity) => boolean); }; // @alpha (undocumented) diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 54d4187536..246c7929f0 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -863,8 +863,12 @@ const _default: FrontendPlugin< 'entity-context-menu-item:catalog/copy-entity-url': ExtensionDefinition<{ kind: 'entity-context-menu-item'; name: 'copy-entity-url'; - config: {}; - configInput: {}; + config: { + filter: EntityPredicate | undefined; + }; + configInput: { + filter?: EntityPredicate | undefined; + }; output: ConfigurableExtensionDataRef< JSX_2.Element, 'core.reactElement', @@ -876,8 +880,12 @@ const _default: FrontendPlugin< 'entity-context-menu-item:catalog/inspect-entity': ExtensionDefinition<{ kind: 'entity-context-menu-item'; name: 'inspect-entity'; - config: {}; - configInput: {}; + config: { + filter: EntityPredicate | undefined; + }; + configInput: { + filter?: EntityPredicate | undefined; + }; output: ConfigurableExtensionDataRef< JSX_2.Element, 'core.reactElement', @@ -889,8 +897,12 @@ const _default: FrontendPlugin< 'entity-context-menu-item:catalog/unregister-entity': ExtensionDefinition<{ kind: 'entity-context-menu-item'; name: 'unregister-entity'; - config: {}; - configInput: {}; + config: { + filter: EntityPredicate | undefined; + }; + configInput: { + filter?: EntityPredicate | undefined; + }; output: ConfigurableExtensionDataRef< JSX_2.Element, 'core.reactElement', From 590bd5fefe971c510861c26c6bbd5c9cdbaeff88 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Fri, 9 May 2025 12:04:35 -0400 Subject: [PATCH 10/16] handle context menu item filtering in the entity page Signed-off-by: Mark Dunphy --- plugins/catalog-react/report-alpha.api.md | 30 ++++++- .../EntityContextMenuItemBlueprint.test.tsx | 18 ++++ .../EntityContextMenuItemBlueprint.tsx | 39 +++------ plugins/catalog/report-alpha.api.md | 79 +++++++++++++---- plugins/catalog/src/alpha/pages.test.tsx | 87 +++++++++++++++++++ plugins/catalog/src/alpha/pages.tsx | 33 +++++-- 6 files changed, 232 insertions(+), 54 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 595741ab67..bb296e7c28 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -338,7 +338,22 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ kind: 'entity-context-menu-item'; name: undefined; params: EntityContextMenuItemParams; - output: ConfigurableExtensionDataRef; + output: + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >; inputs: {}; config: { filter: EntityPredicate | undefined; @@ -346,7 +361,18 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ configInput: { filter?: EntityPredicate | undefined; }; - dataRefs: never; + dataRefs: { + filterFunction: ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + {} + >; + filterExpression: ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + {} + >; + }; }>; // @alpha (undocumented) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index 694e79bb8c..2491b714b1 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -220,6 +220,24 @@ describe('EntityContextMenuItemBlueprint', () => { "name": "test", "output": [ [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-function", + "optional": [Function], + "toString": [Function], + }, + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "catalog.entity-filter-expression", + "optional": [Function], + "toString": [Function], + }, ], "override": [Function], "toString": [Function], diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index acc3ddbc48..59cd0b48e4 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -26,14 +26,12 @@ 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 = () => | { @@ -58,7 +56,15 @@ export type EntityContextMenuItemParams = { export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ kind: 'entity-context-menu-item', attachTo: { id: 'page:catalog/entity', input: 'contextMenuItems' }, - output: [coreExtensionData.reactElement], + output: [ + coreExtensionData.reactElement, + entityFilterFunctionDataRef.optional(), + entityFilterExpressionDataRef.optional(), + ], + dataRefs: { + filterFunction: entityFilterFunctionDataRef, + filterExpression: entityFilterExpressionDataRef, + }, config: { schema: { filter: z => @@ -66,30 +72,9 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ }, }, *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(); - const { entity } = useEntity(); const { title, ...menuItemProps } = params.useProps(); let handleClick = undefined; @@ -104,10 +89,6 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ }; } - if (entity && !filter(entity)) { - return null; - } - return ( {params.icon} @@ -120,5 +101,7 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ }; yield coreExtensionData.reactElement(ExtensionBoundary.lazy(node, loader)); + + yield* resolveEntityFilterData(params.filter, config, node); }, }); diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 246c7929f0..93a4e94225 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -869,11 +869,22 @@ const _default: FrontendPlugin< configInput: { filter?: EntityPredicate | undefined; }; - output: ConfigurableExtensionDataRef< - JSX_2.Element, - 'core.reactElement', - {} - >; + output: + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >; inputs: {}; params: EntityContextMenuItemParams; }>; @@ -886,11 +897,22 @@ const _default: FrontendPlugin< configInput: { filter?: EntityPredicate | undefined; }; - output: ConfigurableExtensionDataRef< - JSX_2.Element, - 'core.reactElement', - {} - >; + output: + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >; inputs: {}; params: EntityContextMenuItemParams; }>; @@ -903,11 +925,22 @@ const _default: FrontendPlugin< configInput: { filter?: EntityPredicate | undefined; }; - output: ConfigurableExtensionDataRef< - JSX_2.Element, - 'core.reactElement', - {} - >; + output: + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >; inputs: {}; params: EntityContextMenuItemParams; }>; @@ -1057,7 +1090,21 @@ const _default: FrontendPlugin< } >; contextMenuItems: ExtensionInput< - ConfigurableExtensionDataRef, + | ConfigurableExtensionDataRef + | ConfigurableExtensionDataRef< + (entity: Entity) => boolean, + 'catalog.entity-filter-function', + { + optional: true; + } + > + | ConfigurableExtensionDataRef< + string, + 'catalog.entity-filter-expression', + { + optional: true; + } + >, { singleton: false; optional: false; diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index f5aeb7d196..6e5d3fab95 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -36,6 +36,7 @@ import { } from '@backstage/plugin-catalog-react'; import { convertLegacyRouteRef } from '@backstage/core-compat-api'; import { rootRouteRef } from '../routes'; +import { Entity } from '@backstage/catalog-model'; describe('Entity page', () => { const entityMock = { @@ -733,5 +734,91 @@ describe('Entity page', () => { expect(onClickMock).toHaveBeenCalledTimes(disabled ? 0 : 1); }); }); + + it.each([ + { + positive: { params: {} }, + negative: { params: { filter: 'kind:api' } }, + }, + { + positive: { params: { filter: 'kind:component' } }, + negative: { params: { filter: 'kind:api' } }, + }, + { + positive: { + params: { + filter: (e: Entity) => e.kind.toLowerCase() === 'component', + }, + }, + negative: { + params: { filter: (e: Entity) => e.kind.toLowerCase() === 'api' }, + }, + }, + ])( + 'should render menu items according to filters', + async ({ positive, negative }) => { + const menuItem = EntityContextMenuItemBlueprint.make({ + name: 'should-render-menu-item', + params: { + icon: Test Icon, + useProps: () => ({ + onClick: onClickMock, + title: 'Should Render', + }), + ...positive.params, + }, + }); + + const filteredMenuItem = EntityContextMenuItemBlueprint.make({ + name: 'should-not-render-menu-item', + params: { + icon: Test Icon, + useProps: () => ({ + onClick: onClickMock, + title: 'Should Not Render', + }), + ...negative.params, + }, + }); + + const tester = createExtensionTester( + Object.assign({ namespace: 'catalog' }, catalogEntityPage), + ) + .add(menuItem) + .add(filteredMenuItem); + + renderInTestApp( + + {tester.reactElement()} + , + { + config: { + app: { + title: 'Custom app', + }, + backend: { baseUrl: 'http://localhost:7000' }, + }, + mountedRoutes: { + '/catalog': convertLegacyRouteRef(rootRouteRef), + '/catalog/:namespace/:kind/:name': + convertLegacyRouteRef(entityRouteRef), + }, + }, + ); + + await waitFor(async () => { + await userEvent.click(screen.getByTestId('menu-button')); + expect(screen.getByText('Should Render')).toBeInTheDocument(); + expect( + screen.queryByText('Should Not Render'), + ).not.toBeInTheDocument(); + }); + }, + ); }); }); diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index f822cb0fed..3708ae11fa 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -32,10 +32,10 @@ import { EntityContentBlueprint, defaultEntityContentGroups, buildFilterFn, + EntityContextMenuItemBlueprint, } from '@backstage/plugin-catalog-react/alpha'; import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; -import { EntityHeader } from './components/EntityHeader'; export const catalogPage = PageBlueprint.makeWithOverrides({ inputs: { @@ -72,7 +72,11 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ EntityContentBlueprint.dataRefs.filterExpression.optional(), EntityContentBlueprint.dataRefs.group.optional(), ]), - contextMenuItems: createExtensionInput([coreExtensionData.reactElement]), + contextMenuItems: createExtensionInput([ + coreExtensionData.reactElement, + EntityContextMenuItemBlueprint.dataRefs.filterFunction.optional(), + EntityContextMenuItemBlueprint.dataRefs.filterExpression.optional(), + ]), }, config: { schema: { @@ -89,9 +93,13 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ loader: async () => { const { EntityLayout } = await import('./components/EntityLayout'); - const menuItems = inputs.contextMenuItems.map(item => - item.get(coreExtensionData.reactElement), - ); + const menuItems = inputs.contextMenuItems.map(item => ({ + element: item.get(coreExtensionData.reactElement), + filter: buildFilterFn( + item.get(EntityContextMenuItemBlueprint.dataRefs.filterFunction), + item.get(EntityContextMenuItemBlueprint.dataRefs.filterExpression), + ), + })); type Groups = Record< string, @@ -100,7 +108,7 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ const header = inputs.header?.get( EntityHeaderBlueprint.dataRefs.element, - ) ?? ; + ); let groups = Object.entries(defaultEntityContentGroups).reduce( (rest, group) => { @@ -137,9 +145,18 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ } const Component = () => { + const entityFromUrl = useEntityFromUrl(); + const { entity } = entityFromUrl; + const filteredMenuItems = entity + ? menuItems.filter(i => i.filter(entity)).map(i => i.element) + : []; + return ( - - + + {Object.values(groups).flatMap(({ title, items }) => items.map(output => ( Date: Wed, 14 May 2025 13:35:58 -0400 Subject: [PATCH 11/16] remove support for string filtering Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.test.tsx | 218 ++++++++---------- .../EntityContextMenuItemBlueprint.tsx | 30 ++- plugins/catalog/src/alpha/pages.test.tsx | 6 +- plugins/catalog/src/alpha/pages.tsx | 8 +- 4 files changed, 125 insertions(+), 137 deletions(-) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index 2491b714b1..6c0efb84dd 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -89,124 +89,117 @@ describe('EntityContextMenuItemBlueprint', () => { "properties": { "filter": { "anyOf": [ - { - "type": "string", - }, { "anyOf": [ { - "anyOf": [ - { - "type": [ - "string", - "number", - "boolean", - ], - }, - { - "items": { - "$ref": "#/properties/filter/anyOf/1/anyOf/0/anyOf/0", - }, - "type": "array", - }, + "type": [ + "string", + "number", + "boolean", ], }, { - "additionalProperties": false, - "properties": { - "$all": { - "items": { - "$ref": "#/properties/filter/anyOf/1", - }, - "type": "array", - }, + "items": { + "$ref": "#/properties/filter/anyOf/0/anyOf/0", }, - "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": "array", }, ], }, + { + "additionalProperties": false, + "properties": { + "$all": { + "items": { + "$ref": "#/properties/filter", + }, + "type": "array", + }, + }, + "required": [ + "$all", + ], + "type": "object", + }, + { + "additionalProperties": false, + "properties": { + "$any": { + "items": { + "$ref": "#/properties/filter", + }, + "type": "array", + }, + }, + "required": [ + "$any", + ], + "type": "object", + }, + { + "additionalProperties": false, + "properties": { + "$not": { + "$ref": "#/properties/filter", + }, + }, + "required": [ + "$not", + ], + "type": "object", + }, + { + "additionalProperties": { + "anyOf": [ + { + "$ref": "#/properties/filter/anyOf/0", + }, + { + "additionalProperties": false, + "properties": { + "$exists": { + "type": "boolean", + }, + }, + "required": [ + "$exists", + ], + "type": "object", + }, + { + "additionalProperties": false, + "properties": { + "$in": { + "items": { + "$ref": "#/properties/filter/anyOf/0/anyOf/0", + }, + "type": "array", + }, + }, + "required": [ + "$in", + ], + "type": "object", + }, + { + "additionalProperties": false, + "properties": { + "$contains": { + "$ref": "#/properties/filter", + }, + }, + "required": [ + "$contains", + ], + "type": "object", + }, + ], + }, + "propertyNames": { + "pattern": "^(?!\\$).*$", + }, + "type": "object", + }, ], }, }, @@ -229,15 +222,6 @@ describe('EntityContextMenuItemBlueprint', () => { "optional": [Function], "toString": [Function], }, - { - "$$type": "@backstage/ExtensionDataRef", - "config": { - "optional": true, - }, - "id": "catalog.entity-filter-expression", - "optional": [Function], - "toString": [Function], - }, ], "override": [Function], "toString": [Function], diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx index 59cd0b48e4..470aa13646 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.tsx @@ -24,14 +24,13 @@ 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 { - entityFilterExpressionDataRef, - entityFilterFunctionDataRef, -} from './extensionData'; + EntityPredicate, + entityPredicateToFilterFunction, +} from '../predicates'; +import type { Entity } from '@backstage/catalog-model'; +import { entityFilterFunctionDataRef } from './extensionData'; import { createEntityPredicateSchema } from '../predicates/createEntityPredicateSchema'; -import { resolveEntityFilterData } from './resolveEntityFilterData'; /** @alpha */ export type UseProps = () => | { @@ -49,7 +48,7 @@ export type UseProps = () => export type EntityContextMenuItemParams = { useProps: UseProps; icon: JSX.Element; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: EntityPredicate | ((entity: Entity) => boolean); }; /** @alpha */ @@ -59,16 +58,13 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ output: [ coreExtensionData.reactElement, entityFilterFunctionDataRef.optional(), - entityFilterExpressionDataRef.optional(), ], dataRefs: { filterFunction: entityFilterFunctionDataRef, - filterExpression: entityFilterExpressionDataRef, }, config: { schema: { - filter: z => - z.union([z.string(), createEntityPredicateSchema(z)]).optional(), + filter: z => createEntityPredicateSchema(z).optional(), }, }, *factory(params: EntityContextMenuItemParams, { node, config }) { @@ -102,6 +98,16 @@ export const EntityContextMenuItemBlueprint = createExtensionBlueprint({ yield coreExtensionData.reactElement(ExtensionBoundary.lazy(node, loader)); - yield* resolveEntityFilterData(params.filter, config, node); + if (config.filter) { + yield entityFilterFunctionDataRef( + entityPredicateToFilterFunction(config.filter), + ); + } else if (typeof params.filter === 'function') { + yield entityFilterFunctionDataRef(params.filter); + } else if (params.filter) { + yield entityFilterFunctionDataRef( + entityPredicateToFilterFunction(params.filter), + ); + } }, }); diff --git a/plugins/catalog/src/alpha/pages.test.tsx b/plugins/catalog/src/alpha/pages.test.tsx index 6e5d3fab95..47b3efbd0a 100644 --- a/plugins/catalog/src/alpha/pages.test.tsx +++ b/plugins/catalog/src/alpha/pages.test.tsx @@ -738,11 +738,11 @@ describe('Entity page', () => { it.each([ { positive: { params: {} }, - negative: { params: { filter: 'kind:api' } }, + negative: { params: { filter: { kind: 'api' } } }, }, { - positive: { params: { filter: 'kind:component' } }, - negative: { params: { filter: 'kind:api' } }, + positive: { params: { filter: { kind: 'component' } } }, + negative: { params: { filter: { kind: 'api' } } }, }, { positive: { diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 3708ae11fa..8eef497845 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -75,7 +75,6 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ contextMenuItems: createExtensionInput([ coreExtensionData.reactElement, EntityContextMenuItemBlueprint.dataRefs.filterFunction.optional(), - EntityContextMenuItemBlueprint.dataRefs.filterExpression.optional(), ]), }, config: { @@ -95,10 +94,9 @@ export const catalogEntityPage = PageBlueprint.makeWithOverrides({ const menuItems = inputs.contextMenuItems.map(item => ({ element: item.get(coreExtensionData.reactElement), - filter: buildFilterFn( - item.get(EntityContextMenuItemBlueprint.dataRefs.filterFunction), - item.get(EntityContextMenuItemBlueprint.dataRefs.filterExpression), - ), + filter: + item.get(EntityContextMenuItemBlueprint.dataRefs.filterFunction) ?? + (() => true), })); type Groups = Record< From bd182548180db7de83e32fa92c3c9e747e39f837 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Wed, 14 May 2025 13:39:01 -0400 Subject: [PATCH 12/16] revert buildFilterFn relocation af0ab1cb788287cf645a5874796e2b375ace6b9d Signed-off-by: Mark Dunphy --- plugins/catalog-react/src/alpha/index.ts | 1 - plugins/catalog/src/alpha/entityContents.tsx | 2 +- .../src/alpha/filter/FilterWrapper.tsx | 10 +++------- .../src/alpha/filter/matchers/createHasMatcher.test.ts | 0 .../src/alpha/filter/matchers/createHasMatcher.ts | 0 .../src/alpha/filter/matchers/createIsMatcher.test.ts | 0 .../src/alpha/filter/matchers/createIsMatcher.ts | 0 .../alpha/filter/matchers/createKindMatcher.test.ts | 0 .../src/alpha/filter/matchers/createKindMatcher.ts | 0 .../alpha/filter/matchers/createTypeMatcher.test.ts | 0 .../src/alpha/filter/matchers/createTypeMatcher.ts | 0 .../src/alpha/filter/matchers/types.ts | 0 .../src/alpha/filter/parseFilterExpression.test.ts | 0 .../src/alpha/filter/parseFilterExpression.ts | 0 plugins/catalog/src/alpha/pages.tsx | 2 +- 15 files changed, 5 insertions(+), 10 deletions(-) rename plugins/{catalog-react => catalog}/src/alpha/filter/FilterWrapper.tsx (94%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createHasMatcher.test.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createHasMatcher.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createIsMatcher.test.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createIsMatcher.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createKindMatcher.test.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createKindMatcher.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createTypeMatcher.test.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/createTypeMatcher.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/matchers/types.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/parseFilterExpression.test.ts (100%) rename plugins/{catalog-react => catalog}/src/alpha/filter/parseFilterExpression.ts (100%) diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index f1b8b05c62..4ff4dbf0dd 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -20,4 +20,3 @@ export * from './predicates'; export { catalogReactTranslationRef } from '../translation'; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; -export { buildFilterFn } from './filter/FilterWrapper'; diff --git a/plugins/catalog/src/alpha/entityContents.tsx b/plugins/catalog/src/alpha/entityContents.tsx index 224557519e..165ed33e04 100644 --- a/plugins/catalog/src/alpha/entityContents.tsx +++ b/plugins/catalog/src/alpha/entityContents.tsx @@ -26,7 +26,7 @@ import { EntityContentLayoutBlueprint, EntityContentLayoutProps, } from '@backstage/plugin-catalog-react/alpha'; -import { buildFilterFn } from '@backstage/plugin-catalog-react/alpha'; +import { buildFilterFn } from './filter/FilterWrapper'; import { useEntity } from '@backstage/plugin-catalog-react'; export const catalogOverviewEntityContent = diff --git a/plugins/catalog-react/src/alpha/filter/FilterWrapper.tsx b/plugins/catalog/src/alpha/filter/FilterWrapper.tsx similarity index 94% rename from plugins/catalog-react/src/alpha/filter/FilterWrapper.tsx rename to plugins/catalog/src/alpha/filter/FilterWrapper.tsx index 76ee69b087..33af9c2c3f 100644 --- a/plugins/catalog-react/src/alpha/filter/FilterWrapper.tsx +++ b/plugins/catalog/src/alpha/filter/FilterWrapper.tsx @@ -24,13 +24,9 @@ import { parseFilterExpression } from './parseFilterExpression'; 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. - * - * @alpha - */ +// 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. export function buildFilterFn( filterFunction?: (entity: Entity) => boolean, filterExpression?: string, diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.test.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createHasMatcher.test.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createHasMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createHasMatcher.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.test.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createIsMatcher.test.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createIsMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createIsMatcher.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.test.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createKindMatcher.test.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createKindMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createKindMatcher.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.test.ts b/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.test.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.test.ts rename to plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.test.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.ts b/plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/createTypeMatcher.ts rename to plugins/catalog/src/alpha/filter/matchers/createTypeMatcher.ts diff --git a/plugins/catalog-react/src/alpha/filter/matchers/types.ts b/plugins/catalog/src/alpha/filter/matchers/types.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/matchers/types.ts rename to plugins/catalog/src/alpha/filter/matchers/types.ts diff --git a/plugins/catalog-react/src/alpha/filter/parseFilterExpression.test.ts b/plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/parseFilterExpression.test.ts rename to plugins/catalog/src/alpha/filter/parseFilterExpression.test.ts diff --git a/plugins/catalog-react/src/alpha/filter/parseFilterExpression.ts b/plugins/catalog/src/alpha/filter/parseFilterExpression.ts similarity index 100% rename from plugins/catalog-react/src/alpha/filter/parseFilterExpression.ts rename to plugins/catalog/src/alpha/filter/parseFilterExpression.ts diff --git a/plugins/catalog/src/alpha/pages.tsx b/plugins/catalog/src/alpha/pages.tsx index 8eef497845..c1f5b55341 100644 --- a/plugins/catalog/src/alpha/pages.tsx +++ b/plugins/catalog/src/alpha/pages.tsx @@ -31,11 +31,11 @@ import { EntityHeaderBlueprint, EntityContentBlueprint, defaultEntityContentGroups, - buildFilterFn, EntityContextMenuItemBlueprint, } from '@backstage/plugin-catalog-react/alpha'; import { rootRouteRef } from '../routes'; import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl'; +import { buildFilterFn } from './filter/FilterWrapper'; export const catalogPage = PageBlueprint.makeWithOverrides({ inputs: { From e875caf0591b4486247f8f31b6d4f4f8ce8ee9a0 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Wed, 14 May 2025 13:41:02 -0400 Subject: [PATCH 13/16] api reports Signed-off-by: Mark Dunphy --- plugins/catalog-react/report-alpha.api.md | 20 +--------------- plugins/catalog/report-alpha.api.md | 28 ----------------------- 2 files changed, 1 insertion(+), 47 deletions(-) diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index bb296e7c28..46d3030140 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -16,12 +16,6 @@ import { ResourcePermission } from '@backstage/plugin-permission-common'; import { RouteRef } from '@backstage/frontend-plugin-api'; import { TranslationRef } from '@backstage/core-plugin-api/alpha'; -// @alpha -export function buildFilterFn( - filterFunction?: (entity: Entity) => boolean, - filterExpression?: string, -): (entity: Entity) => boolean; - // @alpha export const CatalogFilterBlueprint: ExtensionBlueprint<{ kind: 'catalog-filter'; @@ -346,13 +340,6 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ { optional: true; } - > - | ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - { - optional: true; - } >; inputs: {}; config: { @@ -367,11 +354,6 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ 'catalog.entity-filter-function', {} >; - filterExpression: ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - {} - >; }; }>; @@ -379,7 +361,7 @@ export const EntityContextMenuItemBlueprint: ExtensionBlueprint<{ export type EntityContextMenuItemParams = { useProps: UseProps; icon: JSX_2.Element; - filter?: string | EntityPredicate | ((entity: Entity) => boolean); + filter?: EntityPredicate | ((entity: Entity) => boolean); }; // @alpha (undocumented) diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 93a4e94225..7ef20d8228 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -877,13 +877,6 @@ const _default: FrontendPlugin< { optional: true; } - > - | ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - { - optional: true; - } >; inputs: {}; params: EntityContextMenuItemParams; @@ -905,13 +898,6 @@ const _default: FrontendPlugin< { optional: true; } - > - | ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - { - optional: true; - } >; inputs: {}; params: EntityContextMenuItemParams; @@ -933,13 +919,6 @@ const _default: FrontendPlugin< { optional: true; } - > - | ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - { - optional: true; - } >; inputs: {}; params: EntityContextMenuItemParams; @@ -1097,13 +1076,6 @@ const _default: FrontendPlugin< { optional: true; } - > - | ConfigurableExtensionDataRef< - string, - 'catalog.entity-filter-expression', - { - optional: true; - } >, { singleton: false; From c454bd06ff666381de760d94580129c68b016561 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Wed, 14 May 2025 13:44:03 -0400 Subject: [PATCH 14/16] remove unnecessary test cases Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.test.tsx | 85 ------------------- 1 file changed, 85 deletions(-) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index 6c0efb84dd..e15eb8a89c 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -48,25 +48,6 @@ 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', @@ -230,72 +211,6 @@ describe('EntityContextMenuItemBlueprint', () => { `); }); - 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, { config }).reactElement()}
-
, - ); - - await waitFor(() => { - expect(screen.queryByText('Test')).not.toBeInTheDocument(); - }); - }, - ); - - 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, { config }).reactElement()}
-
, - ); - - await waitFor(() => { - expect(screen.getByText('Test')).toBeInTheDocument(); - }); - }, - ); - it('should render a menu item', async () => { const extension = EntityContextMenuItemBlueprint.make({ name: 'test', From 8be3cd3fd89e57b74a819847b020fd75ba944ec9 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Wed, 14 May 2025 15:07:05 -0400 Subject: [PATCH 15/16] remove unused imports Signed-off-by: Mark Dunphy --- .../src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index e15eb8a89c..e91cb70558 100644 --- a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx @@ -20,7 +20,6 @@ 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: () => ({ From 8112cb4b7ca3f5e6c3a20a0555c7596188b3fa26 Mon Sep 17 00:00:00 2001 From: Mark Dunphy Date: Wed, 14 May 2025 16:46:07 -0400 Subject: [PATCH 16/16] add test for filter function Signed-off-by: Mark Dunphy --- .../EntityContextMenuItemBlueprint.test.tsx | 25 +++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContextMenuItemBlueprint.test.tsx index e91cb70558..55598f5de1 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: () => ({ @@ -238,4 +239,28 @@ describe('EntityContextMenuItemBlueprint', () => { expect(screen.getByText('Test')).toBeInTheDocument(); }); }); + + it.each([ + { filter: { kind: 'Api' } }, + { filter: (e: Entity) => e.kind.toLowerCase() === 'api' }, + ])('should return a filter function', async ({ filter }) => { + const extension = EntityContextMenuItemBlueprint.make({ + name: 'test', + params: { + icon: Icon, + useProps: () => ({ title: 'Test', onClick: () => {} }), + filter, + }, + }); + + const tester = createExtensionTester(extension); + + const filterFn = tester.get( + EntityContextMenuItemBlueprint.dataRefs.filterFunction, + ); + + expect(filterFn).toBeDefined(); + expect(filterFn?.({ kind: 'Api' } as Entity)).toBe(true); + expect(filterFn?.({ kind: 'Component' } as Entity)).toBe(false); + }); });