core-compat-api: add initial compatWrapper
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-compat-api': patch
|
||||
---
|
||||
|
||||
Added `compatWrapper`, which can be used to wrap any React element to provide bi-directional interoperability between the `@backstage/core-*-api` and `@backstage/frontend-*-api` APIs.
|
||||
@@ -13,6 +13,7 @@ import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
import { default as React_2 } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
import { RouteRef } from '@backstage/core-plugin-api';
|
||||
import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
import { SubRouteRef } from '@backstage/core-plugin-api';
|
||||
@@ -28,6 +29,9 @@ export function collectLegacyRoutes(
|
||||
flatRoutesElement: JSX.Element,
|
||||
): BackstagePlugin[];
|
||||
|
||||
// @public
|
||||
export function compatWrapper(element: ReactNode): React_2.JSX.Element;
|
||||
|
||||
// @public (undocumented)
|
||||
export function convertLegacyApp(
|
||||
rootElement: React_2.JSX.Element,
|
||||
|
||||
@@ -24,10 +24,12 @@
|
||||
},
|
||||
"devDependencies": {
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/frontend-test-utils": "workspace:^",
|
||||
"@backstage/plugin-puppetdb": "workspace:^",
|
||||
"@backstage/plugin-stackstorm": "workspace:^",
|
||||
"@oriflame/backstage-plugin-score-card": "^0.7.0",
|
||||
"@testing-library/jest-dom": "^6.0.0"
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
"@testing-library/react": "^14.0.0"
|
||||
},
|
||||
"files": [
|
||||
"dist"
|
||||
@@ -40,6 +42,7 @@
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/core-plugin-api": "workspace:^",
|
||||
"@backstage/frontend-plugin-api": "workspace:^",
|
||||
"@backstage/version-bridge": "workspace:^",
|
||||
"@types/react": "^16.13.1 || ^17.0.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* 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 React, { useMemo } from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { AppContextProvider } from '../../../core-app-api/src/app/AppContext';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
components as defaultComponents,
|
||||
icons as defaultIcons,
|
||||
} from '../../../app-defaults/src/defaults';
|
||||
import {
|
||||
BackstagePlugin as NewBackstagePlugin,
|
||||
appTreeApiRef,
|
||||
useApi,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
AppComponents,
|
||||
IconComponent,
|
||||
BackstagePlugin as LegacyBackstagePlugin,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { getOrCreateGlobalSingleton } from '@backstage/version-bridge';
|
||||
|
||||
// Make sure that we only convert each new plugin instance to its legacy equivalent once
|
||||
const legacyPluginStore = getOrCreateGlobalSingleton(
|
||||
'legacy-plugin-compatibility-store',
|
||||
() => new WeakMap<NewBackstagePlugin, LegacyBackstagePlugin>(),
|
||||
);
|
||||
|
||||
function toLegacyPlugin(plugin: NewBackstagePlugin): LegacyBackstagePlugin {
|
||||
let legacy = legacyPluginStore.get(plugin);
|
||||
if (legacy) {
|
||||
return legacy;
|
||||
}
|
||||
|
||||
const errorMsg = 'Not implemented in legacy plugin compatibility layer';
|
||||
const notImplemented = () => {
|
||||
throw new Error(errorMsg);
|
||||
};
|
||||
|
||||
legacy = {
|
||||
getId(): string {
|
||||
return plugin.id;
|
||||
},
|
||||
get routes() {
|
||||
return {};
|
||||
},
|
||||
get externalRoutes() {
|
||||
return {};
|
||||
},
|
||||
getApis: notImplemented,
|
||||
getFeatureFlags: notImplemented,
|
||||
provide: notImplemented,
|
||||
};
|
||||
|
||||
legacyPluginStore.set(plugin, legacy);
|
||||
return legacy;
|
||||
}
|
||||
|
||||
// Recreates the old AppContext APIs using the various new APIs that replaced it
|
||||
function LegacyAppContextProvider(props: { children: ReactNode }) {
|
||||
const appTreeApi = useApi(appTreeApiRef);
|
||||
|
||||
const appContext = useMemo(() => {
|
||||
const { tree } = appTreeApi.getTree();
|
||||
|
||||
let gatheredPlugins: LegacyBackstagePlugin[] | undefined = undefined;
|
||||
|
||||
return {
|
||||
getPlugins(): LegacyBackstagePlugin[] {
|
||||
if (gatheredPlugins) {
|
||||
return gatheredPlugins;
|
||||
}
|
||||
|
||||
const pluginSet = new Set<LegacyBackstagePlugin>();
|
||||
for (const node of tree.nodes.values()) {
|
||||
const plugin = node.spec.source;
|
||||
if (plugin) {
|
||||
pluginSet.add(toLegacyPlugin(plugin));
|
||||
}
|
||||
}
|
||||
gatheredPlugins = Array.from(pluginSet);
|
||||
|
||||
return gatheredPlugins;
|
||||
},
|
||||
|
||||
// TODO: Grab these from new API once it exists
|
||||
getSystemIcon(key: string): IconComponent | undefined {
|
||||
return key in defaultIcons
|
||||
? defaultIcons[key as keyof typeof defaultIcons]
|
||||
: undefined;
|
||||
},
|
||||
|
||||
// TODO: Grab these from new API once it exists
|
||||
getSystemIcons(): Record<string, IconComponent> {
|
||||
return defaultIcons;
|
||||
},
|
||||
|
||||
// TODO: Grab these from new API once it exists
|
||||
getComponents(): AppComponents {
|
||||
return defaultComponents;
|
||||
},
|
||||
};
|
||||
}, [appTreeApi]);
|
||||
|
||||
return (
|
||||
<AppContextProvider appContext={appContext}>
|
||||
{props.children}
|
||||
</AppContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function BackwardsCompatProvider(props: { children: ReactNode }) {
|
||||
return <LegacyAppContextProvider>{props.children}</LegacyAppContextProvider>;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { ReactNode } from 'react';
|
||||
|
||||
export function ForwardsCompatProvider(props: { children: ReactNode }) {
|
||||
// TODO(Rugvip): Implement
|
||||
return <>{props.children}</>;
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { compatWrapper } from './compatWrapper';
|
||||
import { useApp } from '@backstage/core-plugin-api';
|
||||
|
||||
describe('BackwardsCompatProvider', () => {
|
||||
it('should convert the app context', () => {
|
||||
// TODO(Rugvip): Replace with the new renderInTestApp once it's available, and have some plugins
|
||||
createExtensionTester(
|
||||
createExtension({
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory() {
|
||||
function Component() {
|
||||
const app = useApp();
|
||||
return (
|
||||
<div data-testid="ctx">
|
||||
plugins:
|
||||
{app
|
||||
.getPlugins()
|
||||
.map(p => p.getId())
|
||||
.join(', ')}
|
||||
{'\n'}
|
||||
components: {Object.keys(app.getComponents()).join(', ')}
|
||||
{'\n'}
|
||||
icons: {Object.keys(app.getSystemIcons()).join(', ')}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
element: compatWrapper(<Component />),
|
||||
};
|
||||
},
|
||||
}),
|
||||
).render();
|
||||
|
||||
expect(screen.getByTestId('ctx').textContent).toMatchInlineSnapshot(`
|
||||
"plugins:
|
||||
components: Progress, Router, NotFoundErrorPage, BootErrorPage, ErrorBoundaryFallback
|
||||
icons: brokenImage, catalog, scaffolder, techdocs, search, chat, dashboard, docs, email, github, group, help, kind:api, kind:component, kind:domain, kind:group, kind:location, kind:system, kind:user, kind:resource, kind:template, user, warning"
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* 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 React from 'react';
|
||||
import { useVersionedContext } from '@backstage/version-bridge';
|
||||
import { ReactNode } from 'react';
|
||||
import { BackwardsCompatProvider } from './BackwardsCompatProvider';
|
||||
import { ForwardsCompatProvider } from './ForwardsCompatProvider';
|
||||
|
||||
function BidirectionalCompatProvider(props: { children: ReactNode }) {
|
||||
const isInNewApp = !useVersionedContext<{ 1: unknown }>('app-context');
|
||||
|
||||
if (isInNewApp) {
|
||||
return <BackwardsCompatProvider {...props} />;
|
||||
}
|
||||
|
||||
return <ForwardsCompatProvider {...props} />;
|
||||
}
|
||||
|
||||
/**
|
||||
* Wraps a React element in a bidirectional compatibility provider, allow APIs
|
||||
* from `@backstage/core-plugin-api` to be used in an app from `@backstage/frontend-app-api`,
|
||||
* and APIs from `@backstage/frontend-plugin-api` to be used in an app from `@backstage/core-app-api`.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export function compatWrapper(element: ReactNode) {
|
||||
return <BidirectionalCompatProvider>{element}</BidirectionalCompatProvider>;
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
|
||||
export { compatWrapper } from './compatWrapper';
|
||||
@@ -13,6 +13,8 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './compatWrapper';
|
||||
|
||||
export { collectLegacyRoutes } from './collectLegacyRoutes';
|
||||
export { collectLegacyComponents } from './collectLegacyComponents';
|
||||
export { convertLegacyApp } from './convertLegacyApp';
|
||||
|
||||
@@ -3851,10 +3851,13 @@ __metadata:
|
||||
"@backstage/core-app-api": "workspace:^"
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/frontend-test-utils": "workspace:^"
|
||||
"@backstage/plugin-puppetdb": "workspace:^"
|
||||
"@backstage/plugin-stackstorm": "workspace:^"
|
||||
"@backstage/version-bridge": "workspace:^"
|
||||
"@oriflame/backstage-plugin-score-card": ^0.7.0
|
||||
"@testing-library/jest-dom": ^6.0.0
|
||||
"@testing-library/react": ^14.0.0
|
||||
"@types/react": ^16.13.1 || ^17.0.0
|
||||
peerDependencies:
|
||||
react: ^16.13.1 || ^17.0.0 || ^18.0.0
|
||||
|
||||
Reference in New Issue
Block a user