From 08528670dc83cfa13678f0784bbd3a4024cd3fbe Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 3 Aug 2024 12:42:46 +0200 Subject: [PATCH 1/5] core-compat-api: add forwards compat for app context Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/package.json | 1 + .../compatWrapper/BackwardsCompatProvider.tsx | 4 +- .../compatWrapper/ForwardsCompatProvider.tsx | 102 +++++++++++++++++- .../src/compatWrapper/compatWrapper.test.tsx | 47 ++++++-- yarn.lock | 1 + 5 files changed, 145 insertions(+), 10 deletions(-) diff --git a/packages/core-compat-api/package.json b/packages/core-compat-api/package.json index a76f7878dd..4a143a50c7 100644 --- a/packages/core-compat-api/package.json +++ b/packages/core-compat-api/package.json @@ -44,6 +44,7 @@ "@backstage/frontend-app-api": "workspace:^", "@backstage/frontend-test-utils": "workspace:^", "@backstage/plugin-catalog": "workspace:^", + "@backstage/test-utils": "workspace:^", "@oriflame/backstage-plugin-score-card": "^0.8.0", "@testing-library/jest-dom": "^6.0.0", "@testing-library/react": "^15.0.0" diff --git a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx index 288f3f900b..4f9ba3a836 100644 --- a/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/BackwardsCompatProvider.tsx @@ -50,7 +50,9 @@ const legacyPluginStore = getOrCreateGlobalSingleton( () => new WeakMap(), ); -function toLegacyPlugin(plugin: NewBackstagePlugin): LegacyBackstagePlugin { +export function toLegacyPlugin( + plugin: NewBackstagePlugin, +): LegacyBackstagePlugin { let legacy = legacyPluginStore.get(plugin); if (legacy) { return legacy; diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index 5544862aee..182b774cca 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -14,10 +14,106 @@ * limitations under the License. */ -import React from 'react'; +import { + ApiHolder, + ApiRef, + AppContext, + useApp, +} from '@backstage/core-plugin-api'; +import { + ComponentRef, + ComponentsApi, + CoreErrorBoundaryFallbackProps, + CoreNotFoundErrorPageProps, + CoreProgressProps, + IconComponent, + IconsApi, + componentsApiRef, + coreComponentRefs, + iconsApiRef, +} from '@backstage/frontend-plugin-api'; +import React, { ComponentType, useMemo } from 'react'; import { ReactNode } from 'react'; +import { toLegacyPlugin } from './BackwardsCompatProvider'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { ApiProvider } from '../../../core-app-api/src/apis/system/ApiProvider'; + +class CompatComponentsApi implements ComponentsApi { + readonly #Progress: ComponentType; + readonly #NotFoundErrorPage: ComponentType; + readonly #ErrorBoundaryFallback: ComponentType; + + constructor(app: AppContext) { + const components = app.getComponents(); + const ErrorBoundaryFallback = (props: CoreErrorBoundaryFallbackProps) => ( + + ); + this.#Progress = components.Progress; + this.#NotFoundErrorPage = components.NotFoundErrorPage; + this.#ErrorBoundaryFallback = ErrorBoundaryFallback; + } + + getComponent(ref: ComponentRef): ComponentType { + switch (ref.id) { + case coreComponentRefs.progress.id: + return this.#Progress as ComponentType; + case coreComponentRefs.notFoundErrorPage.id: + return this.#NotFoundErrorPage as ComponentType; + case coreComponentRefs.errorBoundaryFallback.id: + return this.#ErrorBoundaryFallback as ComponentType; + default: + throw new Error( + `No backwards compatible component is available for ref '${ref.id}'`, + ); + } + } +} + +class CompatIconsApi implements IconsApi { + readonly #app: AppContext; + + constructor(app: AppContext) { + this.#app = app; + } + + getIcon(key: string): IconComponent | undefined { + return this.#app.getSystemIcon(key); + } + + listIconKeys(): string[] { + return Object.keys(this.#app.getSystemIcons()); + } +} + +class AppFallbackApis implements ApiHolder { + readonly #componentsApi: ComponentsApi; + readonly #iconsApi: IconsApi; + + constructor(app: AppContext) { + this.#componentsApi = new CompatComponentsApi(app); + this.#iconsApi = new CompatIconsApi(app); + } + + get(ref: ApiRef): T | undefined { + if (ref.id === componentsApiRef.id) { + return this.#componentsApi as T; + } else if (ref.id === iconsApiRef.id) { + return this.#iconsApi as T; + } + return undefined; + } +} + +function NewAppApisProvider(props: { children: ReactNode }) { + const app = useApp(); + const appFallbackApis = useMemo(() => new AppFallbackApis(app), [app]); + + return {props.children}; +} export function ForwardsCompatProvider(props: { children: ReactNode }) { - // TODO(Rugvip): Implement - return <>{props.children}; + return {props.children}; } diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index 74b9f209eb..b17ca2d508 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -16,21 +16,27 @@ import React from 'react'; import { + componentsApiRef, + coreComponentRefs, coreExtensionData, createExtension, + iconsApiRef, + useRouteRef as useNewRouteRef, + useApi, } from '@backstage/frontend-plugin-api'; import { createExtensionTester, - renderInTestApp, + renderInTestApp as renderInNewTestApp, } from '@backstage/frontend-test-utils'; import { screen } from '@testing-library/react'; import { compatWrapper } from './compatWrapper'; import { - createRouteRef, useApp, - useRouteRef, + useRouteRef as useOldRouteRef, + createRouteRef as createOldRouteRef, } from '@backstage/core-plugin-api'; import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; +import { renderInTestApp as renderInOldTestApp } from '@backstage/test-utils'; describe('BackwardsCompatProvider', () => { it('should convert the app context', () => { @@ -74,17 +80,46 @@ describe('BackwardsCompatProvider', () => { }); it('should convert the routing context', () => { - const routeRef = createRouteRef({ id: 'test' }); + const routeRef = createOldRouteRef({ id: 'test' }); function Component() { - const link = useRouteRef(routeRef); + const link = useOldRouteRef(routeRef); return
link: {link()}
; } - renderInTestApp(compatWrapper(), { + renderInNewTestApp(compatWrapper(), { mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, }); expect(screen.getByText('link: /test')).toBeInTheDocument(); }); }); + +describe('ForwardsCompatProvider', () => { + it('should convert the app context', async () => { + function Component() { + const components = useApi(componentsApiRef); + const icons = useApi(iconsApiRef); + return ( +
+ components:{' '} + {Object.entries(coreComponentRefs) + .map( + ([name, ref]) => + `${name}=${Boolean(components.getComponent(ref))}`, + ) + .join(', ')} + {'\n'} + icons: {icons.listIconKeys().join(', ')} +
+ ); + } + + await renderInOldTestApp(compatWrapper()); + + expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(` + "components: progress=true, notFoundErrorPage=true, errorBoundaryFallback=true + icons: kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, user, warning" + `); + }); +}); diff --git a/yarn.lock b/yarn.lock index c1dc608e87..e26ceb0aa6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -4171,6 +4171,7 @@ __metadata: "@backstage/frontend-plugin-api": "workspace:^" "@backstage/frontend-test-utils": "workspace:^" "@backstage/plugin-catalog": "workspace:^" + "@backstage/test-utils": "workspace:^" "@backstage/version-bridge": "workspace:^" "@oriflame/backstage-plugin-score-card": ^0.8.0 "@testing-library/jest-dom": ^6.0.0 From 9ade0f99e5277113a8628eaef21cd90d138d8d62 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sat, 3 Aug 2024 16:01:32 +0200 Subject: [PATCH 2/5] core-compat-api: add test for convertLegacyRouteRef Signed-off-by: Patrik Oldsberg --- .../src/convertLegacyRouteRef.test.ts | 119 ++++++++++++++++++ 1 file changed, 119 insertions(+) create mode 100644 packages/core-compat-api/src/convertLegacyRouteRef.test.ts diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts new file mode 100644 index 0000000000..3681d0d2d9 --- /dev/null +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -0,0 +1,119 @@ +/* + * Copyright 2023 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 { + createRouteRef as createOldRouteRef, + createSubRouteRef as createOldSubRouteRef, + createExternalRouteRef as createOldExternalRouteRef, +} from '@backstage/core-plugin-api'; +import { + RouteRef as NewRouteRef, + SubRouteRef as NewSubRouteRef, + ExternalRouteRef as NewExternalRouteRef, +} from '@backstage/frontend-plugin-api'; +import { convertLegacyRouteRef } from './convertLegacyRouteRef'; + +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalRouteRef as toInternalNewRouteRef } from '../../frontend-plugin-api/src/routing/RouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalSubRouteRef as toInternalNewSubRouteRef } from '../../frontend-plugin-api/src/routing/SubRouteRef'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { toInternalExternalRouteRef as toInternalNewExternalRouteRef } from '../../frontend-plugin-api/src/routing/ExternalRouteRef'; + +describe('convertLegacyRouteRef', () => { + it('converts old to new', () => { + const ref1 = createOldRouteRef({ id: 'ref1' }); + const ref2 = createOldRouteRef({ id: 'ref2', params: ['p1', 'p2'] }); + const ref1sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createOldSubRouteRef({ + id: 'sub2', + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createOldSubRouteRef({ + id: 'sub1', + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createOldExternalRouteRef({ + id: 'ref3', + }); + const ref4 = createOldExternalRouteRef({ + id: 'ref4', + optional: true, + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: NewRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: NewRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: NewSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: NewSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: NewExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: NewExternalRouteRef = convertLegacyRouteRef(ref4); + + const ref1Internal = toInternalNewRouteRef(ref1Converted); + const ref2Internal = toInternalNewRouteRef(ref2Converted); + const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted); + const ref1sub2Internal = toInternalNewSubRouteRef(ref1sub2Converted); + const ref2sub1Internal = toInternalNewSubRouteRef(ref2sub1Converted); + const ref3Internal = toInternalNewExternalRouteRef(ref3Converted); + const ref4Internal = toInternalNewExternalRouteRef(ref4Converted); + + expect(ref1Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref1}', + ); + expect(ref1Internal.getParams()).toEqual([]); + expect(ref2Internal.getDescription()).toBe( + 'routeRef{type=absolute,id=ref2}', + ); + expect(ref2Internal.getParams()).toEqual(['p1', 'p2']); + + expect(ref1sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref1sub1Internal.getParams()).toEqual([]); + expect(ref1sub1Internal.getParent()).toBe(ref1); + expect(ref1sub2Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub2}', + ); + expect(ref1sub2Internal.getParams()).toEqual(['p3']); + expect(ref1sub2Internal.getParent()).toBe(ref1); + expect(ref2sub1Internal.getDescription()).toBe( + 'routeRef{type=sub,id=sub1}', + ); + expect(ref2sub1Internal.getParams()).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Internal.getParent()).toBe(ref2); + + expect(ref3Internal.getDefaultTarget()).toBe(undefined); + expect(ref3Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref3}', + ); + expect(ref3Internal.getParams()).toEqual([]); + expect(ref3Internal.optional).toBe(false); + expect(ref4Internal.getDefaultTarget()).toBe('ref2'); + expect(ref4Internal.getDescription()).toBe( + 'routeRef{type=external,id=ref4}', + ); + expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); + expect(ref4Internal.optional).toBe(true); + }); +}); From ef1b2f4033058c0a0842bbbfa865121e584fba9d Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 11:28:38 +0200 Subject: [PATCH 3/5] core-compat-api: add support for converting new route refs to legacy Signed-off-by: Patrik Oldsberg --- packages/core-compat-api/api-report.md | 18 +++ .../src/convertLegacyRouteRef.test.ts | 87 ++++++++++++++ .../src/convertLegacyRouteRef.ts | 107 +++++++++++++++++- 3 files changed, 207 insertions(+), 5 deletions(-) diff --git a/packages/core-compat-api/api-report.md b/packages/core-compat-api/api-report.md index be80ca50e9..3cd78e6f30 100644 --- a/packages/core-compat-api/api-report.md +++ b/packages/core-compat-api/api-report.md @@ -44,6 +44,24 @@ export function convertLegacyRouteRef< ref: ExternalRouteRef, ): ExternalRouteRef_2; +// @public +export function convertLegacyRouteRef( + ref: RouteRef_2, +): RouteRef; + +// @public +export function convertLegacyRouteRef( + ref: SubRouteRef_2, +): SubRouteRef; + +// @public +export function convertLegacyRouteRef< + TParams extends AnyRouteRefParams, + TOptional extends boolean, +>( + ref: ExternalRouteRef_2, +): ExternalRouteRef; + // @public export function convertLegacyRouteRefs< TRefs extends { diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts index 3681d0d2d9..bf82d1e034 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.test.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.test.ts @@ -15,6 +15,9 @@ */ import { + RouteRef as OldRouteRef, + SubRouteRef as OldSubRouteRef, + ExternalRouteRef as OldExternalRouteRef, createRouteRef as createOldRouteRef, createSubRouteRef as createOldSubRouteRef, createExternalRouteRef as createOldExternalRouteRef, @@ -23,6 +26,9 @@ import { RouteRef as NewRouteRef, SubRouteRef as NewSubRouteRef, ExternalRouteRef as NewExternalRouteRef, + createRouteRef as createNewRouteRef, + createSubRouteRef as createNewSubRouteRef, + createExternalRouteRef as createNewExternalRouteRef, } from '@backstage/frontend-plugin-api'; import { convertLegacyRouteRef } from './convertLegacyRouteRef'; @@ -70,6 +76,15 @@ describe('convertLegacyRouteRef', () => { const ref3Converted: NewExternalRouteRef = convertLegacyRouteRef(ref3); const ref4Converted: NewExternalRouteRef = convertLegacyRouteRef(ref4); + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + const ref1Internal = toInternalNewRouteRef(ref1Converted); const ref2Internal = toInternalNewRouteRef(ref2Converted); const ref1sub1Internal = toInternalNewSubRouteRef(ref1sub1Converted); @@ -116,4 +131,76 @@ describe('convertLegacyRouteRef', () => { expect(ref4Internal.getParams()).toEqual(['p1', 'p2']); expect(ref4Internal.optional).toBe(true); }); + + it('converts new to old', () => { + const ref1 = createNewRouteRef(); + const ref2 = createNewRouteRef({ params: ['p1', 'p2'] }); + const ref1sub1 = createNewSubRouteRef({ + parent: ref1, + path: '/sub1', + }); + const ref1sub2 = createNewSubRouteRef({ + parent: ref1, + path: '/sub2/:p3', + }); + const ref2sub1 = createNewSubRouteRef({ + parent: ref2, + path: '/sub1/:p3', + }); + const ref3 = createNewExternalRouteRef(); + const ref4 = createNewExternalRouteRef({ + optional: true, + defaultTarget: 'ref2', + params: ['p1', 'p2'], + }); + + const ref1Converted: OldRouteRef = convertLegacyRouteRef(ref1); + const ref2Converted: OldRouteRef = convertLegacyRouteRef(ref2); + const ref1sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub1); + const ref1sub2Converted: OldSubRouteRef = convertLegacyRouteRef(ref1sub2); + const ref2sub1Converted: OldSubRouteRef = convertLegacyRouteRef(ref2sub1); + const ref3Converted: OldExternalRouteRef = convertLegacyRouteRef(ref3); + const ref4Converted: OldExternalRouteRef = convertLegacyRouteRef(ref4); + + // Check for reference equality + expect(ref1).toBe(ref1Converted); + expect(ref2).toBe(ref2Converted); + expect(ref1sub1).toBe(ref1sub1Converted); + expect(ref1sub2).toBe(ref1sub2Converted); + expect(ref2sub1).toBe(ref2sub1Converted); + expect(ref3).toBe(ref3Converted); + expect(ref4).toBe(ref4Converted); + + expect(String(ref1Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref1Converted.params).toEqual([]); + expect(String(ref2Converted)).toMatch(/^RouteRef\{created at '.*'\}$/); + expect(ref2Converted.params).toEqual(['p1', 'p2']); + + expect(String(ref1sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1 with parent created at '.*'\}$/, + ); + expect(ref1sub1Converted.params).toEqual([]); + expect(ref1sub1Converted.parent).toBe(ref1); + expect(String(ref1sub2Converted)).toMatch( + /^SubRouteRef\{at \/sub2\/:p3 with parent created at '.*'\}$/, + ); + expect(ref1sub2Converted.params).toEqual(['p3']); + expect(ref1sub2Converted.parent).toBe(ref1); + expect(String(ref2sub1Converted)).toMatch( + /^SubRouteRef\{at \/sub1\/:p3 with parent created at '.*'\}$/, + ); + expect(ref2sub1Converted.params).toEqual(['p1', 'p2', 'p3']); + expect(ref2sub1Converted.parent).toBe(ref2); + + expect(String(ref3Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref3Converted.params).toEqual([]); + expect(ref3Converted.optional).toBe(false); + expect(String(ref4Converted)).toMatch( + /^ExternalRouteRef\{created at '.*'\}$/, + ); + expect(ref4Converted.params).toEqual(['p1', 'p2']); + expect(ref4Converted.optional).toBe(true); + }); }); diff --git a/packages/core-compat-api/src/convertLegacyRouteRef.ts b/packages/core-compat-api/src/convertLegacyRouteRef.ts index c967f9e9b5..24c589bb87 100644 --- a/packages/core-compat-api/src/convertLegacyRouteRef.ts +++ b/packages/core-compat-api/src/convertLegacyRouteRef.ts @@ -116,16 +116,113 @@ export function convertLegacyRouteRef< ref: LegacyExternalRouteRef, ): ExternalRouteRef; +/** + * A temporary helper to convert a new route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: RouteRef, +): LegacyRouteRef; + +/** + * A temporary helper to convert a new sub route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createSubRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef( + ref: SubRouteRef, +): LegacySubRouteRef; + +/** + * A temporary helper to convert a new external route ref to the legacy system. + * + * @public + * @remarks + * + * In the future the legacy createExternalRouteRef will instead create refs compatible with both systems. + */ +export function convertLegacyRouteRef< + TParams extends AnyRouteRefParams, + TOptional extends boolean, +>( + ref: ExternalRouteRef, +): LegacyExternalRouteRef; export function convertLegacyRouteRef( - ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, -): RouteRef | SubRouteRef | ExternalRouteRef { + ref: + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef + | RouteRef + | SubRouteRef + | ExternalRouteRef, +): + | RouteRef + | SubRouteRef + | ExternalRouteRef + | LegacyRouteRef + | LegacySubRouteRef + | LegacyExternalRouteRef { + const isNew = '$$type' in ref; + const oldType = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + // Ref has already been converted - if ('$$type' in ref) { - return ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef; + if (isNew && oldType) { + return ref as any; } - const type = (ref as unknown as { [routeRefType]: unknown })[routeRefType]; + if (isNew) { + return convertNewToOld( + ref as unknown as RouteRef | SubRouteRef | ExternalRouteRef, + ); + } + return convertOldToNew(ref, oldType); +} + +function convertNewToOld( + ref: RouteRef | SubRouteRef | ExternalRouteRef, +): LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef { + if (ref.$$type === '@backstage/RouteRef') { + const newRef = toInternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'absolute', + params: newRef.getParams(), + title: newRef.getDescription(), + } as Omit) as unknown as LegacyRouteRef; + } + if (ref.$$type === '@backstage/SubRouteRef') { + const newRef = toInternalSubRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'sub', + parent: convertLegacyRouteRef(newRef.getParent()), + params: newRef.getParams(), + } as Omit) as unknown as LegacySubRouteRef; + } + if (ref.$$type === '@backstage/ExternalRouteRef') { + const newRef = toInternalExternalRouteRef(ref); + return Object.assign(ref, { + [routeRefType]: 'external', + params: newRef.getParams(), + defaultTarget: newRef.getDefaultTarget(), + } as Omit) as unknown as LegacyExternalRouteRef; + } + + throw new Error( + `Failed to convert route ref, unknown type '${(ref as any).$$type}'`, + ); +} + +function convertOldToNew( + ref: LegacyRouteRef | LegacySubRouteRef | LegacyExternalRouteRef, + type: unknown, +): RouteRef | SubRouteRef | ExternalRouteRef { if (type === 'absolute') { const legacyRef = ref as LegacyRouteRef; const legacyRefStr = String(legacyRef); From f8e2383016dc5d42719d4d8d67f573b604897388 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 11:40:32 +0200 Subject: [PATCH 4/5] core-compat-api: add forwards compat for routing context Signed-off-by: Patrik Oldsberg --- .../compatWrapper/ForwardsCompatProvider.tsx | 55 ++++++++++++++++++- .../src/compatWrapper/compatWrapper.test.tsx | 16 ++++++ 2 files changed, 68 insertions(+), 3 deletions(-) diff --git a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx index 182b774cca..b2200387cc 100644 --- a/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx +++ b/packages/core-compat-api/src/compatWrapper/ForwardsCompatProvider.tsx @@ -21,22 +21,34 @@ import { useApp, } from '@backstage/core-plugin-api'; import { + AnyRouteRefParams, ComponentRef, ComponentsApi, CoreErrorBoundaryFallbackProps, CoreNotFoundErrorPageProps, CoreProgressProps, + ExternalRouteRef, IconComponent, IconsApi, + RouteFunc, + RouteRef, + RouteResolutionApi, + RouteResolutionApiResolveOptions, + SubRouteRef, componentsApiRef, coreComponentRefs, iconsApiRef, + routeResolutionApiRef, } from '@backstage/frontend-plugin-api'; import React, { ComponentType, useMemo } from 'react'; import { ReactNode } from 'react'; import { toLegacyPlugin } from './BackwardsCompatProvider'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { ApiProvider } from '../../../core-app-api/src/apis/system/ApiProvider'; +import { useVersionedContext } from '@backstage/version-bridge'; +// eslint-disable-next-line @backstage/no-relative-monorepo-imports +import { type RouteResolver } from '../../../core-plugin-api/src/routing/useRouteRef'; +import { convertLegacyRouteRef } from '../convertLegacyRouteRef'; class CompatComponentsApi implements ComponentsApi { readonly #Progress: ComponentType; @@ -88,13 +100,34 @@ class CompatIconsApi implements IconsApi { } } -class AppFallbackApis implements ApiHolder { +class CompatRouteResolutionApi implements RouteResolutionApi { + readonly #routeResolver: RouteResolver; + + constructor(routeResolver: RouteResolver) { + this.#routeResolver = routeResolver; + } + + resolve( + anyRouteRef: + | RouteRef + | SubRouteRef + | ExternalRouteRef, + options?: RouteResolutionApiResolveOptions | undefined, + ): RouteFunc | undefined { + const legacyRef = convertLegacyRouteRef(anyRouteRef as RouteRef); + return this.#routeResolver.resolve(legacyRef, options?.sourcePath ?? '/'); + } +} + +class ForwardsCompatApis implements ApiHolder { readonly #componentsApi: ComponentsApi; readonly #iconsApi: IconsApi; + readonly #routeResolutionApi: RouteResolutionApi; - constructor(app: AppContext) { + constructor(app: AppContext, routeResolver: RouteResolver) { this.#componentsApi = new CompatComponentsApi(app); this.#iconsApi = new CompatIconsApi(app); + this.#routeResolutionApi = new CompatRouteResolutionApi(routeResolver); } get(ref: ApiRef): T | undefined { @@ -102,6 +135,8 @@ class AppFallbackApis implements ApiHolder { return this.#componentsApi as T; } else if (ref.id === iconsApiRef.id) { return this.#iconsApi as T; + } else if (ref.id === routeResolutionApiRef.id) { + return this.#routeResolutionApi as T; } return undefined; } @@ -109,7 +144,21 @@ class AppFallbackApis implements ApiHolder { function NewAppApisProvider(props: { children: ReactNode }) { const app = useApp(); - const appFallbackApis = useMemo(() => new AppFallbackApis(app), [app]); + const versionedRouteResolverContext = useVersionedContext<{ + 1: RouteResolver; + }>('routing-context'); + if (!versionedRouteResolverContext) { + throw new Error('Routing context is not available'); + } + const routeResolver = versionedRouteResolverContext.atVersion(1); + if (!routeResolver) { + throw new Error('RoutingContext v1 not available'); + } + + const appFallbackApis = useMemo( + () => new ForwardsCompatApis(app, routeResolver), + [app, routeResolver], + ); return {props.children}; } diff --git a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx index b17ca2d508..6a27bf19e8 100644 --- a/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx +++ b/packages/core-compat-api/src/compatWrapper/compatWrapper.test.tsx @@ -22,6 +22,7 @@ import { createExtension, iconsApiRef, useRouteRef as useNewRouteRef, + createRouteRef as createNewRouteRef, useApi, } from '@backstage/frontend-plugin-api'; import { @@ -122,4 +123,19 @@ describe('ForwardsCompatProvider', () => { icons: kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, user, warning" `); }); + + it('should convert the routing context', async () => { + const routeRef = createNewRouteRef(); + + function Component() { + const link = useNewRouteRef(routeRef); + return
link: {link()}
; + } + + await renderInOldTestApp(compatWrapper(), { + mountedRoutes: { '/test': convertLegacyRouteRef(routeRef) }, + }); + + expect(screen.getByText('link: /test')).toBeInTheDocument(); + }); }); From 16cf96c28a5cccc5b0446f7a20efd9004c86e1d8 Mon Sep 17 00:00:00 2001 From: Patrik Oldsberg Date: Sun, 4 Aug 2024 11:48:55 +0200 Subject: [PATCH 5/5] changesets: add changeset for new -> old compat Signed-off-by: Patrik Oldsberg --- .changeset/slow-ducks-rush.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/slow-ducks-rush.md diff --git a/.changeset/slow-ducks-rush.md b/.changeset/slow-ducks-rush.md new file mode 100644 index 0000000000..c09f883e9c --- /dev/null +++ b/.changeset/slow-ducks-rush.md @@ -0,0 +1,5 @@ +--- +'@backstage/core-compat-api': patch +--- + +Both `compatWrapper` and `convertLegacyRouteRef` now support converting from the new system to the old.