Merge pull request #21481 from backstage/mob/naming
frontend-*-api: change how extension IDs are defined
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': patch
|
||||
---
|
||||
|
||||
Updates for compatibility with the new extension IDs.
|
||||
@@ -0,0 +1,17 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
'@backstage/plugin-stack-overflow': patch
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-user-settings': patch
|
||||
'@backstage/plugin-search-react': patch
|
||||
'@backstage/plugin-tech-radar': patch
|
||||
'@backstage/plugin-graphiql': patch
|
||||
'@backstage/plugin-techdocs': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
'@backstage/plugin-explore': patch
|
||||
'@backstage/plugin-search': patch
|
||||
'@backstage/plugin-home': patch
|
||||
'@backstage/plugin-adr': patch
|
||||
---
|
||||
|
||||
Refactor of the alpha exports due to API change in how extension IDs are constructed.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Updates to match the introduction of `ExtensionDefinition` and new extension ID naming patterns.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/core-compat-api': patch
|
||||
---
|
||||
|
||||
Updates for compatibility with the new extension IDs.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': minor
|
||||
---
|
||||
|
||||
**BREAKING**: This version changes how extensions are created and how their IDs are determined. The `createExtension` function now accepts `kind`, `namespace` and `name` instead of `id`. All of the new options are optional, and are used to construct the final extension ID. By convention extension creators should set the `kind` to match their own name, for example `createNavItemExtension` sets the kind `nav-item`.
|
||||
|
||||
The `createExtension` function as well as all extension creators now also return an `ExtensionDefinition` rather than an `Extension`, which in turn needs to be passed to `createPlugin` or `createExtensionOverrides` to be used.
|
||||
@@ -21,7 +21,6 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const ExamplePage = createPageExtension({
|
||||
id: 'example.page',
|
||||
defaultPath: '/example',
|
||||
loader: () => import('./Component').then(m => <m.Component />),
|
||||
});
|
||||
|
||||
@@ -7,17 +7,17 @@ app:
|
||||
plugin.catalog.externalRoutes.viewTechDoc: plugin.techdocs.routes.docRoot
|
||||
|
||||
extensions:
|
||||
- apis.plugin.graphiql.browse.gitlab: true
|
||||
# - apis.plugin.graphiql.browse.gitlab: true
|
||||
- graphiql-endpoint:graphiql/gitlab: true
|
||||
|
||||
# Entity page cards
|
||||
- entity.cards.about
|
||||
- entity.cards.labels
|
||||
- entity.cards.links:
|
||||
- entity-card:catalog/about
|
||||
- entity-card:catalog/labels
|
||||
- entity-card:catalog/links:
|
||||
config:
|
||||
filter: kind:component has:links
|
||||
|
||||
# Entity page content
|
||||
- entity.content.techdocs
|
||||
- entity-content:techdocs
|
||||
|
||||
# scmAuthExtension: >-
|
||||
# createScmAuthExtension({
|
||||
|
||||
@@ -83,7 +83,7 @@ TODO:
|
||||
/* app.tsx */
|
||||
|
||||
const homePageExtension = createExtension({
|
||||
id: 'myhomepage',
|
||||
name: 'myhomepage',
|
||||
attachTo: { id: 'home', input: 'props' },
|
||||
output: {
|
||||
children: coreExtensionData.reactElement,
|
||||
@@ -95,7 +95,7 @@ const homePageExtension = createExtension({
|
||||
});
|
||||
|
||||
const signInPage = createSignInPageExtension({
|
||||
id: 'signInPage',
|
||||
name: 'guest',
|
||||
loader: async () => (props: SignInPageProps) =>
|
||||
<SignInPage {...props} providers={['guest']} />,
|
||||
});
|
||||
|
||||
@@ -36,7 +36,7 @@ export const pageXRouteRef = createRouteRef();
|
||||
// });
|
||||
|
||||
const IndexPage = createPageExtension({
|
||||
id: 'index',
|
||||
name: 'index',
|
||||
defaultPath: '/',
|
||||
routeRef: indexRouteRef,
|
||||
loader: async () => {
|
||||
@@ -68,7 +68,7 @@ const IndexPage = createPageExtension({
|
||||
});
|
||||
|
||||
const Page1 = createPageExtension({
|
||||
id: 'page1',
|
||||
name: 'page1',
|
||||
defaultPath: '/page1',
|
||||
routeRef: page1RouteRef,
|
||||
loader: async () => {
|
||||
@@ -102,7 +102,7 @@ const Page1 = createPageExtension({
|
||||
});
|
||||
|
||||
const ExternalPage = createPageExtension({
|
||||
id: 'pageX',
|
||||
name: 'pageX',
|
||||
defaultPath: '/pageX',
|
||||
routeRef: pageXRouteRef,
|
||||
loader: async () => {
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { AnyRouteRefParams } from '@backstage/core-plugin-api';
|
||||
import { AppComponents } from '@backstage/core-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionOverrides } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef as ExternalRouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
@@ -21,7 +21,7 @@ import { SubRouteRef as SubRouteRef_2 } from '@backstage/frontend-plugin-api';
|
||||
// @public (undocumented)
|
||||
export function collectLegacyComponents(
|
||||
components: Partial<AppComponents>,
|
||||
): Extension<unknown>[];
|
||||
): ExtensionDefinition<unknown>[];
|
||||
|
||||
// @public (undocumented)
|
||||
export function collectLegacyRoutes(
|
||||
|
||||
@@ -31,28 +31,33 @@ describe('collectLegacyComponents', () => {
|
||||
|
||||
expect(
|
||||
collected.map(p => ({
|
||||
id: p.id,
|
||||
kind: p.kind,
|
||||
namespace: p.namespace,
|
||||
attachTo: p.attachTo,
|
||||
disabled: p.disabled,
|
||||
})),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'core.components.progress',
|
||||
kind: 'component',
|
||||
namespace: 'core.components.progress',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'core.components.bootErrorPage',
|
||||
kind: 'component',
|
||||
namespace: 'core.components.bootErrorPage',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'core.components.notFoundErrorPage',
|
||||
kind: 'component',
|
||||
namespace: 'core.components.notFoundErrorPage',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'core.components.errorBoundaryFallback',
|
||||
kind: 'component',
|
||||
namespace: 'core.components.errorBoundaryFallback',
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
disabled: false,
|
||||
},
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
Extension,
|
||||
ComponentRef,
|
||||
createComponentExtension,
|
||||
coreComponentsRefs,
|
||||
ExtensionDefinition,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { AppComponents } from '@backstage/core-plugin-api';
|
||||
|
||||
@@ -33,7 +33,7 @@ const refs: Record<string, ComponentRef<ComponentTypes>> = {
|
||||
|
||||
/** @public */
|
||||
export function collectLegacyComponents(components: Partial<AppComponents>) {
|
||||
return Object.entries(components).reduce<Extension<unknown>[]>(
|
||||
return Object.entries(components).reduce<ExtensionDefinition<unknown>[]>(
|
||||
(extensions, [name, component]) => {
|
||||
const ref = refs[name];
|
||||
return ref
|
||||
|
||||
@@ -51,13 +51,13 @@ describe('collectLegacyRoutes', () => {
|
||||
id: 'score-card',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.score-card.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:score-card',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'score-board' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.scoringdata.service',
|
||||
id: 'api:plugin.scoringdata.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
@@ -67,13 +67,13 @@ describe('collectLegacyRoutes', () => {
|
||||
id: 'stackstorm',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.stackstorm.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:stackstorm',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'stackstorm' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.stackstorm.service',
|
||||
id: 'api:plugin.stackstorm.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
@@ -83,19 +83,19 @@ describe('collectLegacyRoutes', () => {
|
||||
id: 'puppetDb',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.puppetDb.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:puppetDb',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'plugin.puppetDb.page2',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:puppetDb/2',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.puppetdb.service',
|
||||
id: 'api:plugin.puppetdb.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
import React, { ReactNode } from 'react';
|
||||
import {
|
||||
Extension,
|
||||
createApiExtension,
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
BackstagePlugin,
|
||||
ExtensionDefinition,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Route, Routes } from 'react-router-dom';
|
||||
import {
|
||||
@@ -64,7 +64,7 @@ export function collectLegacyRoutes(
|
||||
): BackstagePlugin[] {
|
||||
const createdPluginIds = new Map<
|
||||
LegacyBackstagePlugin,
|
||||
Extension<unknown>[]
|
||||
ExtensionDefinition<unknown>[]
|
||||
>();
|
||||
|
||||
React.Children.forEach(
|
||||
@@ -95,19 +95,18 @@ export function collectLegacyRoutes(
|
||||
'core.mountPoint',
|
||||
);
|
||||
|
||||
const pluginId = plugin.getId();
|
||||
|
||||
const detectedExtensions =
|
||||
createdPluginIds.get(plugin) ?? new Array<Extension<unknown>>();
|
||||
createdPluginIds.get(plugin) ??
|
||||
new Array<ExtensionDefinition<unknown>>();
|
||||
createdPluginIds.set(plugin, detectedExtensions);
|
||||
|
||||
const path: string = route.props.path;
|
||||
|
||||
detectedExtensions.push(
|
||||
createPageExtension({
|
||||
id: `plugin.${pluginId}.page${
|
||||
detectedExtensions.length ? detectedExtensions.length + 1 : ''
|
||||
}`,
|
||||
name: detectedExtensions.length
|
||||
? String(detectedExtensions.length + 1)
|
||||
: undefined,
|
||||
defaultPath: path[0] === '/' ? path.slice(1) : path,
|
||||
routeRef: routeRef ? convertLegacyRouteRef(routeRef) : undefined,
|
||||
|
||||
@@ -132,9 +131,7 @@ export function collectLegacyRoutes(
|
||||
extensions: [
|
||||
...extensions,
|
||||
...Array.from(plugin.getApis()).map(factory =>
|
||||
createApiExtension({
|
||||
factory,
|
||||
}),
|
||||
createApiExtension({ factory }),
|
||||
),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -59,13 +59,13 @@ describe('convertLegacyApp', () => {
|
||||
id: 'score-card',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.score-card.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:score-card',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'score-board' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.scoringdata.service',
|
||||
id: 'api:plugin.scoringdata.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
@@ -75,13 +75,13 @@ describe('convertLegacyApp', () => {
|
||||
id: 'stackstorm',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.stackstorm.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:stackstorm',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'stackstorm' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.stackstorm.service',
|
||||
id: 'api:plugin.stackstorm.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
@@ -91,19 +91,19 @@ describe('convertLegacyApp', () => {
|
||||
id: 'puppetDb',
|
||||
extensions: [
|
||||
{
|
||||
id: 'plugin.puppetDb.page',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:puppetDb',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'plugin.puppetDb.page2',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
id: 'page:puppetDb/2',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
disabled: false,
|
||||
defaultConfig: { path: 'puppetdb' },
|
||||
},
|
||||
{
|
||||
id: 'apis.plugin.puppetdb.service',
|
||||
id: 'api:plugin.puppetdb.service',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
},
|
||||
@@ -113,13 +113,13 @@ describe('convertLegacyApp', () => {
|
||||
id: undefined,
|
||||
extensions: [
|
||||
{
|
||||
id: 'core.layout',
|
||||
id: 'core/layout',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'core.nav',
|
||||
attachTo: { id: 'core.layout', input: 'nav' },
|
||||
id: 'core/nav',
|
||||
attachTo: { id: 'core/layout', input: 'nav' },
|
||||
disabled: true,
|
||||
},
|
||||
],
|
||||
|
||||
@@ -100,7 +100,8 @@ export function convertLegacyApp(
|
||||
const [routesEl] = routesEls;
|
||||
|
||||
const CoreLayoutOverride = createExtension({
|
||||
id: 'core.layout',
|
||||
namespace: 'core',
|
||||
name: 'layout',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
inputs: {
|
||||
content: createExtensionInput(
|
||||
@@ -121,8 +122,9 @@ export function convertLegacyApp(
|
||||
},
|
||||
});
|
||||
const CoreNavOverride = createExtension({
|
||||
id: 'core.nav',
|
||||
attachTo: { id: 'core.layout', input: 'nav' },
|
||||
namespace: 'core',
|
||||
name: 'nav',
|
||||
attachTo: { id: 'core/layout', input: 'nav' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
disabled: true,
|
||||
|
||||
@@ -21,7 +21,7 @@ import {
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const Core = createExtension({
|
||||
id: 'core',
|
||||
namespace: 'core',
|
||||
attachTo: { id: 'root', input: 'default' }, // ignored
|
||||
inputs: {
|
||||
apis: createExtensionInput({
|
||||
|
||||
@@ -23,8 +23,9 @@ import {
|
||||
import { SidebarPage } from '@backstage/core-components';
|
||||
|
||||
export const CoreLayout = createExtension({
|
||||
id: 'core.layout',
|
||||
attachTo: { id: 'core.router', input: 'children' },
|
||||
namespace: 'core',
|
||||
name: 'layout',
|
||||
attachTo: { id: 'core/router', input: 'children' },
|
||||
inputs: {
|
||||
nav: createExtensionInput(
|
||||
{
|
||||
|
||||
@@ -75,8 +75,9 @@ const SidebarNavItem = (props: NavTarget) => {
|
||||
};
|
||||
|
||||
export const CoreNav = createExtension({
|
||||
id: 'core.nav',
|
||||
attachTo: { id: 'core.layout', input: 'nav' },
|
||||
namespace: 'core',
|
||||
name: 'nav',
|
||||
attachTo: { id: 'core/layout', input: 'nav' },
|
||||
inputs: {
|
||||
items: createExtensionInput({
|
||||
target: coreExtensionData.navTarget,
|
||||
|
||||
@@ -36,7 +36,8 @@ import { signInPageComponentDataRef } from '../../../frontend-plugin-api/src/ext
|
||||
import { RouteTracker } from '../routing/RouteTracker';
|
||||
|
||||
export const CoreRouter = createExtension({
|
||||
id: 'core.router',
|
||||
namespace: 'core',
|
||||
name: 'router',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
inputs: {
|
||||
signInPage: createExtensionInput(
|
||||
|
||||
@@ -26,8 +26,9 @@ import {
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
|
||||
export const CoreRoutes = createExtension({
|
||||
id: 'core.routes',
|
||||
attachTo: { id: 'core.layout', input: 'content' },
|
||||
namespace: 'core',
|
||||
name: 'routes',
|
||||
attachTo: { id: 'core/layout', input: 'content' },
|
||||
inputs: {
|
||||
routes: createExtensionInput({
|
||||
path: coreExtensionData.routePath,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { extractRouteInfoFromAppNode } from './extractRouteInfoFromAppNode';
|
||||
import {
|
||||
AnyRouteRefParams,
|
||||
AppNode,
|
||||
Extension,
|
||||
ExtensionDefinition,
|
||||
RouteRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
@@ -40,16 +40,16 @@ const ref5 = createRouteRef();
|
||||
const refOrder: RouteRef<AnyRouteRefParams>[] = [ref1, ref2, ref3, ref4, ref5];
|
||||
|
||||
function createTestExtension(options: {
|
||||
id: string;
|
||||
name: string;
|
||||
parent?: string;
|
||||
path?: string;
|
||||
routeRef?: RouteRef;
|
||||
}) {
|
||||
return createExtension({
|
||||
id: options.id,
|
||||
name: options.name,
|
||||
attachTo: options.parent
|
||||
? { id: options.parent, input: 'children' }
|
||||
: { id: 'core.routes', input: 'routes' },
|
||||
? { id: `test/${options.parent}`, input: 'children' }
|
||||
: { id: 'core/routes', input: 'routes' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath.optional(),
|
||||
@@ -70,14 +70,14 @@ function createTestExtension(options: {
|
||||
});
|
||||
}
|
||||
|
||||
function routeInfoFromExtensions(extensions: Extension<unknown>[]) {
|
||||
function routeInfoFromExtensions(extensions: ExtensionDefinition<unknown>[]) {
|
||||
const plugin = createPlugin({
|
||||
id: 'test',
|
||||
extensions,
|
||||
});
|
||||
const tree = createAppTree({
|
||||
builtinExtensions,
|
||||
config: new MockConfigApi({}),
|
||||
builtinExtensions,
|
||||
features: [plugin],
|
||||
});
|
||||
|
||||
@@ -122,33 +122,33 @@ describe('discovery', () => {
|
||||
it('should collect routes', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'nothing',
|
||||
name: 'nothing',
|
||||
path: 'nothing',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
name: 'page1',
|
||||
path: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
name: 'page2',
|
||||
parent: 'page1',
|
||||
path: 'bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
name: 'page3',
|
||||
parent: 'page2',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
name: 'page4',
|
||||
path: 'divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
name: 'page5',
|
||||
parent: 'page1',
|
||||
path: 'blop',
|
||||
routeRef: ref5,
|
||||
@@ -226,29 +226,29 @@ describe('discovery', () => {
|
||||
it('should handle all react router Route patterns', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
name: 'page1',
|
||||
path: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
name: 'page2',
|
||||
parent: 'page1',
|
||||
path: 'bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
name: 'page3',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
name: 'page4',
|
||||
parent: 'page3',
|
||||
path: 'divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
name: 'page5',
|
||||
parent: 'page3',
|
||||
path: 'blop',
|
||||
routeRef: ref5,
|
||||
@@ -274,29 +274,29 @@ describe('discovery', () => {
|
||||
it('should strip leading slashes in route paths', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
name: 'page1',
|
||||
path: '/foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
name: 'page2',
|
||||
parent: 'page1',
|
||||
path: '/bar/:id',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
name: 'page3',
|
||||
path: '/baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
name: 'page4',
|
||||
parent: 'page3',
|
||||
path: '/divsoup',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
name: 'page5',
|
||||
parent: 'page3',
|
||||
path: '/blop',
|
||||
routeRef: ref5,
|
||||
@@ -322,44 +322,44 @@ describe('discovery', () => {
|
||||
it('should use the route aggregator key to bind child routes to the same path', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'foo',
|
||||
name: 'foo',
|
||||
path: 'foo',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
name: 'page1',
|
||||
parent: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'fooChild',
|
||||
name: 'fooChild',
|
||||
parent: 'foo',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
name: 'page2',
|
||||
parent: 'fooChild',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'fooEmpty',
|
||||
name: 'fooEmpty',
|
||||
parent: 'foo',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
name: 'page3',
|
||||
path: 'bar',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3Child',
|
||||
name: 'page3Child',
|
||||
parent: 'page3',
|
||||
path: '',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
name: 'page4',
|
||||
parent: 'page3Child',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
name: 'page5',
|
||||
parent: 'page4',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
@@ -411,34 +411,34 @@ describe('discovery', () => {
|
||||
it('should use the route aggregator but stop when encountering explicit path', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
name: 'page1',
|
||||
path: 'foo',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1Child',
|
||||
name: 'page1Child',
|
||||
parent: 'page1',
|
||||
path: 'bar',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
name: 'page2',
|
||||
parent: 'page1Child',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
name: 'page3',
|
||||
parent: 'page2',
|
||||
path: 'baz',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
name: 'page4',
|
||||
parent: 'page3',
|
||||
path: '/blop',
|
||||
routeRef: ref4,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page5',
|
||||
name: 'page5',
|
||||
parent: 'page2',
|
||||
routeRef: ref5,
|
||||
}),
|
||||
@@ -500,34 +500,34 @@ describe('discovery', () => {
|
||||
it('should account for loose route paths', () => {
|
||||
const info = routeInfoFromExtensions([
|
||||
createTestExtension({
|
||||
id: 'r',
|
||||
name: 'r',
|
||||
path: 'r',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page1',
|
||||
name: 'page1',
|
||||
parent: 'r',
|
||||
path: 'x',
|
||||
routeRef: ref1,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'y',
|
||||
name: 'y',
|
||||
path: 'y',
|
||||
parent: 'r',
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page2',
|
||||
name: 'page2',
|
||||
parent: 'y',
|
||||
path: '1',
|
||||
routeRef: ref2,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page3',
|
||||
name: 'page3',
|
||||
parent: 'page2',
|
||||
path: 'a',
|
||||
routeRef: ref3,
|
||||
}),
|
||||
createTestExtension({
|
||||
id: 'page4',
|
||||
name: 'page4',
|
||||
parent: 'page2',
|
||||
path: 'b',
|
||||
routeRef: ref4,
|
||||
|
||||
@@ -54,12 +54,11 @@ describe('createAppTree', () => {
|
||||
it('throws an error when a core extension is overridden', () => {
|
||||
const config = new MockConfigApi({});
|
||||
const features = [
|
||||
createPlugin({
|
||||
id: 'plugin',
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createExtension({
|
||||
id: 'core',
|
||||
attachTo: { id: 'core.routes', input: 'route' },
|
||||
name: 'core',
|
||||
attachTo: { id: 'core/routes', input: 'route' },
|
||||
inputs: {},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
@@ -70,33 +69,7 @@ describe('createAppTree', () => {
|
||||
expect(() =>
|
||||
createAppTree({ features, config, builtinExtensions: [] }),
|
||||
).toThrow(
|
||||
"It is forbidden to override the following extension(s): 'core', which is done by the following plugin(s): 'plugin'",
|
||||
);
|
||||
});
|
||||
|
||||
it('throws an error when duplicated extensions are detected', () => {
|
||||
const config = new MockConfigApi({});
|
||||
|
||||
const ExtensionA = createExtension({ ...extBase, id: 'A' });
|
||||
|
||||
const ExtensionB = createExtension({ ...extBase, id: 'B' });
|
||||
|
||||
const PluginA = createPlugin({
|
||||
id: 'A',
|
||||
extensions: [ExtensionA, ExtensionA],
|
||||
});
|
||||
|
||||
const PluginB = createPlugin({
|
||||
id: 'B',
|
||||
extensions: [ExtensionA, ExtensionB, ExtensionB],
|
||||
});
|
||||
|
||||
const features = [PluginA, PluginB];
|
||||
|
||||
expect(() =>
|
||||
createAppTree({ features, config, builtinExtensions: [] }),
|
||||
).toThrow(
|
||||
"The following extensions are duplicated: The extension 'A' was provided 2 time(s) by the plugin 'A' and 1 time(s) by the plugin 'B', The extension 'B' was provided 2 time(s) by the plugin 'B'",
|
||||
"It is forbidden to override the following extension(s): 'core', which is done by one or more extension overrides",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -106,13 +79,13 @@ describe('createAppTree', () => {
|
||||
features: [
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createExtension({ ...extBase, id: 'a' }),
|
||||
createExtension({ ...extBase, id: 'a' }),
|
||||
createExtension({ ...extBase, id: 'b' }),
|
||||
createExtension({ ...extBase, name: 'a' }),
|
||||
createExtension({ ...extBase, name: 'a' }),
|
||||
createExtension({ ...extBase, name: 'b' }),
|
||||
],
|
||||
}),
|
||||
createExtensionOverrides({
|
||||
extensions: [createExtension({ ...extBase, id: 'b' })],
|
||||
extensions: [createExtension({ ...extBase, name: 'b' })],
|
||||
}),
|
||||
],
|
||||
config: new MockConfigApi({}),
|
||||
|
||||
@@ -28,28 +28,33 @@ import {
|
||||
} from './instantiateAppNodeTree';
|
||||
import { AppNodeInstance, AppNodeSpec } from '@backstage/frontend-plugin-api';
|
||||
import { resolveAppTree } from './resolveAppTree';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
|
||||
const testDataRef = createExtensionDataRef<string>('test');
|
||||
const otherDataRef = createExtensionDataRef<number>('other');
|
||||
const inputMirrorDataRef = createExtensionDataRef<unknown>('mirror');
|
||||
|
||||
const simpleExtension = createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
output: z.string().default('test'),
|
||||
other: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { test: config.output, other: config.other };
|
||||
},
|
||||
});
|
||||
const simpleExtension = resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
output: z.string().default('test'),
|
||||
other: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { test: config.output, other: config.other };
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
function makeSpec<TConfig>(
|
||||
extension: Extension<TConfig>,
|
||||
@@ -117,19 +122,21 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should instantiate a node with attachments', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
makeSpec(
|
||||
createExtension({
|
||||
id: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
makeSpec(simpleExtension, {
|
||||
id: 'child-node',
|
||||
@@ -159,19 +166,21 @@ describe('instantiateAppNodeTree', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
{
|
||||
...makeSpec(
|
||||
createExtension({
|
||||
id: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -242,43 +251,46 @@ describe('createAppNodeInstance', () => {
|
||||
const instance = createAppNodeInstance({
|
||||
attachments,
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
optionalSingletonPresent: createExtensionInput(
|
||||
{
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
optionalSingletonPresent: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
optionalSingletonMissing: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
many: createExtensionInput({
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
optionalSingletonMissing: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
many: createExtensionInput({
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -297,7 +309,7 @@ describe('createAppNodeInstance', () => {
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Invalid configuration for extension 'core.test'; caused by Error: Expected number, received string at 'other'",
|
||||
"Invalid configuration for extension 'core/test'; caused by Error: Expected number, received string at 'other'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -305,21 +317,24 @@ describe('createAppNodeInstance', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory() {
|
||||
const error = new Error('NOPE');
|
||||
error.name = 'NopeError';
|
||||
throw error;
|
||||
},
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory() {
|
||||
const error = new Error('NOPE');
|
||||
error.name = 'NopeError';
|
||||
throw error;
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test'; caused by NopeError: NOPE",
|
||||
"Failed to instantiate extension 'core/test'; caused by NopeError: NOPE",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -327,22 +342,25 @@ describe('createAppNodeInstance', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test1: testDataRef,
|
||||
test2: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { test1: 'test', test2: 'test2' };
|
||||
},
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test1: testDataRef,
|
||||
test2: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { test1: 'test', test2: 'test2' };
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', duplicate extension data 'test' received via output 'test2'",
|
||||
"Failed to instantiate extension 'core/test', duplicate extension data 'test' received via output 'test2'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -350,21 +368,24 @@ describe('createAppNodeInstance', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { nonexistent: 'test' } as any;
|
||||
},
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { nonexistent: 'test' } as any;
|
||||
},
|
||||
}),
|
||||
),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', unknown output provided via 'nonexistent'",
|
||||
"Failed to instantiate extension 'core/test', unknown output provided via 'nonexistent'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -372,25 +393,28 @@ describe('createAppNodeInstance', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', input 'singleton' is required but was not received",
|
||||
"Failed to instantiate extension 'core/test', input 'singleton' is required but was not received",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -416,21 +440,24 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
declared: createExtensionInput({
|
||||
test: testDataRef,
|
||||
}),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
declared: createExtensionInput({
|
||||
test: testDataRef,
|
||||
}),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', received undeclared input 'undeclared' from extension 'core.test'",
|
||||
"Failed to instantiate extension 'core/test', received undeclared input 'undeclared' from extension 'core/test'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -451,16 +478,19 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', received undeclared inputs 'undeclared1' from extension 'core.test' and 'undeclared2' from extensions 'core.test', 'core.test'",
|
||||
"Failed to instantiate extension 'core/test', received undeclared inputs 'undeclared1' from extension 'core/test' and 'undeclared2' from extensions 'core/test', 'core/test'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -477,24 +507,27 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', expected exactly one 'singleton' input but received multiple: 'core.test', 'core.test'",
|
||||
"Failed to instantiate extension 'core/test', expected exactly one 'singleton' input but received multiple: 'core/test', 'core/test'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -511,24 +544,27 @@ describe('createAppNodeInstance', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', expected at most one 'singleton' input but received multiple: 'core.test', 'core.test'",
|
||||
"Failed to instantiate extension 'core/test', expected at most one 'singleton' input but received multiple: 'core/test', 'core/test'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -539,24 +575,27 @@ describe('createAppNodeInstance', () => {
|
||||
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
|
||||
]),
|
||||
node: makeNode(
|
||||
createExtension({
|
||||
id: 'core.test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
other: otherDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'core',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
other: otherDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', input 'singleton' did not receive required extension data 'other' from extension 'core.test'",
|
||||
"Failed to instantiate extension 'core/test', input 'singleton' did not receive required extension data 'other' from extension 'core/test'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -25,18 +25,18 @@ describe('readAppExtensionsConfig', () => {
|
||||
it('should disable extension with shorthand notation', () => {
|
||||
expect(
|
||||
readAppExtensionsConfig(
|
||||
new ConfigReader({ app: { extensions: [{ 'core.router': false }] } }),
|
||||
new ConfigReader({ app: { extensions: [{ 'core/router': false }] } }),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'core.router',
|
||||
id: 'core/router',
|
||||
disabled: true,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
readAppExtensionsConfig(
|
||||
new ConfigReader({
|
||||
app: { extensions: [{ 'core.router': { disabled: true } }] },
|
||||
app: { extensions: [{ 'core/router': { disabled: true } }] },
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
@@ -44,7 +44,7 @@ describe('readAppExtensionsConfig', () => {
|
||||
at: undefined,
|
||||
config: undefined,
|
||||
disabled: true,
|
||||
id: 'core.router',
|
||||
id: 'core/router',
|
||||
},
|
||||
]);
|
||||
});
|
||||
@@ -52,33 +52,33 @@ describe('readAppExtensionsConfig', () => {
|
||||
it('should enable extension with shorthand notation', () => {
|
||||
expect(
|
||||
readAppExtensionsConfig(
|
||||
new ConfigReader({ app: { extensions: ['core.router'] } }),
|
||||
new ConfigReader({ app: { extensions: ['core/router'] } }),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'core.router',
|
||||
id: 'core/router',
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
readAppExtensionsConfig(
|
||||
new ConfigReader({ app: { extensions: [{ 'core.router': true }] } }),
|
||||
new ConfigReader({ app: { extensions: [{ 'core/router': true }] } }),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'core.router',
|
||||
id: 'core/router',
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
expect(
|
||||
readAppExtensionsConfig(
|
||||
new ConfigReader({
|
||||
app: { extensions: [{ 'core.router': { disabled: false } }] },
|
||||
app: { extensions: [{ 'core/router': { disabled: false } }] },
|
||||
}),
|
||||
),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'core.router',
|
||||
id: 'core/router',
|
||||
disabled: false,
|
||||
},
|
||||
]);
|
||||
@@ -89,12 +89,12 @@ describe('readAppExtensionsConfig', () => {
|
||||
readAppExtensionsConfig(
|
||||
new ConfigReader({
|
||||
app: {
|
||||
extensions: [{ 'core.router': 'some-string' }],
|
||||
extensions: [{ 'core/router': 'some-string' }],
|
||||
},
|
||||
}),
|
||||
),
|
||||
).toThrow(
|
||||
'Invalid extension configuration at app.extensions[0][core.router], value must be a boolean or object',
|
||||
'Invalid extension configuration at app.extensions[0][core/router], value must be a boolean or object',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -105,7 +105,7 @@ describe('readAppExtensionsConfig', () => {
|
||||
app: {
|
||||
extensions: [
|
||||
{
|
||||
'core.router/routes': {
|
||||
'': {
|
||||
extension: 'example-package#MyPage',
|
||||
config: { foo: 'bar' },
|
||||
},
|
||||
@@ -115,7 +115,7 @@ describe('readAppExtensionsConfig', () => {
|
||||
}),
|
||||
),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[0], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
|
||||
`"Invalid extension configuration at app.extensions[0], extension ID must not be empty or contain whitespace"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -156,8 +156,8 @@ describe('expandShorthandExtensionParameters', () => {
|
||||
});
|
||||
|
||||
it('supports string key', () => {
|
||||
expect(run('core.router')).toEqual({
|
||||
id: 'core.router',
|
||||
expect(run('core/router')).toEqual({
|
||||
id: 'core/router',
|
||||
disabled: false,
|
||||
});
|
||||
expect(() => run('')).toThrowErrorMatchingInlineSnapshot(
|
||||
@@ -166,103 +166,100 @@ describe('expandShorthandExtensionParameters', () => {
|
||||
expect(() => run(' a')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1], extension ID must not be empty or contain whitespace"`,
|
||||
);
|
||||
expect(() => run('core.router/routes')).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1], extension ID must not contain slashes; got 'core.router/routes', did you mean 'core.router'?"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports null value', () => {
|
||||
// this is the result of typing:
|
||||
// - core.router:
|
||||
// - core/router:
|
||||
// The missing value is interpreted as null by the yaml parser so we deal with that
|
||||
expect(run({ 'core.router': null })).toEqual({
|
||||
id: 'core.router',
|
||||
expect(run({ 'core/router': null })).toEqual({
|
||||
id: 'core/router',
|
||||
disabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
it('supports boolean value', () => {
|
||||
expect(run({ 'core.router': true })).toEqual({
|
||||
id: 'core.router',
|
||||
expect(run({ 'core/router': true })).toEqual({
|
||||
id: 'core/router',
|
||||
disabled: false,
|
||||
});
|
||||
expect(run({ 'core.router': false })).toEqual({
|
||||
id: 'core.router',
|
||||
expect(run({ 'core/router': false })).toEqual({
|
||||
id: 'core/router',
|
||||
disabled: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('should not support string values', () => {
|
||||
expect(() =>
|
||||
run({ 'core.router': 'example-package#MyRouter' }),
|
||||
run({ 'core/router': 'example-package#MyRouter' }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router], value must be a boolean or object"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core/router], value must be a boolean or object"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports object id only in the key', () => {
|
||||
expect(() =>
|
||||
run({ 'core.router': { id: 'some.id' } }),
|
||||
run({ 'core/router': { id: 'some.id' } }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core/router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports object attachTo', () => {
|
||||
expect(
|
||||
run({
|
||||
'core.router': { attachTo: { id: 'other.root', input: 'inputs' } },
|
||||
'core/router': { attachTo: { id: 'other.root', input: 'inputs' } },
|
||||
}),
|
||||
).toEqual({
|
||||
id: 'core.router',
|
||||
id: 'core/router',
|
||||
attachTo: { id: 'other.root', input: 'inputs' },
|
||||
});
|
||||
expect(() =>
|
||||
run({
|
||||
'core.router': {
|
||||
'core/router': {
|
||||
id: 'other-id',
|
||||
},
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core/router].id, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports object disabled', () => {
|
||||
expect(run({ 'core.router': { disabled: true } })).toEqual({
|
||||
id: 'core.router',
|
||||
expect(run({ 'core/router': { disabled: true } })).toEqual({
|
||||
id: 'core/router',
|
||||
disabled: true,
|
||||
});
|
||||
expect(run({ 'core.router': { disabled: false } })).toEqual({
|
||||
id: 'core.router',
|
||||
expect(run({ 'core/router': { disabled: false } })).toEqual({
|
||||
id: 'core/router',
|
||||
disabled: false,
|
||||
});
|
||||
expect(() =>
|
||||
run({ 'core.router': { disabled: 0 } }),
|
||||
run({ 'core/router': { disabled: 0 } }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].disabled, must be a boolean"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core/router].disabled, must be a boolean"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('supports object config', () => {
|
||||
expect(
|
||||
run({ 'core.router': { config: { disableRedirects: true } } }),
|
||||
run({ 'core/router': { config: { disableRedirects: true } } }),
|
||||
).toEqual({
|
||||
id: 'core.router',
|
||||
id: 'core/router',
|
||||
config: { disableRedirects: true },
|
||||
});
|
||||
expect(() =>
|
||||
run({ 'core.router': { config: 0 } }),
|
||||
run({ 'core/router': { config: 0 } }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].config, must be an object"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core/router].config, must be an object"`,
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects unknown object keys', () => {
|
||||
expect(() =>
|
||||
run({ 'core.router': { foo: { settings: true } } }),
|
||||
run({ 'core/router': { foo: { settings: true } } }),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
`"Invalid extension configuration at app.extensions[1][core.router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
`"Invalid extension configuration at app.extensions[1][core/router].foo, unknown parameter; expected one of 'attachTo', 'disabled', 'config'"`,
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -70,15 +70,6 @@ export function expandShorthandExtensionParameters(
|
||||
errorMsg('extension ID must not be empty or contain whitespace'),
|
||||
);
|
||||
}
|
||||
|
||||
if (id.includes('/')) {
|
||||
let message = `extension ID must not contain slashes; got '${id}'`;
|
||||
const good = id.split('/')[0];
|
||||
if (good) {
|
||||
message += `, did you mean '${good}'?`;
|
||||
}
|
||||
throw new Error(errorMsg(message));
|
||||
}
|
||||
}
|
||||
|
||||
// Example YAML:
|
||||
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
createExtensionOverrides,
|
||||
createPlugin,
|
||||
Extension,
|
||||
ExtensionDefinition,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { resolveAppNodeSpecs } from './resolveAppNodeSpecs';
|
||||
|
||||
@@ -27,12 +28,26 @@ function makeExt(
|
||||
attachId: string = 'root',
|
||||
) {
|
||||
return {
|
||||
$$type: '@backstage/Extension',
|
||||
id,
|
||||
attachTo: { id: attachId, input: 'default' },
|
||||
disabled: status === 'disabled',
|
||||
} as Extension<unknown>;
|
||||
}
|
||||
|
||||
function makeExtDef(
|
||||
name: string,
|
||||
status: 'disabled' | 'enabled' = 'enabled',
|
||||
attachId: string = 'root',
|
||||
) {
|
||||
return {
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
name,
|
||||
attachTo: { id: attachId, input: 'default' },
|
||||
disabled: status === 'disabled',
|
||||
} as ExtensionDefinition<unknown>;
|
||||
}
|
||||
|
||||
describe('resolveAppNodeSpecs', () => {
|
||||
it('should not filter out disabled extension instances', () => {
|
||||
const a = makeExt('a', 'disabled');
|
||||
@@ -78,9 +93,8 @@ describe('resolveAppNodeSpecs', () => {
|
||||
});
|
||||
|
||||
it('should override attachment points', () => {
|
||||
const a = makeExt('a');
|
||||
const b = makeExt('b');
|
||||
const pluginA = createPlugin({ id: 'test', extensions: [a] });
|
||||
const pluginA = createPlugin({ id: 'test', extensions: [makeExtDef('a')] });
|
||||
expect(
|
||||
resolveAppNodeSpecs({
|
||||
features: [pluginA],
|
||||
@@ -94,8 +108,8 @@ describe('resolveAppNodeSpecs', () => {
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'a',
|
||||
extension: a,
|
||||
id: 'test/a',
|
||||
extension: makeExt('test/a'),
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
source: pluginA,
|
||||
disabled: false,
|
||||
@@ -110,31 +124,34 @@ describe('resolveAppNodeSpecs', () => {
|
||||
});
|
||||
|
||||
it('should fully override configuration and duplicate', () => {
|
||||
const a = makeExt('a');
|
||||
const b = makeExt('b');
|
||||
const plugin = createPlugin({ id: 'test', extensions: [a, b] });
|
||||
const a = makeExt('test/a');
|
||||
const b = makeExt('test/b');
|
||||
const plugin = createPlugin({
|
||||
id: 'test',
|
||||
extensions: [makeExtDef('a'), makeExtDef('b')],
|
||||
});
|
||||
expect(
|
||||
resolveAppNodeSpecs({
|
||||
features: [plugin],
|
||||
builtinExtensions: [],
|
||||
parameters: [
|
||||
{
|
||||
id: 'a',
|
||||
id: 'test/a',
|
||||
config: { foo: { bar: 1 } },
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
id: 'test/b',
|
||||
config: { foo: { bar: 2 } },
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
id: 'test/b',
|
||||
config: { foo: { qux: 3 } },
|
||||
},
|
||||
],
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'a',
|
||||
id: 'test/a',
|
||||
extension: a,
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
source: plugin,
|
||||
@@ -142,7 +159,7 @@ describe('resolveAppNodeSpecs', () => {
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
id: 'test/b',
|
||||
extension: b,
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
source: plugin,
|
||||
@@ -187,11 +204,12 @@ describe('resolveAppNodeSpecs', () => {
|
||||
});
|
||||
|
||||
it('should apply extension overrides', () => {
|
||||
const a = makeExt('a');
|
||||
const b = makeExt('b');
|
||||
const plugin = createPlugin({ id: 'test', extensions: [a, b] });
|
||||
const aOverride = makeExt('a', 'enabled', 'other');
|
||||
const bOverride = makeExt('b', 'disabled', 'other');
|
||||
const plugin = createPlugin({
|
||||
id: 'test',
|
||||
extensions: [makeExtDef('a'), makeExtDef('b')],
|
||||
});
|
||||
const aOverride = makeExt('test/a', 'enabled', 'other');
|
||||
const bOverride = makeExt('test/b', 'disabled', 'other');
|
||||
const cOverride = makeExt('c');
|
||||
|
||||
expect(
|
||||
@@ -199,7 +217,11 @@ describe('resolveAppNodeSpecs', () => {
|
||||
features: [
|
||||
plugin,
|
||||
createExtensionOverrides({
|
||||
extensions: [aOverride, bOverride, cOverride],
|
||||
extensions: [
|
||||
makeExtDef('test/a', 'enabled', 'other'),
|
||||
makeExtDef('test/b', 'disabled', 'other'),
|
||||
makeExtDef('c'),
|
||||
],
|
||||
}),
|
||||
],
|
||||
builtinExtensions: [],
|
||||
@@ -207,14 +229,14 @@ describe('resolveAppNodeSpecs', () => {
|
||||
}),
|
||||
).toEqual([
|
||||
{
|
||||
id: 'a',
|
||||
id: 'test/a',
|
||||
extension: aOverride,
|
||||
attachTo: { id: 'other', input: 'default' },
|
||||
source: plugin,
|
||||
disabled: false,
|
||||
},
|
||||
{
|
||||
id: 'b',
|
||||
id: 'test/b',
|
||||
extension: bOverride,
|
||||
attachTo: { id: 'other', input: 'default' },
|
||||
source: plugin,
|
||||
@@ -231,39 +253,50 @@ describe('resolveAppNodeSpecs', () => {
|
||||
});
|
||||
|
||||
it('should use order from configuration when rather than overrides', () => {
|
||||
const a = makeExt('a', 'disabled');
|
||||
const b = makeExt('b', 'disabled');
|
||||
const c = makeExt('c', 'disabled');
|
||||
const aOverride = makeExt('c', 'disabled');
|
||||
const bOverride = makeExt('b', 'disabled');
|
||||
const cOverride = makeExt('a', 'disabled');
|
||||
|
||||
const result = resolveAppNodeSpecs({
|
||||
features: [
|
||||
createPlugin({ id: 'test', extensions: [a, b, c] }),
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
makeExtDef('a', 'disabled'),
|
||||
makeExtDef('b', 'disabled'),
|
||||
makeExtDef('c', 'disabled'),
|
||||
],
|
||||
}),
|
||||
createExtensionOverrides({
|
||||
extensions: [cOverride, bOverride, aOverride],
|
||||
extensions: [
|
||||
makeExtDef('test/c', 'disabled'),
|
||||
makeExtDef('test/b', 'disabled'),
|
||||
makeExtDef('test/a', 'disabled'),
|
||||
],
|
||||
}),
|
||||
],
|
||||
builtinExtensions: [],
|
||||
parameters: ['b', 'c', 'a'].map(id => ({ id, disabled: false })),
|
||||
parameters: ['test/b', 'test/c', 'test/a'].map(id => ({
|
||||
id,
|
||||
disabled: false,
|
||||
})),
|
||||
});
|
||||
|
||||
expect(result.map(r => r.extension.id)).toEqual(['b', 'c', 'a']);
|
||||
expect(result.map(r => r.extension.id)).toEqual([
|
||||
'test/b',
|
||||
'test/c',
|
||||
'test/a',
|
||||
]);
|
||||
});
|
||||
|
||||
it('throws an error when a forbidden extension is overridden by a plugin', () => {
|
||||
expect(() =>
|
||||
resolveAppNodeSpecs({
|
||||
features: [
|
||||
createPlugin({ id: 'test', extensions: [makeExt('forbidden')] }),
|
||||
createPlugin({ id: 'test', extensions: [makeExtDef('forbidden')] }),
|
||||
],
|
||||
builtinExtensions: [],
|
||||
parameters: [],
|
||||
forbidden: new Set(['forbidden']),
|
||||
forbidden: new Set(['test/forbidden']),
|
||||
}),
|
||||
).toThrow(
|
||||
"It is forbidden to override the following extension(s): 'forbidden', which is done by the following plugin(s): 'test'",
|
||||
"It is forbidden to override the following extension(s): 'test/forbidden', which is done by the following plugin(s): 'test'",
|
||||
);
|
||||
});
|
||||
|
||||
@@ -271,7 +304,7 @@ describe('resolveAppNodeSpecs', () => {
|
||||
expect(() =>
|
||||
resolveAppNodeSpecs({
|
||||
features: [
|
||||
createExtensionOverrides({ extensions: [makeExt('forbidden')] }),
|
||||
createExtensionOverrides({ extensions: [makeExtDef('forbidden')] }),
|
||||
],
|
||||
builtinExtensions: [],
|
||||
parameters: [],
|
||||
|
||||
@@ -16,15 +16,17 @@
|
||||
|
||||
import { createExtension } from '@backstage/frontend-plugin-api';
|
||||
import { resolveAppTree } from './resolveAppTree';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
|
||||
const extBaseConfig = {
|
||||
id: 'test',
|
||||
attachTo: { id: 'nonexistent', input: 'nonexistent' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
};
|
||||
|
||||
const extension = createExtension(extBaseConfig);
|
||||
const extension = resolveExtensionDefinition(
|
||||
createExtension({
|
||||
name: 'test',
|
||||
attachTo: { id: 'nonexistent', input: 'nonexistent' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
);
|
||||
|
||||
const baseSpec = {
|
||||
extension,
|
||||
|
||||
@@ -36,7 +36,10 @@ describe('createApp', () => {
|
||||
configLoader: async () =>
|
||||
new MockConfigApi({
|
||||
app: {
|
||||
extensions: [{ 'themes.light': false }, { 'themes.dark': false }],
|
||||
extensions: [
|
||||
{ 'theme:app/light': false },
|
||||
{ 'theme:app/dark': false },
|
||||
],
|
||||
},
|
||||
}),
|
||||
features: [
|
||||
@@ -68,7 +71,6 @@ describe('createApp', () => {
|
||||
id: duplicatedFeatureId,
|
||||
extensions: [
|
||||
createPageExtension({
|
||||
id: 'test.page.first',
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>First Page</div>,
|
||||
}),
|
||||
@@ -78,7 +80,6 @@ describe('createApp', () => {
|
||||
id: duplicatedFeatureId,
|
||||
extensions: [
|
||||
createPageExtension({
|
||||
id: 'test.page.last',
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>Last Page</div>,
|
||||
}),
|
||||
@@ -106,7 +107,7 @@ describe('createApp', () => {
|
||||
featureFlags: [{ name: 'test-1' }],
|
||||
extensions: [
|
||||
createExtension({
|
||||
id: 'test.page.first',
|
||||
name: 'first',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
output: { element: coreExtensionData.reactElement },
|
||||
factory() {
|
||||
@@ -131,7 +132,8 @@ describe('createApp', () => {
|
||||
featureFlags: [{ name: 'test-2' }],
|
||||
extensions: [
|
||||
createExtension({
|
||||
id: 'core.router',
|
||||
namespace: 'core',
|
||||
name: 'router',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
disabled: true,
|
||||
output: {},
|
||||
@@ -159,7 +161,6 @@ describe('createApp', () => {
|
||||
id: 'my-plugin',
|
||||
extensions: [
|
||||
createPageExtension({
|
||||
id: 'plugin.my-plugin.page',
|
||||
defaultPath: '/',
|
||||
loader: async () => {
|
||||
const Component = () => {
|
||||
@@ -182,32 +183,32 @@ describe('createApp', () => {
|
||||
expect(String(tree.root)).toMatchInlineSnapshot(`
|
||||
"<core out=[core.reactElement]>
|
||||
root [
|
||||
<core.router out=[core.reactElement]>
|
||||
<core/router out=[core.reactElement]>
|
||||
children [
|
||||
<core.layout out=[core.reactElement]>
|
||||
<core/layout out=[core.reactElement]>
|
||||
content [
|
||||
<core.routes out=[core.reactElement]>
|
||||
<core/routes out=[core.reactElement]>
|
||||
routes [
|
||||
<plugin.my-plugin.page out=[core.routing.path, core.routing.ref, core.reactElement] />
|
||||
<page:my-plugin out=[core.routing.path, core.routing.ref, core.reactElement] />
|
||||
]
|
||||
</core.routes>
|
||||
</core/routes>
|
||||
]
|
||||
nav [
|
||||
<core.nav out=[core.reactElement] />
|
||||
<core/nav out=[core.reactElement] />
|
||||
]
|
||||
</core.layout>
|
||||
</core/layout>
|
||||
]
|
||||
</core.router>
|
||||
</core/router>
|
||||
]
|
||||
components [
|
||||
<core.components.progress out=[component.ref] />
|
||||
<core.components.errorBoundaryFallback out=[component.ref] />
|
||||
<core.components.bootErrorPage out=[component.ref] />
|
||||
<core.components.notFoundErrorPage out=[component.ref] />
|
||||
<component:core.components.progress out=[component.ref] />
|
||||
<component:core.components.errorBoundaryFallback out=[component.ref] />
|
||||
<component:core.components.bootErrorPage out=[component.ref] />
|
||||
<component:core.components.notFoundErrorPage out=[component.ref] />
|
||||
]
|
||||
themes [
|
||||
<themes.light out=[core.theme] />
|
||||
<themes.dark out=[core.theme] />
|
||||
<theme:app/light out=[core.theme] />
|
||||
<theme:app/dark out=[core.theme] />
|
||||
]
|
||||
</core>"
|
||||
`);
|
||||
|
||||
@@ -73,6 +73,8 @@ import { AppLanguageSelector } from '../../../core-app-api/src/apis/implementati
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { I18nextTranslationApi } from '../../../core-app-api/src/apis/implementations/TranslationApi/I18nextTranslationApi';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import {
|
||||
apis as defaultApis,
|
||||
components as defaultComponents,
|
||||
@@ -119,7 +121,7 @@ export const builtinExtensions = [
|
||||
DefaultNotFoundErrorPageComponent,
|
||||
LightTheme,
|
||||
DarkTheme,
|
||||
];
|
||||
].map(def => resolveExtensionDefinition(def));
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionTreeNode {
|
||||
@@ -174,7 +176,7 @@ export function createExtensionTree(options: {
|
||||
);
|
||||
},
|
||||
getRootRoutes(): JSX.Element[] {
|
||||
return this.getExtensionAttachments('core.routes', 'routes').map(node => {
|
||||
return this.getExtensionAttachments('core/routes', 'routes').map(node => {
|
||||
const path = node.getData(coreExtensionData.routePath);
|
||||
const element = node.getData(coreExtensionData.reactElement);
|
||||
const routeRef = node.getData(coreExtensionData.routeRef);
|
||||
@@ -201,7 +203,7 @@ export function createExtensionTree(options: {
|
||||
);
|
||||
};
|
||||
|
||||
return this.getExtensionAttachments('core.nav', 'items')
|
||||
return this.getExtensionAttachments('core/nav', 'items')
|
||||
.map((node, index) => {
|
||||
const target = node.getData(coreExtensionData.navTarget);
|
||||
if (!target) {
|
||||
|
||||
@@ -392,7 +392,7 @@ export function createApiExtension<
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
inputs?: TInputs;
|
||||
},
|
||||
): Extension<TConfig>;
|
||||
): ExtensionDefinition<TConfig>;
|
||||
|
||||
export { createApiFactory };
|
||||
|
||||
@@ -405,6 +405,7 @@ export function createComponentExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
ref: TRef;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
@@ -421,7 +422,7 @@ export function createComponentExtension<
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => TRef['T'];
|
||||
};
|
||||
}): Extension<TConfig>;
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtension<
|
||||
@@ -430,7 +431,7 @@ export function createExtension<
|
||||
TConfig = never,
|
||||
>(
|
||||
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
|
||||
): Extension<TConfig>;
|
||||
): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionDataRef<TData>(
|
||||
@@ -477,10 +478,14 @@ export interface CreateExtensionOptions<
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
inputs?: TInputs;
|
||||
// (undocumented)
|
||||
kind?: string;
|
||||
// (undocumented)
|
||||
name?: string;
|
||||
// (undocumented)
|
||||
namespace?: string;
|
||||
// (undocumented)
|
||||
output: TOutput;
|
||||
}
|
||||
|
||||
@@ -516,11 +521,12 @@ export function createExternalRouteRef<
|
||||
|
||||
// @public
|
||||
export function createNavItemExtension(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
routeRef: RouteRef<undefined>;
|
||||
title: string;
|
||||
icon: IconComponent_2;
|
||||
}): Extension<{
|
||||
}): ExtensionDefinition<{
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
@@ -539,7 +545,8 @@ export function createPageExtension<
|
||||
configSchema: PortableSchema<TConfig>;
|
||||
}
|
||||
) & {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
@@ -552,7 +559,7 @@ export function createPageExtension<
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
},
|
||||
): Extension<TConfig>;
|
||||
): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createPlugin<
|
||||
@@ -592,7 +599,8 @@ export function createSignInPageExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
@@ -604,7 +612,7 @@ export function createSignInPageExtension<
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<ComponentType<SignInPageProps>>;
|
||||
}): Extension<TConfig>;
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public
|
||||
export function createSubRouteRef<
|
||||
@@ -616,7 +624,9 @@ export function createSubRouteRef<
|
||||
}): MakeSubRouteRef<PathParams<Path>, ParentParams>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createThemeExtension(theme: AppTheme): Extension<never>;
|
||||
export function createThemeExtension(
|
||||
theme: AppTheme,
|
||||
): ExtensionDefinition<never>;
|
||||
|
||||
export { DiscoveryApi };
|
||||
|
||||
@@ -703,6 +713,40 @@ export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
|
||||
: never]?: TExtensionData[DataName]['T'];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ExtensionDefinition<TConfig> {
|
||||
// (undocumented)
|
||||
$$type: '@backstage/ExtensionDefinition';
|
||||
// (undocumented)
|
||||
attachTo: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
// (undocumented)
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
// (undocumented)
|
||||
disabled: boolean;
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Record<
|
||||
string,
|
||||
undefined | Record<string, unknown> | Array<Record<string, unknown>>
|
||||
>;
|
||||
}): ExtensionDataValues<any>;
|
||||
// (undocumented)
|
||||
inputs: AnyExtensionInputMap;
|
||||
// (undocumented)
|
||||
kind?: string;
|
||||
// (undocumented)
|
||||
name?: string;
|
||||
// (undocumented)
|
||||
namespace?: string;
|
||||
// (undocumented)
|
||||
output: AnyExtensionDataMap;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ExtensionInput<
|
||||
TExtensionData extends AnyExtensionDataMap,
|
||||
@@ -743,7 +787,7 @@ export interface ExtensionOverrides {
|
||||
// @public (undocumented)
|
||||
export interface ExtensionOverridesOptions {
|
||||
// (undocumented)
|
||||
extensions: Extension<unknown>[];
|
||||
extensions: ExtensionDefinition<unknown>[];
|
||||
// (undocumented)
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
@@ -841,7 +885,7 @@ export interface PluginOptions<
|
||||
ExternalRoutes extends AnyExternalRoutes,
|
||||
> {
|
||||
// (undocumented)
|
||||
extensions?: Extension<unknown>[];
|
||||
extensions?: ExtensionDefinition<unknown>[];
|
||||
// (undocumented)
|
||||
externalRoutes?: ExternalRoutes;
|
||||
// (undocumented)
|
||||
|
||||
@@ -24,11 +24,10 @@ import { createRouteRef } from '../routing';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
|
||||
const wrapInBoundaryExtension = (element: JSX.Element) => {
|
||||
const id = 'plugin.extension';
|
||||
const routeRef = createRouteRef();
|
||||
return createExtension({
|
||||
id,
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
name: 'test',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath,
|
||||
@@ -84,15 +83,18 @@ describe('ExtensionBoundary', () => {
|
||||
),
|
||||
).render();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(analyticsApiMock.getEvents()[0]).toMatchObject({
|
||||
await waitFor(() => {
|
||||
const event = analyticsApiMock
|
||||
.getEvents()
|
||||
.find(e => e.subject === subject);
|
||||
|
||||
expect(event).toMatchObject({
|
||||
action,
|
||||
subject,
|
||||
context: {
|
||||
extension: 'plugin.extension',
|
||||
routeRef: 'unknown',
|
||||
extensionId: 'test',
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -59,7 +59,7 @@ export function ExtensionBoundary(props: ExtensionBoundaryProps) {
|
||||
|
||||
// Skipping "routeRef" attribute in the new system, the extension "id" should provide more insight
|
||||
const attributes = {
|
||||
extension: node.spec.id,
|
||||
extensionId: node.spec.id,
|
||||
pluginId: node.spec.source?.id,
|
||||
};
|
||||
|
||||
|
||||
@@ -26,13 +26,14 @@ describe('createApiExtension', () => {
|
||||
factory: () => ({ foo: 'bar' }),
|
||||
});
|
||||
|
||||
const extension = createApiExtension({
|
||||
factory,
|
||||
});
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'apis.test',
|
||||
expect(
|
||||
createApiExtension({
|
||||
factory,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
kind: 'api',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
configSchema: undefined,
|
||||
@@ -65,8 +66,9 @@ describe('createApiExtension', () => {
|
||||
});
|
||||
// boo
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'apis.test',
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
kind: 'api',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
disabled: false,
|
||||
configSchema: undefined,
|
||||
|
||||
@@ -51,7 +51,10 @@ export function createApiExtension<
|
||||
'api' in options ? options.api : (factory as { api: AnyApiRef }).api;
|
||||
|
||||
return createExtension({
|
||||
id: `apis.${apiRef.id}`,
|
||||
kind: 'api',
|
||||
// Since ApiRef IDs use a global namespace we use the namespace here in order to override
|
||||
// potential plugin IDs and always end up with the format `api:<api-ref-id>`
|
||||
namespace: apiRef.id,
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
inputs: extensionInputs,
|
||||
configSchema,
|
||||
|
||||
@@ -32,6 +32,7 @@ export function createComponentExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
ref: TRef;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
@@ -49,9 +50,10 @@ export function createComponentExtension<
|
||||
}) => TRef['T'];
|
||||
};
|
||||
}) {
|
||||
const id = options.ref.id;
|
||||
return createExtension({
|
||||
id,
|
||||
kind: 'component',
|
||||
namespace: options.ref.id,
|
||||
name: options.name,
|
||||
attachTo: { id: 'core', input: 'components' },
|
||||
inputs: options.inputs,
|
||||
disabled: options.disabled,
|
||||
|
||||
@@ -24,15 +24,18 @@ import { RouteRef } from '../routing';
|
||||
* @public
|
||||
*/
|
||||
export function createNavItemExtension(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
routeRef: RouteRef<undefined>;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
}) {
|
||||
const { id, routeRef, title, icon } = options;
|
||||
const { routeRef, title, icon, namespace, name } = options;
|
||||
return createExtension({
|
||||
id,
|
||||
attachTo: { id: 'core.nav', input: 'items' },
|
||||
namespace,
|
||||
name,
|
||||
kind: 'nav-item',
|
||||
attachTo: { id: 'core/nav', input: 'items' },
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
title: z.string().default(title),
|
||||
|
||||
@@ -25,14 +25,15 @@ describe('createNavLogoExtension', () => {
|
||||
it('creates the extension properly', () => {
|
||||
expect(
|
||||
createNavLogoExtension({
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
logoFull: <div>Logo Full</div>,
|
||||
logoIcon: <div>Logo Icon</div>,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'test',
|
||||
attachTo: { id: 'core.nav', input: 'logos' },
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
kind: 'nav-logo',
|
||||
name: 'test',
|
||||
attachTo: { id: 'core/nav', input: 'logos' },
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
|
||||
@@ -21,14 +21,17 @@ import { coreExtensionData, createExtension } from '../wiring';
|
||||
* @public
|
||||
*/
|
||||
export function createNavLogoExtension(options: {
|
||||
id: string;
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
logoIcon: JSX.Element;
|
||||
logoFull: JSX.Element;
|
||||
}) {
|
||||
const { id, logoIcon, logoFull } = options;
|
||||
const { logoIcon, logoFull } = options;
|
||||
return createExtension({
|
||||
id,
|
||||
attachTo: { id: 'core.nav', input: 'logos' },
|
||||
kind: 'nav-logo',
|
||||
name: options?.name,
|
||||
namespace: options?.namespace,
|
||||
attachTo: { id: 'core/nav', input: 'logos' },
|
||||
output: {
|
||||
logos: coreExtensionData.logoElements,
|
||||
},
|
||||
|
||||
@@ -36,14 +36,15 @@ describe('createPageExtension', () => {
|
||||
|
||||
expect(
|
||||
createPageExtension({
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
configSchema,
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'test',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
name: 'test',
|
||||
kind: 'page',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
@@ -57,7 +58,7 @@ describe('createPageExtension', () => {
|
||||
|
||||
expect(
|
||||
createPageExtension({
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
attachTo: { id: 'other', input: 'place' },
|
||||
disabled: true,
|
||||
configSchema,
|
||||
@@ -69,8 +70,9 @@ describe('createPageExtension', () => {
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'test',
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
name: 'test',
|
||||
kind: 'page',
|
||||
attachTo: { id: 'other', input: 'place' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: true,
|
||||
@@ -89,14 +91,15 @@ describe('createPageExtension', () => {
|
||||
|
||||
expect(
|
||||
createPageExtension({
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
defaultPath: '/here',
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/Extension',
|
||||
id: 'test',
|
||||
attachTo: { id: 'core.routes', input: 'routes' },
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
name: 'test',
|
||||
kind: 'page',
|
||||
attachTo: { id: 'core/routes', input: 'routes' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
@@ -118,7 +121,6 @@ describe('createPageExtension', () => {
|
||||
|
||||
createExtensionTester(
|
||||
createPageExtension({
|
||||
id: 'plugin.page',
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>Component</div>,
|
||||
}),
|
||||
|
||||
@@ -20,12 +20,12 @@ import { createSchemaFromZod, PortableSchema } from '../schema';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
Extension,
|
||||
ExtensionInputValues,
|
||||
AnyExtensionInputMap,
|
||||
} from '../wiring';
|
||||
import { RouteRef } from '../routing';
|
||||
import { Expand } from '../types';
|
||||
import { ExtensionDefinition } from '../wiring/createExtension';
|
||||
|
||||
/**
|
||||
* Helper for creating extensions for a routable React page component.
|
||||
@@ -44,7 +44,8 @@ export function createPageExtension<
|
||||
configSchema: PortableSchema<TConfig>;
|
||||
}
|
||||
) & {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
@@ -54,9 +55,7 @@ export function createPageExtension<
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
},
|
||||
): Extension<TConfig> {
|
||||
const { id } = options;
|
||||
|
||||
): ExtensionDefinition<TConfig> {
|
||||
const configSchema =
|
||||
'configSchema' in options
|
||||
? options.configSchema
|
||||
@@ -65,8 +64,10 @@ export function createPageExtension<
|
||||
) as PortableSchema<TConfig>);
|
||||
|
||||
return createExtension({
|
||||
id,
|
||||
attachTo: options.attachTo ?? { id: 'core.routes', input: 'routes' },
|
||||
kind: 'page',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? { id: 'core/routes', input: 'routes' },
|
||||
configSchema,
|
||||
inputs: options.inputs,
|
||||
disabled: options.disabled,
|
||||
|
||||
@@ -23,13 +23,13 @@ import { coreExtensionData, createExtension } from '../wiring';
|
||||
describe('createSignInPageExtension', () => {
|
||||
it('renders a sign-in page', async () => {
|
||||
const SignInPage = createSignInPageExtension({
|
||||
id: 'test',
|
||||
name: 'test',
|
||||
loader: async () => () => <div data-testid="sign-in-page" />,
|
||||
});
|
||||
|
||||
createExtensionTester(
|
||||
createExtension({
|
||||
id: 'dummy',
|
||||
name: 'dummy',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
|
||||
@@ -19,10 +19,10 @@ import { ExtensionBoundary } from '../components';
|
||||
import { PortableSchema } from '../schema';
|
||||
import {
|
||||
createExtension,
|
||||
Extension,
|
||||
ExtensionInputValues,
|
||||
AnyExtensionInputMap,
|
||||
createExtensionDataRef,
|
||||
ExtensionDefinition,
|
||||
} from '../wiring';
|
||||
import { Expand } from '../types';
|
||||
import { SignInPageProps } from '@backstage/core-plugin-api';
|
||||
@@ -39,7 +39,8 @@ export function createSignInPageExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
@@ -48,12 +49,12 @@ export function createSignInPageExtension<
|
||||
config: TConfig;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<ComponentType<SignInPageProps>>;
|
||||
}): Extension<TConfig> {
|
||||
const { id } = options;
|
||||
|
||||
}): ExtensionDefinition<TConfig> {
|
||||
return createExtension({
|
||||
id,
|
||||
attachTo: options.attachTo ?? { id: 'core.router', input: 'signInPage' },
|
||||
kind: 'sign-in-page',
|
||||
namespace: options?.namespace,
|
||||
name: options?.name,
|
||||
attachTo: options.attachTo ?? { id: 'core/router', input: 'signInPage' },
|
||||
configSchema: options.configSchema,
|
||||
inputs: options.inputs,
|
||||
disabled: options.disabled,
|
||||
|
||||
@@ -20,7 +20,9 @@ import { AppTheme } from '@backstage/core-plugin-api';
|
||||
/** @public */
|
||||
export function createThemeExtension(theme: AppTheme) {
|
||||
return createExtension({
|
||||
id: `themes.${theme.id}`,
|
||||
kind: 'theme',
|
||||
namespace: 'app',
|
||||
name: theme.id,
|
||||
attachTo: { id: 'core', input: 'themes' },
|
||||
output: {
|
||||
theme: coreExtensionData.theme,
|
||||
|
||||
@@ -25,7 +25,7 @@ function unused(..._any: any[]) {}
|
||||
describe('createExtension', () => {
|
||||
it('should create an extension with a simple output', () => {
|
||||
const baseConfig = {
|
||||
id: 'test',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringData,
|
||||
@@ -39,7 +39,7 @@ describe('createExtension', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(extension.id).toBe('test');
|
||||
expect(extension.namespace).toBe('test');
|
||||
|
||||
// When declared as an error function without a block the TypeScript errors
|
||||
// are a more specific and will point at the property that is problematic.
|
||||
@@ -163,7 +163,7 @@ describe('createExtension', () => {
|
||||
|
||||
it('should create an extension with a some optional output', () => {
|
||||
const baseConfig = {
|
||||
id: 'test',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringData,
|
||||
@@ -176,7 +176,7 @@ describe('createExtension', () => {
|
||||
foo: 'bar',
|
||||
}),
|
||||
});
|
||||
expect(extension.id).toBe('test');
|
||||
expect(extension.namespace).toBe('test');
|
||||
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
@@ -233,7 +233,7 @@ describe('createExtension', () => {
|
||||
|
||||
it('should create an extension with input', () => {
|
||||
const extension = createExtension({
|
||||
id: 'test',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
mixed: createExtensionInput({
|
||||
@@ -286,6 +286,6 @@ describe('createExtension', () => {
|
||||
};
|
||||
},
|
||||
});
|
||||
expect(extension.id).toBe('test');
|
||||
expect(extension.namespace).toBe('test');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -73,7 +73,9 @@ export interface CreateExtensionOptions<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
> {
|
||||
id: string;
|
||||
kind?: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
@@ -86,6 +88,27 @@ export interface CreateExtensionOptions<
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionDefinition<TConfig> {
|
||||
$$type: '@backstage/ExtensionDefinition';
|
||||
kind?: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo: { id: string; input: string };
|
||||
disabled: boolean;
|
||||
inputs: AnyExtensionInputMap;
|
||||
output: AnyExtensionDataMap;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Record<
|
||||
string,
|
||||
undefined | Record<string, unknown> | Array<Record<string, unknown>>
|
||||
>;
|
||||
}): ExtensionDataValues<any>;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Extension<TConfig> {
|
||||
$$type: '@backstage/Extension';
|
||||
@@ -112,12 +135,17 @@ export function createExtension<
|
||||
TConfig = never,
|
||||
>(
|
||||
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
|
||||
): Extension<TConfig> {
|
||||
): ExtensionDefinition<TConfig> {
|
||||
return {
|
||||
...options,
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
kind: options.kind,
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo,
|
||||
disabled: options.disabled ?? false,
|
||||
$$type: '@backstage/Extension',
|
||||
inputs: options.inputs ?? {},
|
||||
output: options.output,
|
||||
configSchema: options.configSchema,
|
||||
factory({ inputs, ...rest }) {
|
||||
// TODO: Simplify this, but TS wouldn't infer the input type for some reason
|
||||
return options.factory({
|
||||
|
||||
@@ -37,7 +37,21 @@ describe('createExtensionOverrides', () => {
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
createExtension({
|
||||
id: 'a',
|
||||
name: 'a',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
createExtension({
|
||||
namespace: 'b',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
createExtension({
|
||||
kind: 'k',
|
||||
namespace: 'c',
|
||||
name: 'n',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
@@ -54,12 +68,39 @@ describe('createExtensionOverrides', () => {
|
||||
"id": "core",
|
||||
"input": "apis",
|
||||
},
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"id": "a",
|
||||
"inputs": {},
|
||||
"output": {},
|
||||
},
|
||||
{
|
||||
"$$type": "@backstage/Extension",
|
||||
"attachTo": {
|
||||
"id": "core",
|
||||
"input": "apis",
|
||||
},
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"id": "b",
|
||||
"inputs": {},
|
||||
"output": {},
|
||||
},
|
||||
{
|
||||
"$$type": "@backstage/Extension",
|
||||
"attachTo": {
|
||||
"id": "core",
|
||||
"input": "apis",
|
||||
},
|
||||
"configSchema": undefined,
|
||||
"disabled": false,
|
||||
"factory": [Function],
|
||||
"id": "k:c/n",
|
||||
"inputs": {},
|
||||
"output": {},
|
||||
},
|
||||
],
|
||||
"featureFlags": [],
|
||||
"version": "v1",
|
||||
@@ -71,7 +112,7 @@ describe('createExtensionOverrides', () => {
|
||||
const overrides = createExtensionOverrides({
|
||||
extensions: [
|
||||
createExtension({
|
||||
id: 'a',
|
||||
namespace: 'a',
|
||||
attachTo: { id: 'core', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
|
||||
@@ -14,12 +14,13 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Extension } from './createExtension';
|
||||
import { Extension, ExtensionDefinition } from './createExtension';
|
||||
import { resolveExtensionDefinition } from './resolveExtensionDefinition';
|
||||
import { FeatureFlagConfig } from './types';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionOverridesOptions {
|
||||
extensions: Extension<unknown>[];
|
||||
extensions: ExtensionDefinition<unknown>[];
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
@@ -42,7 +43,7 @@ export function createExtensionOverrides(
|
||||
return {
|
||||
$$type: '@backstage/ExtensionOverrides',
|
||||
version: 'v1',
|
||||
extensions: options.extensions,
|
||||
extensions: options.extensions.map(def => resolveExtensionDefinition(def)),
|
||||
featureFlags: options.featureFlags ?? [],
|
||||
} as InternalExtensionOverrides;
|
||||
}
|
||||
|
||||
@@ -28,48 +28,34 @@ import { createExtensionInput } from './createExtensionInput';
|
||||
|
||||
const nameExtensionDataRef = createExtensionDataRef<string>('name');
|
||||
|
||||
const TechRadarPage = createExtension({
|
||||
id: 'plugin.techradar.page',
|
||||
attachTo: { id: 'test.output', input: 'names' },
|
||||
const Extension1 = createExtension({
|
||||
name: '1',
|
||||
attachTo: { id: 'test/output', input: 'names' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
factory() {
|
||||
return { name: 'TechRadar' };
|
||||
return { name: 'extension-1' };
|
||||
},
|
||||
});
|
||||
|
||||
const CatalogPage = createExtension({
|
||||
id: 'plugin.catalog.page',
|
||||
attachTo: { id: 'test.output', input: 'names' },
|
||||
const Extension2 = createExtension({
|
||||
name: '2',
|
||||
attachTo: { id: 'test/output', input: 'names' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('Catalog') }),
|
||||
z.object({ name: z.string().default('extension-2') }),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
},
|
||||
});
|
||||
|
||||
const TechDocsAddon = createExtension({
|
||||
id: 'plugin.techdocs.addon.example',
|
||||
attachTo: { id: 'plugin.techdocs.page', input: 'addons' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('TechDocsAddon') }),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
},
|
||||
});
|
||||
|
||||
const TechDocsPage = createExtension({
|
||||
id: 'plugin.techdocs.page',
|
||||
attachTo: { id: 'test.output', input: 'names' },
|
||||
const Extension3 = createExtension({
|
||||
name: '3',
|
||||
attachTo: { id: 'test/output', input: 'names' },
|
||||
inputs: {
|
||||
addons: createExtensionInput({
|
||||
name: nameExtensionDataRef,
|
||||
@@ -79,12 +65,40 @@ const TechDocsPage = createExtension({
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` };
|
||||
return { name: `extension-3:${inputs.addons.map(n => n.name).join('-')}` };
|
||||
},
|
||||
});
|
||||
|
||||
const Child = createExtension({
|
||||
name: 'child',
|
||||
attachTo: { id: 'test/3', input: 'addons' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('child') }),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
},
|
||||
});
|
||||
|
||||
const Child2 = createExtension({
|
||||
name: 'child2',
|
||||
attachTo: { id: 'test/3', input: 'addons' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('child2') }),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
},
|
||||
});
|
||||
|
||||
const outputExtension = createExtension({
|
||||
id: 'test.output',
|
||||
name: 'output',
|
||||
attachTo: { id: 'core', input: 'root' },
|
||||
inputs: {
|
||||
names: createExtensionInput({
|
||||
@@ -118,40 +132,34 @@ function createTestAppRoot({
|
||||
|
||||
describe('createPlugin', () => {
|
||||
it('should create an empty plugin', () => {
|
||||
const plugin = createPlugin({ id: 'empty' });
|
||||
const plugin = createPlugin({ id: 'test' });
|
||||
|
||||
expect(plugin).toBeDefined();
|
||||
});
|
||||
|
||||
it('should create a plugin with extension instances', async () => {
|
||||
const plugin = createPlugin({
|
||||
id: 'empty',
|
||||
extensions: [TechRadarPage, CatalogPage, outputExtension],
|
||||
id: 'test',
|
||||
extensions: [Extension1, Extension2, outputExtension],
|
||||
});
|
||||
expect(plugin).toBeDefined();
|
||||
|
||||
await renderWithEffects(
|
||||
createTestAppRoot({
|
||||
features: [plugin],
|
||||
config: { app: { extensions: [{ 'core.router': false }] } },
|
||||
config: { app: { extensions: [{ 'core/router': false }] } },
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText('Names: TechRadar, Catalog'),
|
||||
screen.findByText('Names: extension-1, extension-2'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should create a plugin with nested extension instances', async () => {
|
||||
const plugin = createPlugin({
|
||||
id: 'empty',
|
||||
extensions: [
|
||||
TechRadarPage,
|
||||
CatalogPage,
|
||||
TechDocsPage,
|
||||
TechDocsAddon,
|
||||
outputExtension,
|
||||
],
|
||||
id: 'test',
|
||||
extensions: [Extension1, Extension2, Extension3, Child, outputExtension],
|
||||
});
|
||||
expect(plugin).toBeDefined();
|
||||
|
||||
@@ -161,10 +169,10 @@ describe('createPlugin', () => {
|
||||
config: {
|
||||
app: {
|
||||
extensions: [
|
||||
{ 'core.router': false },
|
||||
{ 'core/router': false },
|
||||
{
|
||||
'plugin.catalog.page': {
|
||||
config: { name: 'CatalogRenamed' },
|
||||
'test/2': {
|
||||
config: { name: 'extension-2-renamed' },
|
||||
},
|
||||
},
|
||||
],
|
||||
@@ -175,8 +183,63 @@ describe('createPlugin', () => {
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
'Names: TechRadar, CatalogRenamed, TechDocs-TechDocsAddon',
|
||||
'Names: extension-1, extension-2-renamed, extension-3:child',
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should create a plugin with nested extension instances and multiple children', async () => {
|
||||
const plugin = createPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
Extension1,
|
||||
Extension2,
|
||||
Extension3,
|
||||
Child,
|
||||
Child2,
|
||||
outputExtension,
|
||||
],
|
||||
});
|
||||
expect(plugin).toBeDefined();
|
||||
|
||||
await renderWithEffects(
|
||||
createTestAppRoot({
|
||||
features: [plugin],
|
||||
config: {
|
||||
app: {
|
||||
extensions: [{ 'core/router': false }],
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(
|
||||
screen.findByText(
|
||||
'Names: extension-1, extension-2, extension-3:child-child2',
|
||||
),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should throw on duplicate extensions', async () => {
|
||||
expect(() =>
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions: [Extension1, Extension1],
|
||||
}),
|
||||
).toThrow("Plugin 'test' provided duplicate extensions: test/1");
|
||||
|
||||
expect(() =>
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
extensions: [
|
||||
Extension1,
|
||||
Extension2,
|
||||
Extension2,
|
||||
Extension3,
|
||||
Extension3,
|
||||
Extension3,
|
||||
],
|
||||
}),
|
||||
).toThrow("Plugin 'test' provided duplicate extensions: test/2, test/3");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,9 +14,10 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Extension } from './createExtension';
|
||||
import { Extension, ExtensionDefinition } from './createExtension';
|
||||
import { ExternalRouteRef, RouteRef } from '../routing';
|
||||
import { FeatureFlagConfig } from './types';
|
||||
import { resolveExtensionDefinition } from './resolveExtensionDefinition';
|
||||
|
||||
/** @public */
|
||||
export type AnyRoutes = { [name in string]: RouteRef };
|
||||
@@ -32,7 +33,7 @@ export interface PluginOptions<
|
||||
id: string;
|
||||
routes?: Routes;
|
||||
externalRoutes?: ExternalRoutes;
|
||||
extensions?: Extension<unknown>[];
|
||||
extensions?: ExtensionDefinition<unknown>[];
|
||||
featureFlags?: FeatureFlagConfig[];
|
||||
}
|
||||
|
||||
@@ -64,14 +65,33 @@ export function createPlugin<
|
||||
>(
|
||||
options: PluginOptions<Routes, ExternalRoutes>,
|
||||
): BackstagePlugin<Routes, ExternalRoutes> {
|
||||
const extensions = (options.extensions ?? []).map(def =>
|
||||
resolveExtensionDefinition(def, { namespace: options.id }),
|
||||
);
|
||||
|
||||
const extensionIds = extensions.map(e => e.id);
|
||||
if (extensionIds.length !== new Set(extensionIds).size) {
|
||||
const duplicates = Array.from(
|
||||
new Set(
|
||||
extensionIds.filter((id, index) => extensionIds.indexOf(id) !== index),
|
||||
),
|
||||
);
|
||||
// TODO(Rugvip): This could provide some more information about the kind + name of the extensions
|
||||
throw new Error(
|
||||
`Plugin '${options.id}' provided duplicate extensions: ${duplicates.join(
|
||||
', ',
|
||||
)}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
$$type: '@backstage/BackstagePlugin',
|
||||
version: 'v1',
|
||||
id: options.id,
|
||||
routes: options.routes ?? ({} as Routes),
|
||||
externalRoutes: options.externalRoutes ?? ({} as ExternalRoutes),
|
||||
extensions: options.extensions ?? [],
|
||||
featureFlags: options.featureFlags ?? [],
|
||||
extensions,
|
||||
} as InternalBackstagePlugin<Routes, ExternalRoutes>;
|
||||
}
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ export {
|
||||
export {
|
||||
createExtension,
|
||||
type Extension,
|
||||
type ExtensionDefinition,
|
||||
type CreateExtensionOptions,
|
||||
type ExtensionDataValues,
|
||||
type ExtensionInputValues,
|
||||
|
||||
@@ -0,0 +1,48 @@
|
||||
/*
|
||||
* 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 { ExtensionDefinition } from './createExtension';
|
||||
import { resolveExtensionDefinition } from './resolveExtensionDefinition';
|
||||
|
||||
describe('resolveExtensionDefinition', () => {
|
||||
it.each([
|
||||
[{ namespace: 'ns' }, 'ns'],
|
||||
[{ namespace: 'n' }, 'n'],
|
||||
[{ namespace: 'ns', name: 'n' }, 'ns/n'],
|
||||
[{ kind: 'k', namespace: 'ns' }, 'k:ns'],
|
||||
[{ kind: 'k', namespace: 'ns', name: 'n' }, 'k:ns/n'],
|
||||
])(`should resolve extension IDs %s`, (definition, expected) => {
|
||||
const resolved = resolveExtensionDefinition(
|
||||
definition as ExtensionDefinition<unknown>,
|
||||
);
|
||||
expect(resolved.id).toBe(expected);
|
||||
});
|
||||
|
||||
it('should fail to resolve extension ID without namespace', () => {
|
||||
expect(() =>
|
||||
resolveExtensionDefinition({
|
||||
kind: 'k',
|
||||
} as ExtensionDefinition<unknown>),
|
||||
).toThrow(
|
||||
'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=k namespace=undefined name=undefined',
|
||||
);
|
||||
expect(() =>
|
||||
resolveExtensionDefinition({} as ExtensionDefinition<unknown>),
|
||||
).toThrow(
|
||||
'Extension must declare an explicit namespace or name as it could not be resolved from context, kind=undefined namespace=undefined name=undefined',
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* 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 { Extension, ExtensionDefinition } from './createExtension';
|
||||
|
||||
/** @internal */
|
||||
export function resolveExtensionDefinition<TConfig>(
|
||||
definition: ExtensionDefinition<TConfig>,
|
||||
context?: { namespace?: string },
|
||||
): Extension<TConfig> {
|
||||
const { name, kind, namespace: _, ...rest } = definition;
|
||||
const namespace = definition.namespace ?? context?.namespace;
|
||||
|
||||
const namePart =
|
||||
name && namespace ? `${namespace}/${name}` : namespace || name;
|
||||
if (!namePart) {
|
||||
throw new Error(
|
||||
`Extension must declare an explicit namespace or name as it could not be resolved from context, kind=${kind} namespace=${namespace} name=${name}`,
|
||||
);
|
||||
}
|
||||
|
||||
return {
|
||||
...rest,
|
||||
id: kind ? `${kind}:${namePart}` : namePart,
|
||||
$$type: '@backstage/Extension',
|
||||
};
|
||||
}
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
```ts
|
||||
import { ErrorWithContext } from '@backstage/test-utils';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { MockAnalyticsApi } from '@backstage/test-utils';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
@@ -24,7 +24,7 @@ import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionTester<TConfig>(
|
||||
subject: Extension<TConfig>,
|
||||
subject: ExtensionDefinition<TConfig>,
|
||||
options?: {
|
||||
config?: TConfig;
|
||||
},
|
||||
@@ -36,7 +36,7 @@ export { ErrorWithContext };
|
||||
export class ExtensionTester {
|
||||
// (undocumented)
|
||||
add<TConfig>(
|
||||
extension: Extension<TConfig>,
|
||||
extension: ExtensionDefinition<TConfig>,
|
||||
options?: {
|
||||
config?: TConfig;
|
||||
},
|
||||
|
||||
@@ -26,7 +26,7 @@ describe('createExtensionTester', () => {
|
||||
it('should render a simple extension', async () => {
|
||||
createExtensionTester(
|
||||
createExtension({
|
||||
id: 'test',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: { element: coreExtensionData.reactElement },
|
||||
factory: () => ({ element: <div>test</div> }),
|
||||
@@ -39,7 +39,7 @@ describe('createExtensionTester', () => {
|
||||
it('should render an extension even if disabled by default', async () => {
|
||||
createExtensionTester(
|
||||
createExtension({
|
||||
id: 'test',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
disabled: true,
|
||||
output: { element: coreExtensionData.reactElement },
|
||||
@@ -54,7 +54,7 @@ describe('createExtensionTester', () => {
|
||||
expect(() =>
|
||||
createExtensionTester(
|
||||
createExtension({
|
||||
id: 'test',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
disabled: true,
|
||||
output: { path: coreExtensionData.routePath },
|
||||
@@ -62,7 +62,7 @@ describe('createExtensionTester', () => {
|
||||
}),
|
||||
).render(),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.router', input 'children' did not receive required extension data 'core.reactElement' from extension 'test'",
|
||||
"Failed to instantiate extension 'core/router', input 'children' did not receive required extension data 'core.reactElement' from extension 'test'",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -15,7 +15,12 @@
|
||||
*/
|
||||
|
||||
import { createSpecializedApp } from '@backstage/frontend-app-api';
|
||||
import { Extension, createPlugin } from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
ExtensionDefinition,
|
||||
createExtensionOverrides,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { JsonArray, JsonObject, JsonValue } from '@backstage/types';
|
||||
import { RenderResult, render } from '@testing-library/react';
|
||||
@@ -24,7 +29,7 @@ import { RenderResult, render } from '@testing-library/react';
|
||||
export class ExtensionTester {
|
||||
/** @internal */
|
||||
static forSubject<TConfig>(
|
||||
subject: Extension<TConfig>,
|
||||
subject: ExtensionDefinition<TConfig>,
|
||||
options?: { config?: TConfig },
|
||||
): ExtensionTester {
|
||||
const tester = new ExtensionTester();
|
||||
@@ -33,16 +38,22 @@ export class ExtensionTester {
|
||||
}
|
||||
|
||||
readonly #extensions = new Array<{
|
||||
extension: Extension<any>;
|
||||
id: string;
|
||||
extension: ExtensionDefinition<any>;
|
||||
config?: JsonValue;
|
||||
}>();
|
||||
|
||||
add<TConfig>(
|
||||
extension: Extension<TConfig>,
|
||||
extension: ExtensionDefinition<TConfig>,
|
||||
options?: { config?: TConfig },
|
||||
): ExtensionTester {
|
||||
const withNamespace = {
|
||||
...extension,
|
||||
name: !extension.namespace && !extension.name ? 'test' : extension.name,
|
||||
};
|
||||
this.#extensions.push({
|
||||
extension,
|
||||
id: resolveExtensionDefinition(withNamespace).id,
|
||||
extension: withNamespace,
|
||||
config: options?.config as JsonValue,
|
||||
});
|
||||
|
||||
@@ -61,25 +72,25 @@ export class ExtensionTester {
|
||||
|
||||
const extensionsConfig: JsonArray = [
|
||||
...rest.map(entry => ({
|
||||
[entry.extension.id]: {
|
||||
[entry.id]: {
|
||||
config: entry.config,
|
||||
},
|
||||
})),
|
||||
{
|
||||
[subject.extension.id]: {
|
||||
attachTo: { id: 'core.router', input: 'children' },
|
||||
[subject.id]: {
|
||||
attachTo: { id: 'core/router', input: 'children' },
|
||||
config: subject.config,
|
||||
disabled: false,
|
||||
},
|
||||
},
|
||||
{
|
||||
'core.layout': false,
|
||||
'core/layout': false,
|
||||
},
|
||||
{
|
||||
'core.nav': false,
|
||||
'core/nav': false,
|
||||
},
|
||||
{
|
||||
'core.routes': false,
|
||||
'core/routes': false,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -93,8 +104,7 @@ export class ExtensionTester {
|
||||
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
createPlugin({
|
||||
id: 'test',
|
||||
createExtensionOverrides({
|
||||
extensions: this.#extensions.map(entry => entry.extension),
|
||||
}),
|
||||
],
|
||||
@@ -107,7 +117,7 @@ export class ExtensionTester {
|
||||
|
||||
/** @public */
|
||||
export function createExtensionTester<TConfig>(
|
||||
subject: Extension<TConfig>,
|
||||
subject: ExtensionDefinition<TConfig>,
|
||||
options?: { config?: TConfig },
|
||||
): ExtensionTester {
|
||||
return ExtensionTester.forSubject(subject, options);
|
||||
|
||||
@@ -4,11 +4,11 @@
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const AdrSearchResultListItemExtension: Extension<{
|
||||
export const AdrSearchResultListItemExtension: ExtensionDefinition<{
|
||||
lineClamp: number;
|
||||
noTrack: boolean;
|
||||
}>;
|
||||
|
||||
@@ -31,7 +31,6 @@ function isAdrDocument(result: any): result is AdrDocument {
|
||||
/** @alpha */
|
||||
export const AdrSearchResultListItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'adr',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
// TODO: Define how the icon can be configurable
|
||||
|
||||
@@ -38,7 +38,6 @@ import { catalogApiRef } from '@backstage/plugin-catalog-react';
|
||||
// TODO: It's currently possible to override the import page with a custom one. We need to decide
|
||||
// whether this type of override is typically done with an input or by overriding the entire extension.
|
||||
const CatalogImportPageExtension = createPageExtension({
|
||||
id: 'plugin.catalog-import.page',
|
||||
defaultPath: '/catalog-import',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
loader: () => import('./components/ImportPage').then(m => <m.ImportPage />),
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api';
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionInputValues } from '@backstage/frontend-plugin-api';
|
||||
import { ResourcePermission } from '@backstage/plugin-permission-common';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -17,7 +17,8 @@ import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
export function createEntityCardExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
@@ -30,7 +31,7 @@ export function createEntityCardExtension<
|
||||
loader: (options: {
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}): Extension<{
|
||||
}): ExtensionDefinition<{
|
||||
filter?: string | undefined;
|
||||
}>;
|
||||
|
||||
@@ -38,7 +39,8 @@ export function createEntityCardExtension<
|
||||
export function createEntityContentExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
@@ -54,7 +56,7 @@ export function createEntityContentExtension<
|
||||
loader: (options: {
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}): Extension<{
|
||||
}): ExtensionDefinition<{
|
||||
title: string;
|
||||
path: string;
|
||||
filter?: string | undefined;
|
||||
|
||||
@@ -50,7 +50,8 @@ export const entityFilterExpressionExtensionDataRef =
|
||||
export function createEntityCardExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
@@ -61,12 +62,12 @@ export function createEntityCardExtension<
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}) {
|
||||
const id = `entity.cards.${options.id}`;
|
||||
|
||||
return createExtension({
|
||||
id,
|
||||
kind: 'entity-card',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'entity.content.overview',
|
||||
id: 'entity-content:catalog/overview',
|
||||
input: 'cards',
|
||||
},
|
||||
disabled: options.disabled ?? true,
|
||||
@@ -104,7 +105,8 @@ export function createEntityCardExtension<
|
||||
export function createEntityContentExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
@@ -118,12 +120,12 @@ export function createEntityContentExtension<
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}) {
|
||||
const id = `entity.content.${options.id}`;
|
||||
|
||||
return createExtension({
|
||||
id,
|
||||
kind: 'entity-content',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'plugin.catalog.page.entity',
|
||||
id: 'page:catalog/entity',
|
||||
input: 'contents',
|
||||
},
|
||||
disabled: options.disabled ?? true,
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
|
||||
import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { PortableSchema } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -17,11 +17,12 @@ export function createCatalogFilterExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig = never,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
loader: (options: { config: TConfig }) => Promise<JSX.Element>;
|
||||
}): Extension<TConfig>;
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<
|
||||
|
||||
@@ -28,16 +28,17 @@ export function createCatalogFilterExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig = never,
|
||||
>(options: {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
loader: (options: { config: TConfig }) => Promise<JSX.Element>;
|
||||
}) {
|
||||
const id = `catalog.filter.${options.id}`;
|
||||
|
||||
return createExtension({
|
||||
id,
|
||||
attachTo: { id: 'plugin.catalog.page.index', input: 'filters' },
|
||||
kind: 'catalog-filter',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: { id: 'page:catalog', input: 'filters' },
|
||||
inputs: options.inputs ?? {},
|
||||
configSchema: options.configSchema,
|
||||
output: {
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { createEntityCardExtension } from '@backstage/plugin-catalog-react/alpha';
|
||||
|
||||
export const EntityAboutCard = createEntityCardExtension({
|
||||
id: 'about',
|
||||
name: 'about',
|
||||
loader: async () =>
|
||||
import('../components/AboutCard').then(m => (
|
||||
<m.AboutCard variant="gridItem" />
|
||||
@@ -26,7 +26,7 @@ export const EntityAboutCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityLinksCard = createEntityCardExtension({
|
||||
id: 'links',
|
||||
name: 'links',
|
||||
filter: 'has:links',
|
||||
loader: async () =>
|
||||
import('../components/EntityLinksCard').then(m => {
|
||||
@@ -35,7 +35,7 @@ export const EntityLinksCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityLabelsCard = createEntityCardExtension({
|
||||
id: 'labels',
|
||||
name: 'labels',
|
||||
filter: 'has:labels',
|
||||
loader: async () =>
|
||||
import('../components/EntityLabelsCard').then(m => (
|
||||
@@ -44,7 +44,7 @@ export const EntityLabelsCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityDependsOnComponentsCard = createEntityCardExtension({
|
||||
id: 'dependsOn.components',
|
||||
name: 'depends-on-components',
|
||||
loader: async () =>
|
||||
import('../components/DependsOnComponentsCard').then(m => (
|
||||
<m.DependsOnComponentsCard variant="gridItem" />
|
||||
@@ -52,7 +52,7 @@ export const EntityDependsOnComponentsCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityDependsOnResourcesCard = createEntityCardExtension({
|
||||
id: 'dependsOn.resources',
|
||||
name: 'depends-on-resources',
|
||||
loader: async () =>
|
||||
import('../components/DependsOnResourcesCard').then(m => (
|
||||
<m.DependsOnResourcesCard variant="gridItem" />
|
||||
@@ -60,7 +60,7 @@ export const EntityDependsOnResourcesCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityHasComponentsCard = createEntityCardExtension({
|
||||
id: 'has.components',
|
||||
name: 'has-components',
|
||||
loader: async () =>
|
||||
import('../components/HasComponentsCard').then(m => (
|
||||
<m.HasComponentsCard variant="gridItem" />
|
||||
@@ -68,7 +68,7 @@ export const EntityHasComponentsCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityHasResourcesCard = createEntityCardExtension({
|
||||
id: 'has.resources',
|
||||
name: 'has-resources',
|
||||
loader: async () =>
|
||||
import('../components/HasResourcesCard').then(m => (
|
||||
<m.HasResourcesCard variant="gridItem" />
|
||||
@@ -76,7 +76,7 @@ export const EntityHasResourcesCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityHasSubcomponentsCard = createEntityCardExtension({
|
||||
id: 'has.subcomponents',
|
||||
name: 'has-subcomponents',
|
||||
loader: async () =>
|
||||
import('../components/HasSubcomponentsCard').then(m => (
|
||||
<m.HasSubcomponentsCard variant="gridItem" />
|
||||
@@ -84,7 +84,7 @@ export const EntityHasSubcomponentsCard = createEntityCardExtension({
|
||||
});
|
||||
|
||||
export const EntityHasSystemsCard = createEntityCardExtension({
|
||||
id: 'has.systems',
|
||||
name: 'has-systems',
|
||||
loader: async () =>
|
||||
import('../components/HasSystemsCard').then(m => (
|
||||
<m.HasSystemsCard variant="gridItem" />
|
||||
|
||||
@@ -26,7 +26,7 @@ import {
|
||||
} from '@backstage/plugin-catalog-react/alpha';
|
||||
|
||||
export const OverviewEntityContent = createEntityContentExtension({
|
||||
id: 'overview',
|
||||
name: 'overview',
|
||||
defaultPath: '/',
|
||||
defaultTitle: 'Overview',
|
||||
disabled: false,
|
||||
|
||||
@@ -19,7 +19,7 @@ import { createCatalogFilterExtension } from './createCatalogFilterExtension';
|
||||
import { createSchemaFromZod } from '@backstage/frontend-plugin-api';
|
||||
|
||||
const CatalogEntityTagFilter = createCatalogFilterExtension({
|
||||
id: 'entity.tag',
|
||||
name: 'tag',
|
||||
loader: async () => {
|
||||
const { EntityTagPicker } = await import('@backstage/plugin-catalog-react');
|
||||
return <EntityTagPicker />;
|
||||
@@ -27,7 +27,7 @@ const CatalogEntityTagFilter = createCatalogFilterExtension({
|
||||
});
|
||||
|
||||
const CatalogEntityKindFilter = createCatalogFilterExtension({
|
||||
id: 'entity.kind',
|
||||
name: 'kind',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
initialFilter: z.string().default('component'),
|
||||
@@ -42,7 +42,7 @@ const CatalogEntityKindFilter = createCatalogFilterExtension({
|
||||
});
|
||||
|
||||
const CatalogEntityTypeFilter = createCatalogFilterExtension({
|
||||
id: 'entity.type',
|
||||
name: 'type',
|
||||
loader: async () => {
|
||||
const { EntityTypePicker } = await import(
|
||||
'@backstage/plugin-catalog-react'
|
||||
@@ -52,7 +52,7 @@ const CatalogEntityTypeFilter = createCatalogFilterExtension({
|
||||
});
|
||||
|
||||
const CatalogEntityOwnerFilter = createCatalogFilterExtension({
|
||||
id: 'entity.mode',
|
||||
name: 'mode',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
mode: z.enum(['owners-only', 'all']).optional(),
|
||||
@@ -67,7 +67,7 @@ const CatalogEntityOwnerFilter = createCatalogFilterExtension({
|
||||
});
|
||||
|
||||
const CatalogEntityNamespaceFilter = createCatalogFilterExtension({
|
||||
id: 'entity.namespace',
|
||||
name: 'namespace',
|
||||
loader: async () => {
|
||||
const { EntityNamespacePicker } = await import(
|
||||
'@backstage/plugin-catalog-react'
|
||||
@@ -77,7 +77,7 @@ const CatalogEntityNamespaceFilter = createCatalogFilterExtension({
|
||||
});
|
||||
|
||||
const CatalogEntityLifecycleFilter = createCatalogFilterExtension({
|
||||
id: 'entity.lifecycle',
|
||||
name: 'lifecycle',
|
||||
loader: async () => {
|
||||
const { EntityLifecyclePicker } = await import(
|
||||
'@backstage/plugin-catalog-react'
|
||||
@@ -87,7 +87,7 @@ const CatalogEntityLifecycleFilter = createCatalogFilterExtension({
|
||||
});
|
||||
|
||||
const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({
|
||||
id: 'entity.processing.status',
|
||||
name: 'processing.status',
|
||||
loader: async () => {
|
||||
const { EntityProcessingStatusPicker } = await import(
|
||||
'@backstage/plugin-catalog-react'
|
||||
@@ -97,7 +97,7 @@ const CatalogEntityProcessingStatusFilter = createCatalogFilterExtension({
|
||||
});
|
||||
|
||||
const CatalogUserListFilter = createCatalogFilterExtension({
|
||||
id: 'user.list',
|
||||
name: 'list',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
initialFilter: z.enum(['owned', 'starred', 'all']).default('owned'),
|
||||
|
||||
@@ -20,7 +20,6 @@ import { createNavItemExtension } from '@backstage/frontend-plugin-api';
|
||||
import { rootRouteRef } from '../routes';
|
||||
|
||||
export const CatalogIndexNavItem = createNavItemExtension({
|
||||
id: 'catalog.nav.index',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
title: 'Catalog',
|
||||
icon: HomeIcon,
|
||||
|
||||
@@ -30,7 +30,6 @@ import { rootRouteRef } from '../routes';
|
||||
import { useEntityFromUrl } from '../components/CatalogEntityPage/useEntityFromUrl';
|
||||
|
||||
export const CatalogIndexPage = createPageExtension({
|
||||
id: 'plugin.catalog.page.index',
|
||||
defaultPath: '/catalog',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
inputs: {
|
||||
@@ -46,7 +45,7 @@ export const CatalogIndexPage = createPageExtension({
|
||||
});
|
||||
|
||||
export const CatalogEntityPage = createPageExtension({
|
||||
id: 'plugin.catalog.page.entity',
|
||||
name: 'entity',
|
||||
defaultPath: '/catalog/:namespace/:kind/:name',
|
||||
routeRef: convertLegacyRouteRef(entityRouteRef),
|
||||
inputs: {
|
||||
|
||||
@@ -18,7 +18,6 @@ import { createSearchResultListItemExtension } from '@backstage/plugin-search-re
|
||||
|
||||
export const CatalogSearchResultListItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'catalog',
|
||||
predicate: result => result.type === 'software-catalog',
|
||||
component: () =>
|
||||
import('../components/CatalogSearchResultListItem').then(
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<{}, {}>;
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const ExploreSearchResultListItemExtension: Extension<{
|
||||
export const ExploreSearchResultListItemExtension: ExtensionDefinition<{
|
||||
noTrack?: boolean | undefined;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -20,7 +20,6 @@ import { createSearchResultListItemExtension } from '@backstage/plugin-search-re
|
||||
/** @alpha */
|
||||
export const ExploreSearchResultListItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'explore',
|
||||
predicate: result => result.type === 'tools',
|
||||
component: () =>
|
||||
import('./components/ToolSearchResultListItem').then(
|
||||
|
||||
@@ -4,20 +4,21 @@
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { GraphQLEndpoint } from '@backstage/plugin-graphiql';
|
||||
import { PortableSchema } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
export function createEndpointExtension<TConfig extends {}>(options: {
|
||||
id: string;
|
||||
export function createGraphiQLEndpointExtension<TConfig extends {}>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
factory: (options: { config: TConfig }) => {
|
||||
endpoint: GraphQLEndpoint;
|
||||
};
|
||||
}): Extension<TConfig>;
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<
|
||||
@@ -29,15 +30,15 @@ const _default: BackstagePlugin<
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const graphiqlBrowseApi: Extension<{}>;
|
||||
export const graphiqlBrowseApi: ExtensionDefinition<{}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const GraphiqlPage: Extension<{
|
||||
export const GraphiqlPage: ExtensionDefinition<{
|
||||
path: string;
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const graphiqlPageSidebarItem: Extension<{
|
||||
export const graphiqlPageSidebarItem: ExtensionDefinition<{
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import { convertLegacyRouteRef } from '@backstage/core-compat-api';
|
||||
|
||||
/** @alpha */
|
||||
export const GraphiqlPage = createPageExtension({
|
||||
id: 'plugin.graphiql.page',
|
||||
defaultPath: '/graphiql',
|
||||
routeRef: convertLegacyRouteRef(graphiQLRouteRef),
|
||||
loader: () => import('./components').then(m => <m.GraphiQLPage />),
|
||||
@@ -46,7 +45,6 @@ export const GraphiqlPage = createPageExtension({
|
||||
|
||||
/** @alpha */
|
||||
export const graphiqlPageSidebarItem = createNavItemExtension({
|
||||
id: 'plugin.graphiql.nav.index',
|
||||
title: 'GraphiQL',
|
||||
icon: GraphiQLIcon as IconComponent,
|
||||
routeRef: convertLegacyRouteRef(graphiQLRouteRef),
|
||||
@@ -59,7 +57,7 @@ const endpointDataRef = createExtensionDataRef<GraphQLEndpoint>(
|
||||
|
||||
/** @alpha */
|
||||
export const graphiqlBrowseApi = createApiExtension({
|
||||
api: graphQlBrowseApiRef, // apis.plugin.graphiql.browse
|
||||
api: graphQlBrowseApiRef,
|
||||
inputs: {
|
||||
endpoints: createExtensionInput({
|
||||
endpoint: endpointDataRef,
|
||||
@@ -74,15 +72,18 @@ export const graphiqlBrowseApi = createApiExtension({
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export function createEndpointExtension<TConfig extends {}>(options: {
|
||||
id: string;
|
||||
export function createGraphiQLEndpointExtension<TConfig extends {}>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
factory: (options: { config: TConfig }) => { endpoint: GraphQLEndpoint };
|
||||
}) {
|
||||
return createExtension({
|
||||
id: `apis.plugin.graphiql.browse.${options.id}`,
|
||||
attachTo: { id: 'apis.plugin.graphiql.browse', input: 'endpoints' },
|
||||
kind: 'graphiql-endpoint',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: { id: 'api:graphiql/browse', input: 'endpoints' },
|
||||
configSchema: options.configSchema,
|
||||
disabled: options.disabled ?? false,
|
||||
output: {
|
||||
@@ -97,8 +98,8 @@ export function createEndpointExtension<TConfig extends {}>(options: {
|
||||
}
|
||||
|
||||
/** @alpha */
|
||||
const gitlabGraphiQLBrowseExtension = createEndpointExtension({
|
||||
id: 'gitlab',
|
||||
const gitlabGraphiQLBrowseExtension = createGraphiQLEndpointExtension({
|
||||
name: 'gitlab',
|
||||
disabled: true,
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z
|
||||
|
||||
@@ -33,7 +33,6 @@ const rootRouteRef = createRouteRef();
|
||||
export const titleExtensionDataRef = createExtensionDataRef<string>('title');
|
||||
|
||||
const HomepageCompositionRootExtension = createPageExtension({
|
||||
id: 'home',
|
||||
defaultPath: '/home',
|
||||
routeRef: rootRouteRef,
|
||||
inputs: {
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
/// <reference types="react" />
|
||||
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ListItemProps } from '@material-ui/core';
|
||||
import { PortableSchema } from '@backstage/frontend-plugin-api';
|
||||
import { SearchDocument } from '@backstage/plugin-search-common';
|
||||
@@ -23,7 +23,9 @@ export function createSearchResultListItemExtension<
|
||||
TConfig extends {
|
||||
noTrack?: boolean;
|
||||
},
|
||||
>(options: SearchResultItemExtensionOptions<TConfig>): Extension<TConfig>;
|
||||
>(
|
||||
options: SearchResultItemExtensionOptions<TConfig>,
|
||||
): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type SearchResultItemExtensionComponent = <
|
||||
@@ -47,7 +49,8 @@ export type SearchResultItemExtensionOptions<
|
||||
noTrack?: boolean;
|
||||
},
|
||||
> = {
|
||||
id: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
|
||||
@@ -44,8 +44,7 @@ describe('createSearchResultListItemExtension', () => {
|
||||
|
||||
const TechDocsSearchResultItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'techdocs',
|
||||
attachTo: { id: 'plugin.search.page', input: 'items' },
|
||||
namespace: 'techdocs',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z
|
||||
.object({
|
||||
@@ -67,14 +66,13 @@ describe('createSearchResultListItemExtension', () => {
|
||||
|
||||
const ExploreSearchResultItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'explore',
|
||||
attachTo: { id: 'plugin.search.page', input: 'items' },
|
||||
namespace: 'explore',
|
||||
predicate: result => result.type === 'explore',
|
||||
component: async () => ExploreSearchResultItemComponent,
|
||||
});
|
||||
|
||||
const SearchPageExtension = createPageExtension({
|
||||
id: 'plugin.search.page',
|
||||
namespace: 'search',
|
||||
defaultPath: '/',
|
||||
inputs: {
|
||||
items: createExtensionInput({
|
||||
|
||||
@@ -58,9 +58,13 @@ export type SearchResultItemExtensionOptions<
|
||||
TConfig extends { noTrack?: boolean },
|
||||
> = {
|
||||
/**
|
||||
* The extension id.
|
||||
* The extension namespace.
|
||||
*/
|
||||
id: string;
|
||||
namespace?: string;
|
||||
/**
|
||||
* The extension name.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The extension attachment point (e.g., search modal or page).
|
||||
*/
|
||||
@@ -86,8 +90,6 @@ export type SearchResultItemExtensionOptions<
|
||||
export function createSearchResultListItemExtension<
|
||||
TConfig extends { noTrack?: boolean },
|
||||
>(options: SearchResultItemExtensionOptions<TConfig>) {
|
||||
const id = `plugin.search.result.item.${options.id}`;
|
||||
|
||||
const configSchema =
|
||||
'configSchema' in options
|
||||
? options.configSchema
|
||||
@@ -98,9 +100,11 @@ export function createSearchResultListItemExtension<
|
||||
) as PortableSchema<TConfig>);
|
||||
|
||||
return createExtension({
|
||||
id,
|
||||
kind: 'search-result-list-item',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'plugin.search.page',
|
||||
id: 'page:search',
|
||||
input: 'items',
|
||||
},
|
||||
configSchema,
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
@@ -17,15 +17,15 @@ const _default: BackstagePlugin<
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const SearchApi: Extension<{}>;
|
||||
export const SearchApi: ExtensionDefinition<{}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const SearchNavItem: Extension<{
|
||||
export const SearchNavItem: ExtensionDefinition<{
|
||||
title: string;
|
||||
}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const SearchPage: Extension<{
|
||||
export const SearchPage: ExtensionDefinition<{
|
||||
path: string;
|
||||
noTrack: boolean;
|
||||
}>;
|
||||
|
||||
@@ -98,7 +98,6 @@ const useSearchPageStyles = makeStyles((theme: Theme) => ({
|
||||
|
||||
/** @alpha */
|
||||
export const SearchPage = createPageExtension({
|
||||
id: 'plugin.search.page',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
@@ -235,7 +234,6 @@ export const SearchPage = createPageExtension({
|
||||
|
||||
/** @alpha */
|
||||
export const SearchNavItem = createNavItemExtension({
|
||||
id: 'plugin.search.nav.index',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
title: 'Search',
|
||||
icon: SearchIcon,
|
||||
|
||||
@@ -33,7 +33,6 @@ const StackOverflowApi = createApiExtension({
|
||||
|
||||
/** @alpha */
|
||||
const StackOverflowSearchResultListItem = createSearchResultListItemExtension({
|
||||
id: 'stack-overflow',
|
||||
predicate: result => result.type === 'stack-overflow',
|
||||
component: () =>
|
||||
import('./search/StackOverflowSearchResultListItem').then(
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
@@ -17,7 +17,10 @@ const _default: BackstagePlugin<
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const TechRadarPage: Extension<{
|
||||
export const sampleTechRadarApi: ExtensionDefinition<{}>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const TechRadarPage: ExtensionDefinition<{
|
||||
height: number;
|
||||
width: number;
|
||||
title: string;
|
||||
|
||||
@@ -68,6 +68,7 @@
|
||||
"@backstage/cli": "workspace:^",
|
||||
"@backstage/core-app-api": "workspace:^",
|
||||
"@backstage/dev-utils": "workspace:^",
|
||||
"@backstage/frontend-test-utils": "workspace:^",
|
||||
"@backstage/test-utils": "workspace:^",
|
||||
"@testing-library/dom": "^9.0.0",
|
||||
"@testing-library/jest-dom": "^6.0.0",
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { TechRadarPage, sampleTechRadarApi } from './alpha';
|
||||
|
||||
describe('TechRadarPage', () => {
|
||||
beforeAll(() => {
|
||||
Object.defineProperty(window.SVGElement.prototype, 'getBBox', {
|
||||
value: () => ({ width: 100, height: 100 }),
|
||||
configurable: true,
|
||||
});
|
||||
});
|
||||
|
||||
it('renders without exploding', async () => {
|
||||
createExtensionTester(TechRadarPage).add(sampleTechRadarApi).render();
|
||||
|
||||
await expect(screen.findByText('Tech Radar')).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -29,7 +29,6 @@ import { rootRouteRef } from './plugin';
|
||||
|
||||
/** @alpha */
|
||||
export const TechRadarPage = createPageExtension({
|
||||
id: 'plugin.techradar.page',
|
||||
defaultPath: '/tech-radar',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
@@ -48,11 +47,9 @@ export const TechRadarPage = createPageExtension({
|
||||
import('./components').then(m => <m.RadarPage {...config} />),
|
||||
});
|
||||
|
||||
const sampleTechRadarApi = createApiExtension({
|
||||
api: techRadarApiRef,
|
||||
factory() {
|
||||
return createApiFactory(techRadarApiRef, new SampleTechRadarApi());
|
||||
},
|
||||
/** @alpha */
|
||||
export const sampleTechRadarApi = createApiExtension({
|
||||
factory: createApiFactory(techRadarApiRef, new SampleTechRadarApi()),
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
|
||||
```ts
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { Extension } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
// @alpha (undocumented)
|
||||
@@ -23,7 +23,7 @@ const _default: BackstagePlugin<
|
||||
export default _default;
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const TechDocsSearchResultListItemExtension: Extension<{
|
||||
export const TechDocsSearchResultListItemExtension: ExtensionDefinition<{
|
||||
lineClamp: number;
|
||||
noTrack: boolean;
|
||||
asListItem: boolean;
|
||||
|
||||
@@ -46,53 +46,45 @@ import { createEntityContentExtension } from '@backstage/plugin-catalog-react/al
|
||||
|
||||
/** @alpha */
|
||||
const techDocsStorage = createApiExtension({
|
||||
api: techdocsStorageApiRef,
|
||||
|
||||
factory() {
|
||||
return createApiFactory({
|
||||
api: techdocsStorageApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
|
||||
new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
}),
|
||||
});
|
||||
},
|
||||
factory: createApiFactory({
|
||||
api: techdocsStorageApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
identityApi: identityApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, identityApi, fetchApi }) =>
|
||||
new TechDocsStorageClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
identityApi,
|
||||
fetchApi,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
const techDocsClient = createApiExtension({
|
||||
api: techdocsApiRef,
|
||||
factory() {
|
||||
return createApiFactory({
|
||||
api: techdocsApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, fetchApi }) =>
|
||||
new TechDocsClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
}),
|
||||
});
|
||||
},
|
||||
factory: createApiFactory({
|
||||
api: techdocsApiRef,
|
||||
deps: {
|
||||
configApi: configApiRef,
|
||||
discoveryApi: discoveryApiRef,
|
||||
fetchApi: fetchApiRef,
|
||||
},
|
||||
factory: ({ configApi, discoveryApi, fetchApi }) =>
|
||||
new TechDocsClient({
|
||||
configApi,
|
||||
discoveryApi,
|
||||
fetchApi,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
|
||||
/** @alpha */
|
||||
export const TechDocsSearchResultListItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
id: 'techdocs',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
// TODO: Define how the icon can be configurable
|
||||
@@ -118,7 +110,6 @@ export const TechDocsSearchResultListItemExtension =
|
||||
* @alpha
|
||||
*/
|
||||
const TechDocsIndexPage = createPageExtension({
|
||||
id: 'plugin.techdocs.indexPage',
|
||||
defaultPath: '/docs',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
loader: () =>
|
||||
@@ -133,7 +124,7 @@ const TechDocsIndexPage = createPageExtension({
|
||||
* @alpha
|
||||
*/
|
||||
const TechDocsReaderPage = createPageExtension({
|
||||
id: 'plugin.techdocs.readerPage',
|
||||
name: 'reader',
|
||||
defaultPath: '/docs/:namespace/:kind/:name',
|
||||
routeRef: convertLegacyRouteRef(rootDocsRouteRef),
|
||||
loader: () =>
|
||||
@@ -148,7 +139,6 @@ const TechDocsReaderPage = createPageExtension({
|
||||
* @alpha
|
||||
*/
|
||||
const TechDocsEntityContent = createEntityContentExtension({
|
||||
id: 'techdocs',
|
||||
defaultPath: 'docs',
|
||||
defaultTitle: 'TechDocs',
|
||||
loader: () => import('./Router').then(m => <m.EmbeddedDocsRouter />),
|
||||
@@ -156,7 +146,6 @@ const TechDocsEntityContent = createEntityContentExtension({
|
||||
|
||||
/** @alpha */
|
||||
const TechDocsNavItem = createNavItemExtension({
|
||||
id: 'plugin.techdocs.nav.index',
|
||||
icon: LibraryBooks,
|
||||
title: 'Docs',
|
||||
routeRef: convertLegacyRouteRef(rootRouteRef),
|
||||
|
||||
@@ -27,7 +27,6 @@ import React from 'react';
|
||||
export * from './translation';
|
||||
|
||||
const UserSettingsPage = createPageExtension({
|
||||
id: 'plugin.user-settings.page',
|
||||
defaultPath: '/settings',
|
||||
routeRef: convertLegacyRouteRef(settingsRouteRef),
|
||||
inputs: {
|
||||
|
||||
@@ -9587,6 +9587,7 @@ __metadata:
|
||||
"@backstage/core-plugin-api": "workspace:^"
|
||||
"@backstage/dev-utils": "workspace:^"
|
||||
"@backstage/frontend-plugin-api": "workspace:^"
|
||||
"@backstage/frontend-test-utils": "workspace:^"
|
||||
"@backstage/test-utils": "workspace:^"
|
||||
"@backstage/theme": "workspace:^"
|
||||
"@material-ui/core": ^4.12.2
|
||||
|
||||
Reference in New Issue
Block a user