From 1612837c8ebe678f4b3825151c55794dca1d2a05 Mon Sep 17 00:00:00 2001 From: blam Date: Wed, 14 Aug 2024 13:50:46 +0200 Subject: [PATCH] chore: updating catalog react Signed-off-by: blam --- plugins/catalog-react/package.json | 5 +- .../blueprints/EntityCardBlueprint.test.tsx | 178 ++++++++++++++ .../alpha/blueprints/EntityCardBlueprint.tsx | 75 ++++++ .../EntityContentBlueprint.test.tsx | 226 ++++++++++++++++++ .../blueprints/EntityContentBlueprint.tsx | 99 ++++++++ .../src/alpha/blueprints/index.ts | 17 ++ .../src/{alpha.tsx => alpha/extensions.tsx} | 23 +- plugins/catalog-react/src/alpha/index.ts | 17 ++ plugins/catalog-react/src/setupTests.ts | 15 ++ 9 files changed, 646 insertions(+), 9 deletions(-) create mode 100644 plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx create mode 100644 plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx create mode 100644 plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx create mode 100644 plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx create mode 100644 plugins/catalog-react/src/alpha/blueprints/index.ts rename plugins/catalog-react/src/{alpha.tsx => alpha/extensions.tsx} (93%) create mode 100644 plugins/catalog-react/src/alpha/index.ts diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index e6b6acae3a..ef5d924200 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -29,7 +29,7 @@ "sideEffects": false, "exports": { ".": "./src/index.ts", - "./alpha": "./src/alpha.tsx", + "./alpha": "./src/alpha/index.ts", "./package.json": "./package.json" }, "main": "src/index.ts", @@ -37,7 +37,7 @@ "typesVersions": { "*": { "alpha": [ - "src/alpha.tsx" + "src/alpha/index.ts" ], "package.json": [ "package.json" @@ -85,6 +85,7 @@ "devDependencies": { "@backstage/cli": "workspace:^", "@backstage/core-app-api": "workspace:^", + "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-scaffolder-common": "workspace:^", "@backstage/test-utils": "workspace:^", diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx new file mode 100644 index 0000000000..db787766fd --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.test.tsx @@ -0,0 +1,178 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EntityCardBlueprint } from './EntityCardBlueprint'; +import { + coreExtensionData, + createExtension, + createExtensionInput, +} from '@backstage/frontend-plugin-api'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { waitFor, screen } from '@testing-library/react'; +import { Entity } from '@backstage/catalog-model'; + +describe('EntityCardBlueprint', () => { + it('should return an extension with sensible defaults', () => { + const extension = EntityCardBlueprint.make({ + name: 'test', + params: { + filter: 'has:labels', + loader: async () =>
im a card
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "entity-content:catalog/overview", + "input": "cards", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "entity-card", + "name": "test", + "namespace": undefined, + "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], + "version": "v2", + } + `); + }); + + it('should output the correct filter output', () => { + const mockFilter = (_entity: Entity) => true; + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + loader: async () =>
Test!
, + filter: 'test', + }, + }), + ).data(EntityCardBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + loader: async () =>
Test!
, + }, + }), + { config: { filter: 'test' } }, + ).data(EntityCardBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityCardBlueprint.make({ + name: 'test', + params: { + filter: mockFilter, + loader: async () =>
Test!
, + }, + }), + ).data(EntityCardBlueprint.dataRefs.filterFunction), + ).toBe(mockFilter); + }); + + it('should allow overriding config and inputs', async () => { + const extension = EntityCardBlueprint.makeWithOverrides({ + name: 'test', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + mock: z => z.string(), + }, + }, + factory(originalFactory, { inputs, config }) { + return originalFactory({ + loader: async () => ( +
+ config: {config.mock} +
+ {inputs.mock.map((i, k) => ( +
{i.get(coreExtensionData.reactElement)}
+ ))} +
+
+ ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'entity-card:test', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
im a mock
)]; + }, + }); + + createExtensionTester(extension, { config: { mock: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx new file mode 100644 index 0000000000..c3153b6059 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityCardBlueprint.tsx @@ -0,0 +1,75 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { lazy } from 'react'; +import { + ExtensionBoundary, + coreExtensionData, + createExtensionBlueprint, +} from '@backstage/frontend-plugin-api'; +import { catalogExtensionData } from '../extensions'; + +/** + * @alpha + * A blueprint for creating cards for the entity pages in the catalog. + */ +export const EntityCardBlueprint = createExtensionBlueprint({ + kind: 'entity-card', + attachTo: { id: 'entity-content:catalog/overview', input: 'cards' }, + output: [ + coreExtensionData.reactElement, + catalogExtensionData.entityFilterFunction.optional(), + catalogExtensionData.entityFilterExpression.optional(), + ], + dataRefs: { + filterFunction: catalogExtensionData.entityFilterFunction, + filterExpression: catalogExtensionData.entityFilterExpression, + }, + config: { + schema: { + filter: z => z.string().optional(), + }, + }, + *factory( + { + loader, + filter, + }: { + loader: () => Promise; + filter?: + | typeof catalogExtensionData.entityFilterFunction.T + | typeof catalogExtensionData.entityFilterExpression.T; + }, + { node, config }, + ) { + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.reactElement( + + + , + ); + + if (config.filter) { + yield catalogExtensionData.entityFilterExpression(config.filter); + } else if (typeof filter === 'string') { + yield catalogExtensionData.entityFilterExpression(filter); + } else if (typeof filter === 'function') { + yield catalogExtensionData.entityFilterFunction(filter); + } + }, +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx new file mode 100644 index 0000000000..dab6013bab --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.test.tsx @@ -0,0 +1,226 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { EntityContentBlueprint } from './EntityContentBlueprint'; +import { createExtensionTester } from '@backstage/frontend-test-utils'; +import { + coreExtensionData, + createExtension, + createExtensionInput, + createRouteRef, +} from '@backstage/frontend-plugin-api'; +import { Entity } from '@backstage/catalog-model'; +import { waitFor, screen } from '@testing-library/react'; + +describe('EntityContentBlueprint', () => { + it('should return an extension with sane defaults', () => { + const extension = EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
Test!
, + }, + }); + + expect(extension).toMatchInlineSnapshot(` + { + "$$type": "@backstage/ExtensionDefinition", + "attachTo": { + "id": "page:catalog/entity", + "input": "contents", + }, + "configSchema": { + "parse": [Function], + "schema": { + "$schema": "http://json-schema.org/draft-07/schema#", + "additionalProperties": false, + "properties": { + "filter": { + "type": "string", + }, + "path": { + "type": "string", + }, + "title": { + "type": "string", + }, + }, + "type": "object", + }, + }, + "disabled": false, + "factory": [Function], + "inputs": {}, + "kind": "entity-content", + "name": "test", + "namespace": undefined, + "output": [ + [Function], + [Function], + [Function], + { + "$$type": "@backstage/ExtensionDataRef", + "config": { + "optional": true, + }, + "id": "core.routing.ref", + "optional": [Function], + "toString": [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], + "version": "v2", + } + `); + }); + + it('should emit the correct defaults', () => { + const mockRouteRef = createRouteRef(); + const extension = EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + routeRef: mockRouteRef, + loader: async () =>
Test!
, + }, + }); + + const tester = createExtensionTester(extension); + + // todo(blam): route paths are always set to / in the createExtensionTester. This will work eventually. + // expect(tester.data(coreExtensionData.routePath)).toBe('/test'); + + expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef); + expect(tester.data(EntityContentBlueprint.dataRefs.title)).toBe('Test'); + }); + + it('should emit the correct filter output', () => { + const mockFilter = (_entity: Entity) => true; + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
Test!
, + filter: 'test', + }, + }), + ).data(EntityContentBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () =>
Test!
, + }, + }), + { config: { filter: 'test' } }, + ).data(EntityContentBlueprint.dataRefs.filterExpression), + ).toBe('test'); + + expect( + createExtensionTester( + EntityContentBlueprint.make({ + name: 'test', + params: { + defaultPath: '/test', + defaultTitle: 'Test', + filter: mockFilter, + loader: async () =>
Test!
, + }, + }), + ).data(EntityContentBlueprint.dataRefs.filterFunction), + ).toBe(mockFilter); + }); + + it('should allow overriding config and inputs', async () => { + const extension = EntityContentBlueprint.makeWithOverrides({ + name: 'test', + inputs: { + mock: createExtensionInput([coreExtensionData.reactElement]), + }, + config: { + schema: { + mock: z => z.string(), + }, + }, + factory(originalFactory, { inputs, config }) { + return originalFactory({ + defaultPath: '/test', + defaultTitle: 'Test', + loader: async () => ( +
+ config: {config.mock} +
+ {inputs.mock.map((i, k) => ( +
{i.get(coreExtensionData.reactElement)}
+ ))} +
+
+ ), + }); + }, + }); + + const mockExtension = createExtension({ + attachTo: { id: 'entity-content:test', input: 'mock' }, + output: [coreExtensionData.reactElement], + factory() { + return [coreExtensionData.reactElement(
im a mock
)]; + }, + }); + + createExtensionTester(extension, { config: { mock: 'mock test config' } }) + .add(mockExtension) + .render(); + + await waitFor(() => { + expect(screen.getByTestId('test')).toBeInTheDocument(); + expect(screen.getByTestId('test')).toHaveTextContent( + 'config: mock test config', + ); + expect(screen.getByTestId('contents')).toHaveTextContent('im a mock'); + }); + }); +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx new file mode 100644 index 0000000000..7c7a6c43b7 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/EntityContentBlueprint.tsx @@ -0,0 +1,99 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React, { lazy } from 'react'; +import { + coreExtensionData, + createExtensionBlueprint, + ExtensionBoundary, + RouteRef, +} from '@backstage/frontend-plugin-api'; +import { catalogExtensionData } from '../extensions'; + +/** + * @alpha + * Creates an EntityContent extension. + */ +export const EntityContentBlueprint = createExtensionBlueprint({ + kind: 'entity-content', + attachTo: { id: 'page:catalog/entity', input: 'contents' }, + output: [ + coreExtensionData.reactElement, + coreExtensionData.routePath, + catalogExtensionData.entityContentTitle, + coreExtensionData.routeRef.optional(), + catalogExtensionData.entityFilterFunction.optional(), + catalogExtensionData.entityFilterExpression.optional(), + ], + dataRefs: { + title: catalogExtensionData.entityContentTitle, + filterFunction: catalogExtensionData.entityFilterFunction, + filterExpression: catalogExtensionData.entityFilterExpression, + }, + config: { + schema: { + path: z => z.string().optional(), + title: z => z.string().optional(), + filter: z => z.string().optional(), + }, + }, + *factory( + { + loader, + defaultPath, + defaultTitle, + filter, + routeRef, + }: { + loader: () => Promise; + defaultPath: string; + defaultTitle: string; + routeRef?: RouteRef; + filter?: + | typeof catalogExtensionData.entityFilterFunction.T + | typeof catalogExtensionData.entityFilterExpression.T; + }, + { node, config }, + ) { + const path = config.path ?? defaultPath; + const title = config.title ?? defaultTitle; + + const ExtensionComponent = lazy(() => + loader().then(element => ({ default: () => element })), + ); + + yield coreExtensionData.reactElement( + + + , + ); + + yield coreExtensionData.routePath(path); + + yield catalogExtensionData.entityContentTitle(title); + + if (routeRef) { + yield coreExtensionData.routeRef(routeRef); + } + + if (config.filter) { + yield catalogExtensionData.entityFilterExpression(config.filter); + } else if (typeof filter === 'string') { + yield catalogExtensionData.entityFilterExpression(filter); + } else if (typeof filter === 'function') { + yield catalogExtensionData.entityFilterFunction(filter); + } + }, +}); diff --git a/plugins/catalog-react/src/alpha/blueprints/index.ts b/plugins/catalog-react/src/alpha/blueprints/index.ts new file mode 100644 index 0000000000..dccaf28cd9 --- /dev/null +++ b/plugins/catalog-react/src/alpha/blueprints/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export { EntityCardBlueprint } from './EntityCardBlueprint'; +export { EntityContentBlueprint } from './EntityContentBlueprint'; diff --git a/plugins/catalog-react/src/alpha.tsx b/plugins/catalog-react/src/alpha/extensions.tsx similarity index 93% rename from plugins/catalog-react/src/alpha.tsx rename to plugins/catalog-react/src/alpha/extensions.tsx index 42d69726b6..fa91494bd4 100644 --- a/plugins/catalog-react/src/alpha.tsx +++ b/plugins/catalog-react/src/alpha/extensions.tsx @@ -28,13 +28,16 @@ import { import React, { lazy } from 'react'; import { Entity } from '@backstage/catalog-model'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports -import { Expand } from '../../../packages/frontend-plugin-api/src/types'; +import { Expand } from '../../../../packages/frontend-plugin-api/src/types'; -export { useEntityPermission } from './hooks/useEntityPermission'; -export { isOwnerOf } from './utils'; -export * from './translation'; +export { useEntityPermission } from '../hooks/useEntityPermission'; +export { isOwnerOf } from '../utils'; +export * from '../translation'; -/** @alpha */ +/** + * @alpha + * @deprecated use `dataRefs` on the blueprints instead + */ export const catalogExtensionData = { entityContentTitle: createExtensionDataRef().with({ id: 'catalog.entity-content-title', @@ -48,7 +51,10 @@ export const catalogExtensionData = { }; // TODO: Figure out how to merge with provided config schema -/** @alpha */ +/** + * @alpha + * @deprecated use {@link EntityCardBlueprint} instead + */ export function createEntityCardExtension< TConfig extends { filter?: string }, TInputs extends AnyExtensionInputMap, @@ -110,7 +116,10 @@ export function createEntityCardExtension< }); } -/** @alpha */ +/** + * @alpha + * @deprecated use {@link EntityContentBlueprint} instead + */ export function createEntityContentExtension< TInputs extends AnyExtensionInputMap, >(options: { diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts new file mode 100644 index 0000000000..132b288008 --- /dev/null +++ b/plugins/catalog-react/src/alpha/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2024 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './blueprints'; +export * from './extensions'; diff --git a/plugins/catalog-react/src/setupTests.ts b/plugins/catalog-react/src/setupTests.ts index 963c0f188b..dae4360fd3 100644 --- a/plugins/catalog-react/src/setupTests.ts +++ b/plugins/catalog-react/src/setupTests.ts @@ -15,3 +15,18 @@ */ import '@testing-library/jest-dom'; + +// eslint-disable-next-line no-console +const originalConsoleWarn = console.warn; +// eslint-disable-next-line no-console +console.warn = (...args: any[]) => { + const message = args[0]; + if ( + typeof message === 'string' && + (message.includes('CSSOM.parse is not a function') || + message.includes('[JSS]')) + ) { + return; + } + originalConsoleWarn(...args); +};