core-compat-api: tests and fixes for entity page conversion
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -0,0 +1,171 @@
|
||||
/*
|
||||
* Copyright 2025 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 { ExtensionAttachToSpec } from '@backstage/frontend-plugin-api';
|
||||
import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog';
|
||||
import React from 'react';
|
||||
import { collectEntityPageContents } from './collectEntityPageContents';
|
||||
import {
|
||||
createComponentExtension,
|
||||
createPlugin,
|
||||
} from '@backstage/core-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
resolveExtensionDefinition,
|
||||
toInternalExtension,
|
||||
} from '../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
|
||||
const fooPlugin = createPlugin({
|
||||
id: 'foo',
|
||||
});
|
||||
|
||||
const FooContent = fooPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'FooContent',
|
||||
component: { sync: () => <div>foo content</div> },
|
||||
}),
|
||||
);
|
||||
const OtherFooContent = fooPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'OtherFooContent',
|
||||
component: { sync: () => <div>other foo content</div> },
|
||||
}),
|
||||
);
|
||||
|
||||
const simpleTestContent = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<div>overview content</div>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/foo" title="Foo">
|
||||
<FooContent />
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/bar" title="Bar">
|
||||
<div>bar content</div>
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
);
|
||||
|
||||
const otherTestContent = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<div>other overview content</div>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/foo" title="Foo">
|
||||
<OtherFooContent />
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
);
|
||||
|
||||
function collect(element: React.JSX.Element) {
|
||||
const result = new Array<{
|
||||
id: string;
|
||||
attachTo: ExtensionAttachToSpec;
|
||||
}>();
|
||||
|
||||
collectEntityPageContents(element, {
|
||||
discoverExtension(extension, plugin) {
|
||||
const ext = toInternalExtension(
|
||||
resolveExtensionDefinition(extension, {
|
||||
namespace: plugin?.getId() ?? 'test',
|
||||
}),
|
||||
);
|
||||
result.push({ id: ext.id, attachTo: ext.attachTo });
|
||||
},
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
describe('collectEntityPageContents', () => {
|
||||
it('should collect contents from a simple entity page', () => {
|
||||
expect(collect(simpleTestContent)).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "entity-content:catalog/overview",
|
||||
"input": "cards",
|
||||
},
|
||||
"id": "entity-card:test/discovered-1",
|
||||
},
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "page:catalog/entity",
|
||||
"input": "contents",
|
||||
},
|
||||
"id": "entity-content:foo/discovered-1",
|
||||
},
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "page:catalog/entity",
|
||||
"input": "contents",
|
||||
},
|
||||
"id": "entity-content:test/discovered-2",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
|
||||
it('should collect contents from an entity page with an entity switch', () => {
|
||||
expect(
|
||||
collect(
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isKind('test')}>
|
||||
{simpleTestContent}
|
||||
</EntitySwitch.Case>
|
||||
<EntitySwitch.Case>{otherTestContent}</EntitySwitch.Case>
|
||||
</EntitySwitch>,
|
||||
),
|
||||
).toMatchInlineSnapshot(`
|
||||
[
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "entity-content:catalog/overview",
|
||||
"input": "cards",
|
||||
},
|
||||
"id": "entity-card:test/discovered-1",
|
||||
},
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "page:catalog/entity",
|
||||
"input": "contents",
|
||||
},
|
||||
"id": "entity-content:foo/discovered-1",
|
||||
},
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "page:catalog/entity",
|
||||
"input": "contents",
|
||||
},
|
||||
"id": "entity-content:test/discovered-2",
|
||||
},
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "entity-content:catalog/overview",
|
||||
"input": "cards",
|
||||
},
|
||||
"id": "entity-card:test/discovered-2",
|
||||
},
|
||||
{
|
||||
"attachTo": {
|
||||
"id": "page:catalog/entity",
|
||||
"input": "contents",
|
||||
},
|
||||
"id": "entity-content:foo/discovered-3",
|
||||
},
|
||||
]
|
||||
`);
|
||||
});
|
||||
});
|
||||
@@ -74,7 +74,7 @@ function invertFilter(ifFunc?: EntityFilter): EntityFilter {
|
||||
export function collectEntityPageContents(
|
||||
entityPageElement: React.JSX.Element,
|
||||
context: {
|
||||
discoveryExtension(
|
||||
discoverExtension(
|
||||
extension: ExtensionDefinition,
|
||||
plugin?: LegacyBackstagePlugin,
|
||||
): void;
|
||||
@@ -94,7 +94,7 @@ export function collectEntityPageContents(
|
||||
const mergedIf = allFilters(parentFilter, pageNode.if);
|
||||
|
||||
if (pageNode.path === '/') {
|
||||
context.discoveryExtension(
|
||||
context.discoverExtension(
|
||||
EntityCardBlueprint.makeWithOverrides({
|
||||
name: `discovered-${cardCounter++}`,
|
||||
factory(originalFactory, { apis }) {
|
||||
@@ -109,7 +109,7 @@ export function collectEntityPageContents(
|
||||
} else {
|
||||
const name = `discovered-${routeCounter++}`;
|
||||
|
||||
context.discoveryExtension(
|
||||
context.discoverExtension(
|
||||
EntityContentBlueprint.makeWithOverrides({
|
||||
name,
|
||||
factory(originalFactory, { apis }) {
|
||||
|
||||
@@ -57,7 +57,7 @@ describe('collectLegacyRoutes', () => {
|
||||
|
||||
expect(
|
||||
collected.map(p => ({
|
||||
id: p.id,
|
||||
id: p.$$type === '@backstage/FrontendPlugin' ? p.id : p.pluginId,
|
||||
extensions: OpaqueFrontendPlugin.toInternal(p).extensions.map(e => ({
|
||||
id: e.id,
|
||||
attachTo: e.attachTo,
|
||||
@@ -177,7 +177,7 @@ describe('collectLegacyRoutes', () => {
|
||||
|
||||
expect(
|
||||
collected.map(p => ({
|
||||
id: p.id,
|
||||
id: p.$$type === '@backstage/FrontendPlugin' ? p.id : p.pluginId,
|
||||
extensions: OpaqueFrontendPlugin.toInternal(p).extensions.map(e => ({
|
||||
id: e.id,
|
||||
attachTo: e.attachTo,
|
||||
|
||||
@@ -270,7 +270,7 @@ export function collectLegacyRoutes(
|
||||
|
||||
if (entityPage) {
|
||||
collectEntityPageContents(entityPage, {
|
||||
discoveryExtension(extension, plugin) {
|
||||
discoverExtension(extension, plugin) {
|
||||
if (!plugin || plugin.getId() === 'catalog') {
|
||||
getPluginExtensions(orphanRoutesPlugin).push(extension);
|
||||
} else {
|
||||
|
||||
@@ -21,6 +21,16 @@ import { ScoreBoardPage } from '@oriflame/backstage-plugin-score-card';
|
||||
import React, { ReactNode } from 'react';
|
||||
import { Route } from 'react-router-dom';
|
||||
import { convertLegacyApp } from './convertLegacyApp';
|
||||
import {
|
||||
createApiFactory,
|
||||
createComponentExtension,
|
||||
createPlugin,
|
||||
} from '@backstage/core-plugin-api';
|
||||
import { EntityLayout, EntitySwitch, isKind } from '@backstage/plugin-catalog';
|
||||
import { renderInTestApp } from '@backstage/frontend-test-utils';
|
||||
import { default as catalogPlugin } from '@backstage/plugin-catalog/alpha';
|
||||
import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
|
||||
|
||||
const Root = ({ children }: { children: ReactNode }) => <>{children}</>;
|
||||
|
||||
@@ -159,4 +169,153 @@ describe('convertLegacyApp', () => {
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it('should convert entity pages', async () => {
|
||||
const fooPlugin = createPlugin({
|
||||
id: 'foo',
|
||||
});
|
||||
|
||||
const FooContent = fooPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'FooContent',
|
||||
component: { sync: () => <div>foo content</div> },
|
||||
}),
|
||||
);
|
||||
const OtherFooContent = fooPlugin.provide(
|
||||
createComponentExtension({
|
||||
name: 'OtherFooContent',
|
||||
component: { sync: () => <div>other foo content</div> },
|
||||
}),
|
||||
);
|
||||
|
||||
const simpleTestContent = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<div>overview content</div>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/foo" title="Foo">
|
||||
<FooContent />
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/bar" title="Bar">
|
||||
<div>bar content</div>
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
);
|
||||
|
||||
const otherTestContent = (
|
||||
<EntityLayout>
|
||||
<EntityLayout.Route path="/" title="Overview">
|
||||
<div>other overview content</div>
|
||||
</EntityLayout.Route>
|
||||
<EntityLayout.Route path="/foo" title="Foo">
|
||||
<OtherFooContent />
|
||||
</EntityLayout.Route>
|
||||
</EntityLayout>
|
||||
);
|
||||
|
||||
const entityPage = (
|
||||
<EntitySwitch>
|
||||
<EntitySwitch.Case if={isKind('test')}>
|
||||
{simpleTestContent}
|
||||
</EntitySwitch.Case>
|
||||
<EntitySwitch.Case>{otherTestContent}</EntitySwitch.Case>
|
||||
</EntitySwitch>
|
||||
);
|
||||
|
||||
const converted = convertLegacyApp(
|
||||
<FlatRoutes>
|
||||
<Route path="/test" element={<div>test</div>} />
|
||||
</FlatRoutes>,
|
||||
{ entityPage },
|
||||
);
|
||||
|
||||
const catalogOverride = catalogPlugin.withOverrides({
|
||||
extensions: [
|
||||
catalogPlugin.getExtension('api:catalog').override({
|
||||
params: {
|
||||
factory: createApiFactory(
|
||||
catalogApiRef,
|
||||
catalogApiMock({
|
||||
entities: [
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'test',
|
||||
metadata: {
|
||||
name: 'x',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
{
|
||||
apiVersion: 'backstage.io/v1alpha1',
|
||||
kind: 'other',
|
||||
metadata: {
|
||||
name: 'x',
|
||||
},
|
||||
spec: {},
|
||||
},
|
||||
],
|
||||
}),
|
||||
),
|
||||
},
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
// Overview
|
||||
const renderOverviewTest = await renderInTestApp(<div />, {
|
||||
features: [catalogOverride, ...converted],
|
||||
initialRouteEntries: ['/catalog/default/test/x'],
|
||||
});
|
||||
await expect(
|
||||
renderOverviewTest.findByText('overview content'),
|
||||
).resolves.toBeInTheDocument();
|
||||
renderOverviewTest.unmount();
|
||||
|
||||
const renderOverviewOther = await renderInTestApp(<div />, {
|
||||
features: [catalogOverride, ...converted],
|
||||
initialRouteEntries: ['/catalog/default/other/x'],
|
||||
});
|
||||
await expect(
|
||||
renderOverviewOther.findByText('other overview content'),
|
||||
).resolves.toBeInTheDocument();
|
||||
renderOverviewOther.unmount();
|
||||
|
||||
// Foo tab
|
||||
const renderFooTest = await renderInTestApp(<div />, {
|
||||
features: [catalogOverride, ...converted],
|
||||
initialRouteEntries: ['/catalog/default/test/x/foo'],
|
||||
});
|
||||
await expect(
|
||||
renderFooTest.findByText('foo content'),
|
||||
).resolves.toBeInTheDocument();
|
||||
renderFooTest.unmount();
|
||||
|
||||
const renderFooOther = await renderInTestApp(<div />, {
|
||||
features: [catalogOverride, ...converted],
|
||||
initialRouteEntries: ['/catalog/default/other/x/foo'],
|
||||
});
|
||||
await expect(
|
||||
renderFooOther.findByText('other foo content'),
|
||||
).resolves.toBeInTheDocument();
|
||||
renderFooOther.unmount();
|
||||
|
||||
// Bar tab
|
||||
const renderBarTest = await renderInTestApp(<div />, {
|
||||
features: [catalogOverride, ...converted],
|
||||
initialRouteEntries: ['/catalog/default/test/x/bar'],
|
||||
});
|
||||
await expect(
|
||||
renderBarTest.findByText('bar content'),
|
||||
).resolves.toBeInTheDocument();
|
||||
renderBarTest.unmount();
|
||||
|
||||
const renderBarOther = await renderInTestApp(<div />, {
|
||||
features: [catalogOverride, ...converted],
|
||||
initialRouteEntries: ['/catalog/default/other/x/bar'],
|
||||
});
|
||||
await expect(
|
||||
renderBarOther.findByText('other overview content'),
|
||||
).resolves.toBeInTheDocument(); // /bar does not exist, fall back to rendering overview
|
||||
renderBarOther.unmount();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -86,7 +86,7 @@ export function convertLegacyApp(
|
||||
options: ConvertLegacyAppOptions = {},
|
||||
): (FrontendPlugin | FrontendModule)[] {
|
||||
if (getComponentData(rootElement, 'core.type') === 'FlatRoutes') {
|
||||
return collectLegacyRoutes(rootElement);
|
||||
return collectLegacyRoutes(rootElement, options?.entityPage);
|
||||
}
|
||||
|
||||
const appRouterEls = selectChildren(
|
||||
|
||||
Reference in New Issue
Block a user