From 8925f18f7c93a6725010498e4412545a0e189e2e Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 17:24:10 +0200 Subject: [PATCH 01/13] core-compat-api: initial stab at plugin conversion Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/package.json | 3 +- .../src/convertLegacyPlugin.test.tsx | 0 .../src/convertLegacyPlugin.tsx | 85 +++++++++++++++++++ yarn.lock | 1 + 4 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 packages/core-compat-api/src/convertLegacyPlugin.test.tsx create mode 100644 packages/core-compat-api/src/convertLegacyPlugin.tsx diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index 560a547af5..9ebc1a0185 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -34,7 +34,8 @@ "@backstage/core-plugin-api": "workspace:^", "@backstage/frontend-plugin-api": "workspace:^", "@backstage/version-bridge": "workspace:^", - "@types/react": "^16.13.1 || ^17.0.0" + "@types/react": "^16.13.1 || ^17.0.0", + "lodash": "^4.17.21" }, "devDependencies": { "@backstage-community/plugin-puppetdb": "^0.1.18", diff --git a/packages/core-compat-api/src/convertLegacyPlugin.test.tsx b/packages/core-compat-api/src/convertLegacyPlugin.test.tsx new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/core-compat-api/src/convertLegacyPlugin.tsx b/packages/core-compat-api/src/convertLegacyPlugin.tsx new file mode 100644 index 0000000000..052888d53c --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyPlugin.tsx @@ -0,0 +1,85 @@ +/* + * 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 { + BackstagePlugin as LegacyBackstagePlugin, + getComponentData, + RouteRef as LegacyRouteRef, +} from '@backstage/core-plugin-api'; +import { + ExtensionDefinition, + BackstagePlugin as NewBackstagePlugin, + createPageExtension, + createPlugin, +} from '@backstage/frontend-plugin-api'; +import kebabCase from 'lodash/kebabCase'; +import { + convertLegacyRouteRef, + convertLegacyRouteRefs, +} from './convertLegacyRouteRef'; +import { ComponentType } from 'react'; +import React from 'react'; +import { compatWrapper } from './compatWrapper'; + +/** @internal */ +export function convertLegacyExtension( + LegacyExtension: ComponentType<{}>, + legacyPlugin: LegacyBackstagePlugin, +): ExtensionDefinition { + const element = ; + + const plugin = getComponentData( + element, + 'core.plugin', + ); + if (legacyPlugin !== plugin) { + throw new Error( + `The extension does not belong to the same plugin, got ${plugin?.getId()}`, + ); + } + + const name = getComponentData(element, 'core.extensionName'); + if (!name) { + throw new Error('Extension has no name'); + } + + const mountPoint = getComponentData( + element, + 'core.mountPoint', + ); + + if (name.endsWith('Page')) { + return createPageExtension({ + defaultPath: kebabCase(name.slice(0, -'Page'.length)), + routeRef: mountPoint && convertLegacyRouteRef(mountPoint), + loader: () => Promise.resolve(compatWrapper(element)), + }); + } +} + +/** @public */ +export function convertLegacyPlugin( + legacyPlugin: LegacyBackstagePlugin, + options: { extensions: ExtensionDefinition[] }, +): NewBackstagePlugin { + return createPlugin({ + id: legacyPlugin.getId(), + featureFlags: [...legacyPlugin.getFeatureFlags()], + routes: convertLegacyRouteRefs(legacyPlugin.routes ?? {}), + externalRoutes: convertLegacyRouteRefs(legacyPlugin.externalRoutes ?? {}), + extensions: options.extensions, + }); +} diff --git a/yarn.lock b/yarn.lock index 301d7de22f..82b65687e6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4202,6 +4202,7 @@ __metadata: "@testing-library/jest-dom": ^6.0.0 "@testing-library/react": ^15.0.0 "@types/react": ^16.13.1 || ^17.0.0 + lodash: ^4.17.21 peerDependencies: react: ^16.13.1 || ^17.0.0 || ^18.0.0 react-router-dom: 6.0.0-beta.0 || ^6.3.0 From 71fb0269e3454dff644b78ff4556dbf73eee5b5f Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 13:01:27 +0200 Subject: [PATCH 02/13] core-compat-api: spit out explicit legacy extension converter Signed-off-by: Patrik Oldsberg --- ...gin.tsx => convertLegacyPageExtension.tsx} | 63 +++++++------------ .../src/convertLegacyPlugin.ts | 37 +++++++++++ 2 files changed, 58 insertions(+), 42 deletions(-) rename packages/core-compat-api/src/{convertLegacyPlugin.tsx => convertLegacyPageExtension.tsx} (50%) create mode 100644 packages/core-compat-api/src/convertLegacyPlugin.ts diff --git a/packages/core-compat-api/src/convertLegacyPlugin.tsx b/packages/core-compat-api/src/convertLegacyPageExtension.tsx similarity index 50% rename from packages/core-compat-api/src/convertLegacyPlugin.tsx rename to packages/core-compat-api/src/convertLegacyPageExtension.tsx index 052888d53c..2ff5e0c574 100644 --- a/packages/core-compat-api/src/convertLegacyPlugin.tsx +++ b/packages/core-compat-api/src/convertLegacyPageExtension.tsx @@ -15,44 +15,31 @@ */ import { - BackstagePlugin as LegacyBackstagePlugin, getComponentData, RouteRef as LegacyRouteRef, } from '@backstage/core-plugin-api'; import { ExtensionDefinition, - BackstagePlugin as NewBackstagePlugin, - createPageExtension, - createPlugin, + PageBlueprint, } from '@backstage/frontend-plugin-api'; import kebabCase from 'lodash/kebabCase'; -import { - convertLegacyRouteRef, - convertLegacyRouteRefs, -} from './convertLegacyRouteRef'; +import { convertLegacyRouteRef } from './convertLegacyRouteRef'; import { ComponentType } from 'react'; import React from 'react'; import { compatWrapper } from './compatWrapper'; -/** @internal */ -export function convertLegacyExtension( +/** @public */ +export function convertLegacyPageExtension( LegacyExtension: ComponentType<{}>, - legacyPlugin: LegacyBackstagePlugin, -): ExtensionDefinition { + overrides?: { + name?: string; + defaultPath?: string; + }, +): ExtensionDefinition { const element = ; - const plugin = getComponentData( - element, - 'core.plugin', - ); - if (legacyPlugin !== plugin) { - throw new Error( - `The extension does not belong to the same plugin, got ${plugin?.getId()}`, - ); - } - - const name = getComponentData(element, 'core.extensionName'); - if (!name) { + const extName = getComponentData(element, 'core.extensionName'); + if (!extName) { throw new Error('Extension has no name'); } @@ -61,25 +48,17 @@ export function convertLegacyExtension( 'core.mountPoint', ); - if (name.endsWith('Page')) { - return createPageExtension({ - defaultPath: kebabCase(name.slice(0, -'Page'.length)), + const name = extName.endsWith('Page') + ? extName.slice(0, -'Page'.length) + : extName; + const kebabName = kebabCase(name); + + return PageBlueprint.make({ + name: overrides?.name ?? kebabName, + params: { + defaultPath: overrides?.defaultPath ?? `/${kebabName}`, routeRef: mountPoint && convertLegacyRouteRef(mountPoint), loader: () => Promise.resolve(compatWrapper(element)), - }); - } -} - -/** @public */ -export function convertLegacyPlugin( - legacyPlugin: LegacyBackstagePlugin, - options: { extensions: ExtensionDefinition[] }, -): NewBackstagePlugin { - return createPlugin({ - id: legacyPlugin.getId(), - featureFlags: [...legacyPlugin.getFeatureFlags()], - routes: convertLegacyRouteRefs(legacyPlugin.routes ?? {}), - externalRoutes: convertLegacyRouteRefs(legacyPlugin.externalRoutes ?? {}), - extensions: options.extensions, + }, }); } diff --git a/packages/core-compat-api/src/convertLegacyPlugin.ts b/packages/core-compat-api/src/convertLegacyPlugin.ts new file mode 100644 index 0000000000..05beb4e469 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyPlugin.ts @@ -0,0 +1,37 @@ +/* + * 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 { BackstagePlugin as LegacyBackstagePlugin } from '@backstage/core-plugin-api'; +import { + ExtensionDefinition, + BackstagePlugin as NewBackstagePlugin, + createFrontendPlugin, +} from '@backstage/frontend-plugin-api'; +import { convertLegacyRouteRefs } from './convertLegacyRouteRef'; + +/** @public */ +export function convertLegacyPlugin( + legacyPlugin: LegacyBackstagePlugin, + options: { extensions: ExtensionDefinition[] }, +): NewBackstagePlugin { + return createFrontendPlugin({ + id: legacyPlugin.getId(), + featureFlags: [...legacyPlugin.getFeatureFlags()], + routes: convertLegacyRouteRefs(legacyPlugin.routes ?? {}), + externalRoutes: convertLegacyRouteRefs(legacyPlugin.externalRoutes ?? {}), + extensions: options.extensions, + }); +} From 6f23e5c24ef266655868db80029f2f70c1ba52a3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 13:15:54 +0200 Subject: [PATCH 03/13] core-compat-api: test + support APIs for convertLegacyPlugin Signed-off-by: Patrik Oldsberg --- .../src/convertLegacyPlugin.test.tsx | 91 +++++++++++++++++++ .../src/convertLegacyPlugin.ts | 6 +- 2 files changed, 96 insertions(+), 1 deletion(-) diff --git a/packages/core-compat-api/src/convertLegacyPlugin.test.tsx b/packages/core-compat-api/src/convertLegacyPlugin.test.tsx index e69de29bb2..4e17c1d61b 100644 --- a/packages/core-compat-api/src/convertLegacyPlugin.test.tsx +++ b/packages/core-compat-api/src/convertLegacyPlugin.test.tsx @@ -0,0 +1,91 @@ +/* + * 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 { + createPlugin as createLegacyPlugin, + createRouteRef as createLegacyRouteRef, + createExternalRouteRef as createLegacyExternalRouteRef, + createApiFactory, + createApiRef, +} from '@backstage/core-plugin-api'; +import { convertLegacyPlugin } from './convertLegacyPlugin'; +import { PageBlueprint } from '@backstage/frontend-plugin-api'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalBackstagePlugin } from '../../frontend-plugin-api/src/wiring/createFrontendPlugin'; + +describe('convertLegacyPlugin', () => { + it('should convert a plain legacy plugin to a new plugin', () => { + expect( + convertLegacyPlugin(createLegacyPlugin({ id: 'test' }), { + extensions: [], + }), + ).toMatchInlineSnapshot(` + { + "$$type": "@backstage/BackstagePlugin", + "extensions": [], + "externalRoutes": {}, + "featureFlags": [], + "getExtension": [Function], + "id": "test", + "routes": {}, + "toString": [Function], + "version": "v1", + "withOverrides": [Function], + } + `); + }); + + it('should convert a legacy plugin with options to a new plugin', () => { + const apiRef = createApiRef({ id: 'plugin.test.client' }); + + const routeRef = createLegacyRouteRef({ id: 'test' }); + const extRouteRef = createLegacyExternalRouteRef({ id: 'testExt' }); + + const converted = convertLegacyPlugin( + createLegacyPlugin({ + id: 'test', + apis: [createApiFactory(apiRef, 'hello')], + routes: { test: routeRef }, + externalRoutes: { + testExt: extRouteRef, + }, + featureFlags: [{ name: 'test-flag' }], + }), + { + extensions: [ + PageBlueprint.make({ + params: { defaultPath: '/test', loader: async () => ({} as any) }, + }), + ], + }, + ); + + const internalConverted = toInternalBackstagePlugin(converted); + + expect(internalConverted.id).toBe('test'); + expect(internalConverted.routes).toEqual({ + test: routeRef, + }); + expect(internalConverted.externalRoutes).toEqual({ + testExt: extRouteRef, + }); + expect(internalConverted.featureFlags).toEqual([{ name: 'test-flag' }]); + expect(internalConverted.extensions.map(e => e.id)).toEqual([ + 'api:test/plugin.test.client', + 'page:test', + ]); + }); +}); diff --git a/packages/core-compat-api/src/convertLegacyPlugin.ts b/packages/core-compat-api/src/convertLegacyPlugin.ts index 05beb4e469..db8daea9cd 100644 --- a/packages/core-compat-api/src/convertLegacyPlugin.ts +++ b/packages/core-compat-api/src/convertLegacyPlugin.ts @@ -16,6 +16,7 @@ import { BackstagePlugin as LegacyBackstagePlugin } from '@backstage/core-plugin-api'; import { + ApiBlueprint, ExtensionDefinition, BackstagePlugin as NewBackstagePlugin, createFrontendPlugin, @@ -27,11 +28,14 @@ export function convertLegacyPlugin( legacyPlugin: LegacyBackstagePlugin, options: { extensions: ExtensionDefinition[] }, ): NewBackstagePlugin { + const apiExtensions = Array.from(legacyPlugin.getApis()).map(factory => + ApiBlueprint.make({ name: factory.api.id, params: { factory } }), + ); return createFrontendPlugin({ id: legacyPlugin.getId(), featureFlags: [...legacyPlugin.getFeatureFlags()], routes: convertLegacyRouteRefs(legacyPlugin.routes ?? {}), externalRoutes: convertLegacyRouteRefs(legacyPlugin.externalRoutes ?? {}), - extensions: options.extensions, + extensions: [...apiExtensions, ...options.extensions], }); } From c6d86b7191495085ac16b4843471d2711e6a938a Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 13:32:40 +0200 Subject: [PATCH 04/13] core-compat-api: test for convertLegacyPageExtension Signed-off-by: Patrik Oldsberg --- .../src/convertLegacyPageExtension.test.tsx | 99 +++++++++++++++++++ 1 file changed, 99 insertions(+) create mode 100644 packages/core-compat-api/src/convertLegacyPageExtension.test.tsx diff --git a/packages/core-compat-api/src/convertLegacyPageExtension.test.tsx b/packages/core-compat-api/src/convertLegacyPageExtension.test.tsx new file mode 100644 index 0000000000..482474d580 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyPageExtension.test.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 { + createPlugin as createLegacyPlugin, + createRouteRef as createLegacyRouteRef, + createRoutableExtension, +} from '@backstage/core-plugin-api'; +import { coreExtensionData } from '@backstage/frontend-plugin-api'; +import { + createExtensionTester, + renderInTestApp, +} from '@backstage/frontend-test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { convertLegacyPageExtension } from './convertLegacyPageExtension'; +import { convertLegacyRouteRef } from './convertLegacyRouteRef'; + +const routeRef = createLegacyRouteRef({ id: 'test' }); +const legacyPlugin = createLegacyPlugin({ + id: 'test', + routes: { + test: routeRef, + }, +}); + +describe('convertLegacyPageExtension', () => { + it('should convert a page extension', async () => { + const LegacyExtension = legacyPlugin.provide( + createRoutableExtension({ + name: 'ExamplePage', + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ); + + const converted = convertLegacyPageExtension(LegacyExtension); + expect(converted.kind).toBe('page'); + expect(converted.namespace).toBe(undefined); + expect(converted.name).toBe('example'); + + const tester = createExtensionTester(converted); + + await renderInTestApp(tester.reactElement(), { + mountedRoutes: { + '/': convertLegacyRouteRef(routeRef), + }, + }); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + + expect(tester.get(coreExtensionData.routePath)).toBe('/example'); + expect(tester.get(coreExtensionData.routeRef)).toBe(routeRef); + }); + + it('should convert a page extension with overrides', async () => { + const LegacyExtension = legacyPlugin.provide( + createRoutableExtension({ + name: 'ExamplePage', + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ); + + const converted = convertLegacyPageExtension(LegacyExtension, { + name: 'other', + defaultPath: '/other', + }); + expect(converted.kind).toBe('page'); + expect(converted.namespace).toBe(undefined); + expect(converted.name).toBe('other'); + + const tester = createExtensionTester(converted); + + await renderInTestApp(tester.reactElement(), { + mountedRoutes: { + '/': convertLegacyRouteRef(routeRef), + }, + }); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + + expect(tester.get(coreExtensionData.routePath)).toBe('/other'); + expect(tester.get(coreExtensionData.routeRef)).toBe(routeRef); + }); +}); From 771bd7a5ffa7e245a64f8845e510184bbaeb8169 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 13:55:51 +0200 Subject: [PATCH 05/13] core-compat-api: async instead of Promise.resolve Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/src/convertLegacyPageExtension.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/core-compat-api/src/convertLegacyPageExtension.tsx b/packages/core-compat-api/src/convertLegacyPageExtension.tsx index 2ff5e0c574..7ef4e0faff 100644 --- a/packages/core-compat-api/src/convertLegacyPageExtension.tsx +++ b/packages/core-compat-api/src/convertLegacyPageExtension.tsx @@ -58,7 +58,7 @@ export function convertLegacyPageExtension( params: { defaultPath: overrides?.defaultPath ?? `/${kebabName}`, routeRef: mountPoint && convertLegacyRouteRef(mountPoint), - loader: () => Promise.resolve(compatWrapper(element)), + loader: async () => compatWrapper(element), }, }); } From e0e5cea175e924fd514086975ebd6652e9cfdc07 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:31:31 +0200 Subject: [PATCH 06/13] catalog-react: add initial legacy converters Signed-off-by: Patrik Oldsberg --- plugins/catalog-react/package.json | 1 + .../convertLegacyEntityCardExtension.tsx | 52 ++++++++++++++ .../convertLegacyEntityContentExtension.tsx | 69 +++++++++++++++++++ .../src/alpha/converters/index.ts | 18 +++++ plugins/catalog-react/src/alpha/index.ts | 2 + yarn.lock | 1 + 6 files changed, 143 insertions(+) create mode 100644 plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx create mode 100644 plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx create mode 100644 plugins/catalog-react/src/alpha/converters/index.ts diff --git a/plugins/catalog-react/package.json b/plugins/catalog-react/package.json index ef5d924200..f7b43a29b4 100644 --- a/plugins/catalog-react/package.json +++ b/plugins/catalog-react/package.json @@ -59,6 +59,7 @@ "dependencies": { "@backstage/catalog-client": "workspace:^", "@backstage/catalog-model": "workspace:^", + "@backstage/core-compat-api": "workspace:^", "@backstage/core-components": "workspace:^", "@backstage/core-plugin-api": "workspace:^", "@backstage/errors": "workspace:^", diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx new file mode 100644 index 0000000000..25ea778f0d --- /dev/null +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx @@ -0,0 +1,52 @@ +/* + * 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 { compatWrapper } from '@backstage/core-compat-api'; +import { getComponentData } from '@backstage/core-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import kebabCase from 'lodash/kebabCase'; +import React, { ComponentType } from 'react'; +import { EntityCardBlueprint } from '../blueprints'; + +/** @alpha */ +export function convertLegacyEntityCardExtension( + LegacyExtension: ComponentType<{}>, + overrides?: { + name?: string; + filter?: + | typeof EntityCardBlueprint.dataRefs.filterFunction.T + | typeof EntityCardBlueprint.dataRefs.filterExpression.T; + }, +): ExtensionDefinition { + const element = ; + + const extName = getComponentData(element, 'core.extensionName'); + if (!extName) { + throw new Error('Extension has no name'); + } + + const match = extName.match(/^Entity(.*)Card$/); + const name = match?.[1] ?? extName; + const kebabName = kebabCase(name); + + return EntityCardBlueprint.make({ + name: overrides?.name ?? kebabName, + params: { + filter: overrides?.filter, + loader: async () => compatWrapper(element), + }, + }); +} diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx new file mode 100644 index 0000000000..ea452a4d24 --- /dev/null +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx @@ -0,0 +1,69 @@ +/* + * 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 { + compatWrapper, + convertLegacyRouteRef, +} from '@backstage/core-compat-api'; +import { + getComponentData, + RouteRef as LegacyRouteRef, +} from '@backstage/core-plugin-api'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; +import kebabCase from 'lodash/kebabCase'; +import startCase from 'lodash/startCase'; +import React, { ComponentType } from 'react'; +import { EntityContentBlueprint } from '../blueprints'; + +/** @alpha */ +export function convertLegacyEntityContentExtension( + LegacyExtension: ComponentType<{}>, + overrides?: { + name?: string; + filter?: + | typeof EntityContentBlueprint.dataRefs.filterFunction.T + | typeof EntityContentBlueprint.dataRefs.filterExpression.T; + defaultPath?: string; + defaultTitle?: string; + }, +): ExtensionDefinition { + const element = ; + + const extName = getComponentData(element, 'core.extensionName'); + if (!extName) { + throw new Error('Extension has no name'); + } + + const mountPoint = getComponentData( + element, + 'core.mountPoint', + ); + + const match = extName.match(/^Entity(.*)Content$/); + const name = match?.[1] ?? extName; + const kebabName = kebabCase(name); + + return EntityContentBlueprint.make({ + name: overrides?.name ?? kebabName, + params: { + filter: overrides?.filter, + defaultPath: overrides?.defaultPath ?? `/${kebabName}`, + defaultTitle: overrides?.defaultTitle ?? startCase(name), + routeRef: mountPoint && convertLegacyRouteRef(mountPoint), + loader: async () => compatWrapper(element), + }, + }); +} diff --git a/plugins/catalog-react/src/alpha/converters/index.ts b/plugins/catalog-react/src/alpha/converters/index.ts new file mode 100644 index 0000000000..731ab846c0 --- /dev/null +++ b/plugins/catalog-react/src/alpha/converters/index.ts @@ -0,0 +1,18 @@ +/* + * 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 { convertLegacyEntityCardExtension } from './convertLegacyEntityCardExtension'; +export { convertLegacyEntityContentExtension } from './convertLegacyEntityContentExtension'; diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index 132b288008..3930fbde87 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -13,5 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export * from './blueprints'; export * from './extensions'; +export * from './converters'; diff --git a/yarn.lock b/yarn.lock index 82b65687e6..f20870e0c4 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5918,6 +5918,7 @@ __metadata: "@backstage/catalog-model": "workspace:^" "@backstage/cli": "workspace:^" "@backstage/core-app-api": "workspace:^" + "@backstage/core-compat-api": "workspace:^" "@backstage/core-components": "workspace:^" "@backstage/core-plugin-api": "workspace:^" "@backstage/errors": "workspace:^" From 769514e9922032f28a57c0cbe7e9bfe7062dc25d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:34:19 +0200 Subject: [PATCH 07/13] catalog-react: better name conversion for legacy converters Signed-off-by: Patrik Oldsberg --- .../convertLegacyEntityCardExtension.tsx | 26 ++++++++++++++----- .../convertLegacyEntityContentExtension.tsx | 26 +++++++++++++++---- 2 files changed, 41 insertions(+), 11 deletions(-) diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx index 25ea778f0d..afe5211d1b 100644 --- a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx @@ -15,9 +15,8 @@ */ import { compatWrapper } from '@backstage/core-compat-api'; -import { getComponentData } from '@backstage/core-plugin-api'; +import { BackstagePlugin, getComponentData } from '@backstage/core-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; -import kebabCase from 'lodash/kebabCase'; import React, { ComponentType } from 'react'; import { EntityCardBlueprint } from '../blueprints'; @@ -38,12 +37,27 @@ export function convertLegacyEntityCardExtension( throw new Error('Extension has no name'); } - const match = extName.match(/^Entity(.*)Card$/); - const name = match?.[1] ?? extName; - const kebabName = kebabCase(name); + const plugin = getComponentData(element, 'core.plugin'); + const pluginId = plugin?.getId(); + + const match = extName.match(/^Entity(.*)Content$/); + const infix = match?.[1] ?? extName; + + let name: string | undefined = infix; + if ( + pluginId && + name + .toLocaleLowerCase('en-US') + .startsWith(pluginId.toLocaleLowerCase('en-US')) + ) { + name = name.slice(pluginId.length); + if (!name) { + name = undefined; + } + } return EntityCardBlueprint.make({ - name: overrides?.name ?? kebabName, + name: overrides?.name ?? name, params: { filter: overrides?.filter, loader: async () => compatWrapper(element), diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx index ea452a4d24..a44a6281da 100644 --- a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx @@ -19,6 +19,7 @@ import { convertLegacyRouteRef, } from '@backstage/core-compat-api'; import { + BackstagePlugin, getComponentData, RouteRef as LegacyRouteRef, } from '@backstage/core-plugin-api'; @@ -52,16 +53,31 @@ export function convertLegacyEntityContentExtension( 'core.mountPoint', ); + const plugin = getComponentData(element, 'core.plugin'); + const pluginId = plugin?.getId(); + const match = extName.match(/^Entity(.*)Content$/); - const name = match?.[1] ?? extName; - const kebabName = kebabCase(name); + const infix = match?.[1] ?? extName; + + let name: string | undefined = infix; + if ( + pluginId && + name + .toLocaleLowerCase('en-US') + .startsWith(pluginId.toLocaleLowerCase('en-US')) + ) { + name = name.slice(pluginId.length); + if (!name) { + name = undefined; + } + } return EntityContentBlueprint.make({ - name: overrides?.name ?? kebabName, + name: overrides?.name ?? name, params: { filter: overrides?.filter, - defaultPath: overrides?.defaultPath ?? `/${kebabName}`, - defaultTitle: overrides?.defaultTitle ?? startCase(name), + defaultPath: overrides?.defaultPath ?? `/${kebabCase(infix)}`, + defaultTitle: overrides?.defaultTitle ?? startCase(infix), routeRef: mountPoint && convertLegacyRouteRef(mountPoint), loader: async () => compatWrapper(element), }, From 3f734557aceacc4e9870f879ca94fb01f9548d59 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:35:28 +0200 Subject: [PATCH 08/13] core-compat-api: use namespace pattern for APIs converted with convertLegacyPlugin Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/src/convertLegacyPlugin.test.tsx | 2 +- packages/core-compat-api/src/convertLegacyPlugin.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/core-compat-api/src/convertLegacyPlugin.test.tsx b/packages/core-compat-api/src/convertLegacyPlugin.test.tsx index 4e17c1d61b..657ee91725 100644 --- a/packages/core-compat-api/src/convertLegacyPlugin.test.tsx +++ b/packages/core-compat-api/src/convertLegacyPlugin.test.tsx @@ -84,7 +84,7 @@ describe('convertLegacyPlugin', () => { }); expect(internalConverted.featureFlags).toEqual([{ name: 'test-flag' }]); expect(internalConverted.extensions.map(e => e.id)).toEqual([ - 'api:test/plugin.test.client', + 'api:plugin.test.client', 'page:test', ]); }); diff --git a/packages/core-compat-api/src/convertLegacyPlugin.ts b/packages/core-compat-api/src/convertLegacyPlugin.ts index db8daea9cd..5856108f29 100644 --- a/packages/core-compat-api/src/convertLegacyPlugin.ts +++ b/packages/core-compat-api/src/convertLegacyPlugin.ts @@ -29,7 +29,7 @@ export function convertLegacyPlugin( options: { extensions: ExtensionDefinition[] }, ): NewBackstagePlugin { const apiExtensions = Array.from(legacyPlugin.getApis()).map(factory => - ApiBlueprint.make({ name: factory.api.id, params: { factory } }), + ApiBlueprint.make({ namespace: factory.api.id, params: { factory } }), ); return createFrontendPlugin({ id: legacyPlugin.getId(), From 448d62ae8819a7e6dce5a79adb146fa258788194 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:36:57 +0200 Subject: [PATCH 09/13] core-compat-api: export new legacy converts + update API report Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/api-report.md | 21 +++++++++++++++++++++ packages/core-compat-api/src/index.ts | 2 ++ 2 files changed, 23 insertions(+) diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index 8d177b4dab..75b91cc055 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -8,6 +8,10 @@ import { AnalyticsApi as AnalyticsApi_2 } from '@backstage/frontend-plugin-api'; import { AnalyticsEvent } from '@backstage/core-plugin-api'; import { AnalyticsEvent as AnalyticsEvent_2 } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/core-plugin-api'; +import { BackstagePlugin } from '@backstage/core-plugin-api'; +import { BackstagePlugin as BackstagePlugin_2 } from '@backstage/frontend-plugin-api'; +import { ComponentType } from 'react'; +import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import { ExternalRouteRef } from '@backstage/core-plugin-api'; import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api'; import { FrontendFeature } from '@backstage/frontend-plugin-api'; @@ -26,6 +30,23 @@ export function convertLegacyApp( rootElement: React_2.JSX.Element, ): FrontendFeature[]; +// @public (undocumented) +export function convertLegacyPageExtension( + LegacyExtension: ComponentType<{}>, + overrides?: { + name?: string; + defaultPath?: string; + }, +): ExtensionDefinition; + +// @public (undocumented) +export function convertLegacyPlugin( + legacyPlugin: BackstagePlugin, + options: { + extensions: ExtensionDefinition[]; + }, +): BackstagePlugin_2; + // @public export function convertLegacyRouteRef( ref: RouteRef, diff --git a/packages/core-compat-api/src/index.ts b/packages/core-compat-api/src/index.ts index 88e1892eac..7f99a7ab7a 100644 --- a/packages/core-compat-api/src/index.ts +++ b/packages/core-compat-api/src/index.ts @@ -18,6 +18,8 @@ export * from './compatWrapper'; export * from './apis'; export { convertLegacyApp } from './convertLegacyApp'; +export { convertLegacyPlugin } from './convertLegacyPlugin'; +export { convertLegacyPageExtension } from './convertLegacyPageExtension'; export { convertLegacyRouteRef, convertLegacyRouteRefs, From 3edf625c922ce9208810d0ebc24c5837a3c19dff Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:46:05 +0200 Subject: [PATCH 10/13] catalog-react: legacy converter tests + fixes Signed-off-by: Patrik Oldsberg --- .../convertLegacyEntityCardExtension.test.tsx | 129 +++++++++++++++++ .../convertLegacyEntityCardExtension.tsx | 4 +- ...nvertLegacyEntityContentExtension.test.tsx | 136 ++++++++++++++++++ .../convertLegacyEntityContentExtension.tsx | 1 + 4 files changed, 269 insertions(+), 1 deletion(-) create mode 100644 plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.test.tsx create mode 100644 plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.test.tsx diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.test.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.test.tsx new file mode 100644 index 0000000000..7dacd9c3e8 --- /dev/null +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.test.tsx @@ -0,0 +1,129 @@ +/* + * 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 { + createPlugin as createLegacyPlugin, + createRouteRef as createLegacyRouteRef, + createRoutableExtension, +} from '@backstage/core-plugin-api'; +import { + createExtensionTester, + renderInTestApp, +} from '@backstage/frontend-test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { convertLegacyEntityCardExtension } from './convertLegacyEntityCardExtension'; +import { convertLegacyRouteRef } from '@backstage/core-compat-api'; +import { EntityContentBlueprint } from '../blueprints'; + +const routeRef = createLegacyRouteRef({ id: 'test' }); +const legacyPlugin = createLegacyPlugin({ + id: 'test', + routes: { + test: routeRef, + }, +}); + +describe('convertLegacyEntityCardExtension', () => { + it('should convert an entity card extension', async () => { + const LegacyExtension = legacyPlugin.provide( + createRoutableExtension({ + name: 'EntityExampleCard', + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ); + + const converted = convertLegacyEntityCardExtension(LegacyExtension); + expect(converted.kind).toBe('entity-card'); + expect(converted.namespace).toBe(undefined); + expect(converted.name).toBe('example'); + + const tester = createExtensionTester(converted); + + await renderInTestApp(tester.reactElement(), { + mountedRoutes: { + '/': convertLegacyRouteRef(routeRef), + }, + }); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + + expect(tester.get(EntityContentBlueprint.dataRefs.filterExpression)).toBe( + undefined, + ); + expect(tester.get(EntityContentBlueprint.dataRefs.filterFunction)).toBe( + undefined, + ); + }); + + it('should convert an entity card extension with overrides', async () => { + const LegacyExtension = legacyPlugin.provide( + createRoutableExtension({ + name: 'EntityExampleCard', + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ); + + const converted = convertLegacyEntityCardExtension(LegacyExtension, { + name: 'other', + filter: 'my-filter', + }); + expect(converted.kind).toBe('entity-card'); + expect(converted.namespace).toBe(undefined); + expect(converted.name).toBe('other'); + + const tester = createExtensionTester(converted); + + await renderInTestApp(tester.reactElement(), { + mountedRoutes: { + '/': convertLegacyRouteRef(routeRef), + }, + }); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + + expect(tester.get(EntityContentBlueprint.dataRefs.filterExpression)).toBe( + 'my-filter', + ); + expect(tester.get(EntityContentBlueprint.dataRefs.filterFunction)).toBe( + undefined, + ); + }); + + it('should support various naming patterns for entity card extensions', async () => { + const withName = (name: string) => { + const converted = convertLegacyEntityCardExtension( + legacyPlugin.provide( + createRoutableExtension({ + name, + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ), + ); + return converted.name; + }; + + expect(withName('EntityTestCard')).toBe(undefined); + expect(withName('EntityTestTrimCard')).toBe('trim'); + expect(withName('EntityTeStTrimCard')).toBe('trim'); + expect(withName('EntityExampleCard')).toBe('example'); + expect(withName('EntityExAmpleCard')).toBe('ex-ample'); + expect(withName('ExampleCard')).toBe('example-card'); + }); +}); diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx index afe5211d1b..76442f526d 100644 --- a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityCardExtension.tsx @@ -19,6 +19,7 @@ import { BackstagePlugin, getComponentData } from '@backstage/core-plugin-api'; import { ExtensionDefinition } from '@backstage/frontend-plugin-api'; import React, { ComponentType } from 'react'; import { EntityCardBlueprint } from '../blueprints'; +import kebabCase from 'lodash/kebabCase'; /** @alpha */ export function convertLegacyEntityCardExtension( @@ -40,7 +41,7 @@ export function convertLegacyEntityCardExtension( const plugin = getComponentData(element, 'core.plugin'); const pluginId = plugin?.getId(); - const match = extName.match(/^Entity(.*)Content$/); + const match = extName.match(/^Entity(.*)Card$/); const infix = match?.[1] ?? extName; let name: string | undefined = infix; @@ -55,6 +56,7 @@ export function convertLegacyEntityCardExtension( name = undefined; } } + name = name && kebabCase(name); return EntityCardBlueprint.make({ name: overrides?.name ?? name, diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.test.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.test.tsx new file mode 100644 index 0000000000..6d7c8f0420 --- /dev/null +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.test.tsx @@ -0,0 +1,136 @@ +/* + * 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 { + createPlugin as createLegacyPlugin, + createRouteRef as createLegacyRouteRef, + createRoutableExtension, +} from '@backstage/core-plugin-api'; +import { coreExtensionData } from '@backstage/frontend-plugin-api'; +import { + createExtensionTester, + renderInTestApp, +} from '@backstage/frontend-test-utils'; +import { screen } from '@testing-library/react'; +import React from 'react'; +import { convertLegacyEntityContentExtension } from './convertLegacyEntityContentExtension'; +import { convertLegacyRouteRef } from '@backstage/core-compat-api'; +import { EntityContentBlueprint } from '../blueprints'; + +const routeRef = createLegacyRouteRef({ id: 'test' }); +const legacyPlugin = createLegacyPlugin({ + id: 'test', + routes: { + test: routeRef, + }, +}); + +describe('convertLegacyEntityContentExtension', () => { + it('should convert an entity content extension', async () => { + const LegacyExtension = legacyPlugin.provide( + createRoutableExtension({ + name: 'EntityExampleContent', + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ); + + const converted = convertLegacyEntityContentExtension(LegacyExtension); + expect(converted.kind).toBe('entity-content'); + expect(converted.namespace).toBe(undefined); + expect(converted.name).toBe('example'); + + const tester = createExtensionTester(converted); + + await renderInTestApp(tester.reactElement(), { + mountedRoutes: { + '/': convertLegacyRouteRef(routeRef), + }, + }); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + + expect(tester.get(coreExtensionData.routePath)).toBe('/example'); + expect(tester.get(coreExtensionData.routeRef)).toBe(routeRef); + expect(tester.get(EntityContentBlueprint.dataRefs.filterExpression)).toBe( + undefined, + ); + expect(tester.get(EntityContentBlueprint.dataRefs.filterFunction)).toBe( + undefined, + ); + }); + + it('should convert an entity content extension with overrides', async () => { + const LegacyExtension = legacyPlugin.provide( + createRoutableExtension({ + name: 'EntityExampleContent', + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ); + + const converted = convertLegacyEntityContentExtension(LegacyExtension, { + name: 'other', + defaultPath: '/other', + defaultTitle: 'Other', + filter: 'my-filter', + }); + expect(converted.kind).toBe('entity-content'); + expect(converted.namespace).toBe(undefined); + expect(converted.name).toBe('other'); + + const tester = createExtensionTester(converted); + + await renderInTestApp(tester.reactElement(), { + mountedRoutes: { + '/': convertLegacyRouteRef(routeRef), + }, + }); + + await expect(screen.findByText('Hello')).resolves.toBeInTheDocument(); + + expect(tester.get(coreExtensionData.routePath)).toBe('/other'); + expect(tester.get(coreExtensionData.routeRef)).toBe(routeRef); + expect(tester.get(EntityContentBlueprint.dataRefs.filterExpression)).toBe( + 'my-filter', + ); + expect(tester.get(EntityContentBlueprint.dataRefs.filterFunction)).toBe( + undefined, + ); + }); + + it('should support various naming patterns for entity content extensions', async () => { + const withName = (name: string) => { + const converted = convertLegacyEntityContentExtension( + legacyPlugin.provide( + createRoutableExtension({ + name, + mountPoint: routeRef, + component: async () => () =>
Hello
, + }), + ), + ); + return converted.name; + }; + + expect(withName('EntityTestContent')).toBe(undefined); + expect(withName('EntityTestTrimContent')).toBe('trim'); + expect(withName('EntityTeStTrimContent')).toBe('trim'); + expect(withName('EntityExampleContent')).toBe('example'); + expect(withName('EntityExAmpleContent')).toBe('ex-ample'); + expect(withName('ExampleContent')).toBe('example-content'); + }); +}); diff --git a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx index a44a6281da..e44b5671fc 100644 --- a/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx +++ b/plugins/catalog-react/src/alpha/converters/convertLegacyEntityContentExtension.tsx @@ -71,6 +71,7 @@ export function convertLegacyEntityContentExtension( name = undefined; } } + name = name && kebabCase(name); return EntityContentBlueprint.make({ name: overrides?.name ?? name, From c5cc0562e161dd6f56556675fd2b6955747b9566 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:46:23 +0200 Subject: [PATCH 11/13] app-next: add legacy conversion of TechDocs Signed-off-by: Patrik Oldsberg --- packages/app-next/src/App.tsx | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/packages/app-next/src/App.tsx b/packages/app-next/src/App.tsx index c41f33e01b..170decee42 100644 --- a/packages/app-next/src/App.tsx +++ b/packages/app-next/src/App.tsx @@ -29,7 +29,12 @@ import { createExtensionOverrides, ApiBlueprint, } from '@backstage/frontend-plugin-api'; -import techdocsPlugin from '@backstage/plugin-techdocs/alpha'; +import { + techdocsPlugin, + TechDocsIndexPage, + TechDocsReaderPage, + EntityTechdocsContent, +} from '@backstage/plugin-techdocs'; import appVisualizerPlugin from '@backstage/plugin-app-visualizer'; import { homePage } from './HomePage'; import { convertLegacyApp } from '@backstage/core-compat-api'; @@ -45,6 +50,9 @@ import { } from '@backstage/integration-react'; import kubernetesPlugin from '@backstage/plugin-kubernetes/alpha'; import { signInPageOverrides } from './overrides/SignInPage'; +import { convertLegacyPlugin } from '@backstage/core-compat-api'; +import { convertLegacyPageExtension } from '@backstage/core-compat-api'; +import { convertLegacyEntityContentExtension } from '@backstage/plugin-catalog-react/alpha'; /* @@ -75,6 +83,17 @@ TODO: /* app.tsx */ +const convertedTechdocsPlugin = convertLegacyPlugin(techdocsPlugin, { + extensions: [ + // TODO: We likely also need a way to convert an entire tree similar to collectLegacyRoutes + convertLegacyPageExtension(TechDocsIndexPage), + convertLegacyPageExtension(TechDocsReaderPage, { + defaultPath: '/docs/:namespace/:kind/:name/*', + }), + convertLegacyEntityContentExtension(EntityTechdocsContent), + ], +}); + const homePageExtension = createExtension({ name: 'myhomepage', attachTo: { id: 'page:home', input: 'props' }, @@ -114,7 +133,7 @@ const collectedLegacyPlugins = convertLegacyApp( const app = createApp({ features: [ pagesPlugin, - techdocsPlugin, + convertedTechdocsPlugin, userSettingsPlugin, homePlugin, appVisualizerPlugin, From eef894019440cda052f2a4853acc00e63e3b57e5 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:48:06 +0200 Subject: [PATCH 12/13] catalog-react: update API report Signed-off-by: Patrik Oldsberg --- plugins/catalog-react/api-report-alpha.md | 25 +++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/plugins/catalog-react/api-report-alpha.md b/plugins/catalog-react/api-report-alpha.md index 6f801ed45f..ffd33aa348 100644 --- a/plugins/catalog-react/api-report-alpha.md +++ b/plugins/catalog-react/api-report-alpha.md @@ -7,6 +7,7 @@ import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api'; import { AnyRouteRefParams } from '@backstage/frontend-plugin-api'; +import { ComponentType } from 'react'; import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api'; import { Entity } from '@backstage/catalog-model'; import { ExtensionBlueprint } from '@backstage/frontend-plugin-api'; @@ -95,6 +96,30 @@ export const catalogReactTranslationRef: TranslationRef< } >; +// @alpha (undocumented) +export function convertLegacyEntityCardExtension( + LegacyExtension: ComponentType<{}>, + overrides?: { + name?: string; + filter?: + | typeof EntityCardBlueprint.dataRefs.filterFunction.T + | typeof EntityCardBlueprint.dataRefs.filterExpression.T; + }, +): ExtensionDefinition; + +// @alpha (undocumented) +export function convertLegacyEntityContentExtension( + LegacyExtension: ComponentType<{}>, + overrides?: { + name?: string; + filter?: + | typeof EntityContentBlueprint.dataRefs.filterFunction.T + | typeof EntityContentBlueprint.dataRefs.filterExpression.T; + defaultPath?: string; + defaultTitle?: string; + }, +): ExtensionDefinition; + // @alpha @deprecated (undocumented) export function createEntityCardExtension< TConfig extends { From 519b8e059b39bfaf7024f35a9a24a8a0e16215d3 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Fri, 16 Aug 2024 14:51:40 +0200 Subject: [PATCH 13/13] changesets: add changesets for new legacy converters Signed-off-by: Patrik Oldsberg --- .changeset/silent-camels-walk.md | 5 +++++ .changeset/thirty-balloons-end.md | 5 +++++ 2 files changed, 10 insertions(+) create mode 100644 .changeset/silent-camels-walk.md create mode 100644 .changeset/thirty-balloons-end.md diff --git a/.changeset/silent-camels-walk.md b/.changeset/silent-camels-walk.md new file mode 100644 index 0000000000..244a615335 --- /dev/null +++ b/.changeset/silent-camels-walk.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': patch +--- + +Added utilities for converting existing entity card and content extensions to the new frontend system. This is in particular useful when used in combination with the new `convertLegacyPlugin` utility from `@backstage/core-compat-api`. diff --git a/.changeset/thirty-balloons-end.md b/.changeset/thirty-balloons-end.md new file mode 100644 index 0000000000..f6e252988e --- /dev/null +++ b/.changeset/thirty-balloons-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +Added new utilities for converting legacy plugins and extensions to the new system. The `convertLegacyPlugin` option will convert an existing plugin to the new system, although you need to supply extensions for the plugin yourself. To help out with this, there is also a new `convertLegacyPageExtension` which converts an existing page extension to the new system.