Merge pull request #32496 from backstage/rugvip/internal

frontend-plugin-api: add new internal extension input option, complete app-react deprecations
This commit is contained in:
Patrik Oldsberg
2026-01-26 12:13:24 +01:00
committed by GitHub
70 changed files with 523 additions and 633 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-app-api': patch
---
Implemented support for the `internal` extension input option.
+6
View File
@@ -0,0 +1,6 @@
---
'@backstage/core-compat-api': patch
'@backstage/plugin-app-visualizer': patch
---
Internal updates for blueprint moves to `@backstage/plugin-app-react`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/create-app': patch
---
Switched `next-app` template to use blueprint from `@backstage/plugin-app-react`.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-plugin-api': patch
---
Added a new `internal` option to `createExtensionInput` that marks the input as only allowing attachments from the same plugin.
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/frontend-plugin-api': minor
---
**BREAKING**: The following blueprints have been removed and are now only available from `@backstage/plugin-app-react`:
- `IconBundleBlueprint`
- `NavContentBlueprint`
- `RouterBlueprint`
- `SignInPageBlueprint`
- `SwappableComponentBlueprint`
- `ThemeBlueprint`
- `TranslationBlueprint`
+13
View File
@@ -0,0 +1,13 @@
---
'@backstage/plugin-app': minor
---
**BREAKING**: Extensions created with the following blueprints must now be provided via an override or a module for the `app` plugin. Extensions from other plugins will now trigger a warning in the app and be ignored.
- `IconBundleBlueprint`
- `NavContentBlueprint`
- `RouterBlueprint`
- `SignInPageBlueprint`
- `SwappableComponentBlueprint`
- `ThemeBlueprint`
- `TranslationBlueprint`
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/frontend-defaults': patch
---
Dependency update for tests.
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-app-react': patch
---
Internal refactor to move implementation of blueprints from `@backstage/frontend-plugin-api` to this package.
@@ -174,6 +174,8 @@ const navigationExtension = createExtension({
The input (see [1] above) is an object that we create using `createExtensionInput`. The first argument is the set of extension data that we accept via this input, and works just like the `output` option. The second argument is optional, and it allows us to put constraints on the extensions that are attached to our input. If the `singleton: true` option is set, only a single extension can be attached at a time, and unless the `optional: true` option is set it will also be required that there is exactly one attached extension.
Another option that can be used when creating an extension input is the `internal: true` option, which restricts the input to only accept extensions from the same plugin as the extension defining the input. Extensions from other plugins that attempt to attach to an internal input will be ignored, and a warning will be reported. This is useful when you want to limit extensibility to overrides and modules of your plugin, rather than letting it be open to any plugin.
So how can we now attach the output to the parent extension's input? If we think about a navigation component, like the Sidebar in Backstage, there might be plugins that want to attach a link to their plugin to this navigation component. In this case the plugin only needs to know the extension `id` and the name of the extension `input` to attach the extension `output` returned by the `factory` to the specified extension:
```tsx
+1
View File
@@ -48,6 +48,7 @@
"@backstage/integration-react": "workspace:^",
"@backstage/plugin-api-docs": "workspace:^",
"@backstage/plugin-app": "workspace:^",
"@backstage/plugin-app-react": "workspace:^",
"@backstage/plugin-app-visualizer": "workspace:^",
"@backstage/plugin-auth": "workspace:^",
"@backstage/plugin-auth-react": "workspace:^",
@@ -14,10 +14,8 @@
* limitations under the License.
*/
import {
SwappableComponentBlueprint,
NotFoundErrorPage,
} from '@backstage/frontend-plugin-api';
import { NotFoundErrorPage } from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import Box from '@material-ui/core/Box';
import Typography from '@material-ui/core/Typography';
import { Button } from '@backstage/core-components';
@@ -28,10 +28,8 @@ import {
import SearchIcon from '@material-ui/icons/Search';
import MenuIcon from '@material-ui/icons/Menu';
import BuildIcon from '@material-ui/icons/Build';
import {
createFrontendModule,
NavContentBlueprint,
} from '@backstage/frontend-plugin-api';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { NavContentBlueprint } from '@backstage/plugin-app-react';
import { SidebarSearchModal } from '@backstage/plugin-search';
import { NotificationsSidebarItem } from '@backstage/plugin-notifications';
import {
+1
View File
@@ -33,6 +33,7 @@
"dependencies": {
"@backstage/core-plugin-api": "workspace:^",
"@backstage/frontend-plugin-api": "workspace:^",
"@backstage/plugin-app-react": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
@@ -16,21 +16,23 @@
import { ComponentType } from 'react';
import {
SwappableComponentBlueprint,
ApiBlueprint,
ErrorDisplayProps,
createExtension,
createFrontendModule,
ExtensionDefinition,
FrontendModule,
IconBundleBlueprint,
RouterBlueprint,
SignInPageBlueprint,
ThemeBlueprint,
ErrorDisplay as SwappableErrorDisplay,
NotFoundErrorPage as SwappableNotFoundErrorPage,
Progress as SwappableProgress,
} from '@backstage/frontend-plugin-api';
import {
IconBundleBlueprint,
RouterBlueprint,
SignInPageBlueprint,
SwappableComponentBlueprint,
ThemeBlueprint,
} from '@backstage/plugin-app-react';
import {
AnyApiFactory,
AppComponents,
+2
View File
@@ -55,6 +55,7 @@ import { version as ui } from '../../../ui/package.json';
import { version as pluginApiDocs } from '../../../../plugins/api-docs/package.json';
import { version as pluginAppVisualizer } from '../../../../plugins/app-visualizer/package.json';
import { version as pluginAppBackend } from '../../../../plugins/app-backend/package.json';
import { version as pluginAppReact } from '../../../../plugins/app-react/package.json';
import { version as pluginAuthBackend } from '../../../../plugins/auth-backend/package.json';
import { version as pluginAuthBackendModuleGithubProvider } from '../../../../plugins/auth-backend-module-github-provider/package.json';
import { version as pluginAuthBackendModuleGuestProvider } from '../../../../plugins/auth-backend-module-guest-provider/package.json';
@@ -118,6 +119,7 @@ export const packageVersions = {
'@backstage/repo-tools': repoTools,
'@backstage/plugin-api-docs': pluginApiDocs,
'@backstage/plugin-app-backend': pluginAppBackend,
'@backstage/plugin-app-react': pluginAppReact,
'@backstage/plugin-app-visualizer': pluginAppVisualizer,
'@backstage/plugin-auth-backend': pluginAuthBackend,
'@backstage/plugin-auth-backend-module-github-provider':
@@ -21,6 +21,7 @@
"@backstage/frontend-defaults": "^{{ version '@backstage/frontend-defaults'}}",
"@backstage/frontend-plugin-api": "^{{ version '@backstage/frontend-plugin-api'}}",
"@backstage/integration-react": "^{{ version '@backstage/integration-react'}}",
"@backstage/plugin-app-react": "^{{ version '@backstage/plugin-app-react'}}",
"@backstage/plugin-app-visualizer": "^{{ version '@backstage/plugin-app-visualizer'}}",
"@backstage/plugin-catalog": "^{{ version '@backstage/plugin-catalog'}}",
"@backstage/plugin-notifications": "^{{ version '@backstage/plugin-notifications'}}",
@@ -7,7 +7,7 @@ import {
} from '@backstage/core-components';
import { compatWrapper } from '@backstage/core-compat-api';
import { Sidebar } from '@backstage/core-components';
import { NavContentBlueprint } from '@backstage/frontend-plugin-api';
import { NavContentBlueprint } from '@backstage/plugin-app-react';
import { SidebarLogo } from './SidebarLogo';
import CreateComponentIcon from '@material-ui/icons/AddCircleOutline';
import HomeIcon from '@material-ui/icons/Home';
+8
View File
@@ -59,6 +59,14 @@ export type AppErrorTypes = {
inputName: string;
};
};
EXTENSION_INPUT_INTERNAL_IGNORED: {
context: {
node: AppNode;
inputName: string;
extensionId: string;
plugin: FrontendPlugin;
};
};
EXTENSION_ATTACHMENT_CONFLICT: {
context: {
node: AppNode;
@@ -142,6 +142,36 @@ function createV1Extension(opts: {
return ext;
}
function mirrorInputs(ctx: {
inputs: {
[name in string]:
| undefined
| ResolvedExtensionInput<ExtensionInput>
| Array<ResolvedExtensionInput<ExtensionInput>>;
};
}) {
return [
inputMirrorDataRef(
Object.fromEntries(
Object.entries(ctx.inputs).map(([k, v]) => [
k,
Array.isArray(v)
? v.map(vi => ({
node: vi.node,
test: vi.get(testDataRef),
other: vi.get(otherDataRef),
}))
: {
node: v?.node,
test: v?.get(testDataRef),
other: v?.get(otherDataRef),
},
]),
),
),
];
}
describe('instantiateAppNodeTree', () => {
describe('v1', () => {
const simpleExtension = createV1Extension({
@@ -237,6 +267,60 @@ describe('instantiateAppNodeTree', () => {
expect(childNode?.instance).toBeDefined();
});
it('should ignore non-matching plugin attachments for internal inputs', () => {
const otherPlugin = createFrontendPlugin({ pluginId: 'other' });
const tree = resolveAppTree(
'root-node',
[
makeSpec(
resolveExtensionDefinition(
createExtension({
attachTo: { id: 'ignored', input: 'ignored' },
inputs: {
test: createExtensionInput([testDataRef], {
singleton: true,
internal: true,
}),
},
output: [inputMirrorDataRef],
factory: mirrorInputs,
}),
{ namespace: 'root-node' },
),
),
makeSpec(simpleExtension, {
id: 'child-node-app',
attachTo: { id: 'root-node', input: 'test' },
}),
makeSpec(simpleExtension, {
id: 'child-node-other',
attachTo: { id: 'root-node', input: 'test' },
plugin: otherPlugin,
}),
],
collector,
);
instantiateAppNodeTree(tree.root, testApis, collector);
expect(tree.root.instance?.getData(inputMirrorDataRef)).toMatchObject({
test: { node: { spec: { id: 'child-node-app' } }, test: 'test' },
});
expect(collector.collectErrors()).toEqual([
{
code: 'EXTENSION_INPUT_INTERNAL_IGNORED',
message:
"extension 'child-node-other' from plugin 'other' attached to input 'test' on 'root-node' was ignored, the input is marked as internal and attached extensions must therefore be provided via an override or a module for the 'app' plugin, not the 'other' plugin",
context: {
node: tree.root,
inputName: 'test',
extensionId: 'child-node-other',
plugin: otherPlugin,
},
},
]);
});
it('should not instantiate disabled attachments', () => {
const tree = resolveAppTree(
'root-node',
@@ -783,36 +867,6 @@ describe('instantiateAppNodeTree', () => {
{ namespace: 'app' },
);
function mirrorInputs(ctx: {
inputs: {
[name in string]:
| undefined
| ResolvedExtensionInput<ExtensionInput>
| Array<ResolvedExtensionInput<ExtensionInput>>;
};
}) {
return [
inputMirrorDataRef(
Object.fromEntries(
Object.entries(ctx.inputs).map(([k, v]) => [
k,
Array.isArray(v)
? v.map(vi => ({
node: vi.node,
test: vi.get(testDataRef),
other: vi.get(otherDataRef),
}))
: {
node: v?.node,
test: v?.get(testDataRef),
other: v?.get(otherDataRef),
},
]),
),
),
];
}
it('should instantiate a single node', () => {
const tree = resolveAppTree(
'root-node',
@@ -247,10 +247,32 @@ function resolveV2Inputs(
inputMap: { [inputName in string]: ExtensionInput },
attachments: ReadonlyMap<string, AppNode[]>,
parentCollector: ErrorCollector<{ node: AppNode }>,
node: AppNode,
): ResolvedExtensionInputs<{ [inputName in string]: ExtensionInput }> {
return mapValues(inputMap, (input, inputName) => {
const attachedNodes = attachments.get(inputName) ?? [];
const allAttachedNodes = attachments.get(inputName) ?? [];
const collector = parentCollector.child({ inputName });
const inputPluginId = node.spec.plugin.id;
const attachedNodes = input.config.internal
? allAttachedNodes.filter(attachment => {
const attachmentPluginId = attachment.spec.plugin.id;
if (attachmentPluginId !== inputPluginId) {
collector.report({
code: 'EXTENSION_INPUT_INTERNAL_IGNORED',
message:
`extension '${attachment.spec.id}' from plugin '${attachmentPluginId}' attached to input '${inputName}' on '${node.spec.id}' was ignored, ` +
`the input is marked as internal and attached extensions must therefore be provided via an override or a module for the '${inputPluginId}' plugin, not the '${attachmentPluginId}' plugin`,
context: {
extensionId: attachment.spec.id,
plugin: attachment.spec.plugin,
},
});
return false;
}
return true;
})
: allAttachedNodes;
if (input.config.singleton) {
if (attachedNodes.length > 1) {
@@ -371,6 +393,7 @@ export function createAppNodeInstance(options: {
internalExtension.inputs,
attachments,
collector,
node,
),
};
const outputDataValues = options.extensionFactoryMiddleware
@@ -38,6 +38,14 @@ export type AppErrorTypes = {
EXTENSION_INPUT_DATA_MISSING: {
context: { node: AppNode; inputName: string };
};
EXTENSION_INPUT_INTERNAL_IGNORED: {
context: {
node: AppNode;
inputName: string;
extensionId: string;
plugin: FrontendPlugin;
};
};
EXTENSION_ATTACHMENT_CONFLICT: {
context: { node: AppNode; inputName: string };
};
+1
View File
@@ -42,6 +42,7 @@
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/plugin-app-react": "workspace:^",
"@backstage/test-utils": "workspace:^",
"@testing-library/jest-dom": "^6.0.0",
"@testing-library/react": "^16.0.0",
@@ -22,11 +22,11 @@ import {
PageBlueprint,
createFrontendPlugin,
createFrontendFeatureLoader,
ThemeBlueprint,
createFrontendModule,
useAppNode,
FrontendPluginInfo,
} from '@backstage/frontend-plugin-api';
import { ThemeBlueprint } from '@backstage/plugin-app-react';
import { screen, waitFor } from '@testing-library/react';
import { createApp } from './createApp';
import { mockApis, renderWithEffects } from '@backstage/test-utils';
@@ -60,8 +60,8 @@ describe('createApp', () => {
}),
},
features: [
createFrontendPlugin({
pluginId: 'test',
createFrontendModule({
pluginId: 'app',
extensions: [
ThemeBlueprint.make({
name: 'derp',
@@ -14,10 +14,8 @@
* limitations under the License.
*/
import {
SignInPageBlueprint,
createFrontendModule,
} from '@backstage/frontend-plugin-api';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { SignInPageBlueprint } from '@backstage/plugin-app-react';
import { render, screen, waitFor } from '@testing-library/react';
import { useEffect } from 'react';
import { createPublicSignInApp } from './createPublicSignInApp';
@@ -22,6 +22,7 @@ const DEFAULT_WARNING_CODES: Array<keyof AppErrorTypes> = [
'EXTENSION_IGNORED',
'INVALID_EXTENSION_CONFIG_KEY',
'EXTENSION_INPUT_DATA_IGNORED',
'EXTENSION_INPUT_INTERNAL_IGNORED',
'EXTENSION_OUTPUT_IGNORED',
];
+5 -257
View File
@@ -13,7 +13,6 @@ import { ExpandRecursive } from '@backstage/types';
import { ExtensionBlueprint as ExtensionBlueprint_2 } from '@backstage/frontend-plugin-api';
import { ExtensionBlueprintParams as ExtensionBlueprintParams_2 } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef as ExtensionDataRef_2 } from '@backstage/frontend-plugin-api';
import { IconComponent as IconComponent_2 } from '@backstage/frontend-plugin-api';
import { JsonObject } from '@backstage/types';
import { JsonValue } from '@backstage/types';
import { JSX as JSX_2 } from 'react/jsx-runtime';
@@ -21,7 +20,6 @@ import { JSX as JSX_3 } from 'react';
import { Observable } from '@backstage/types';
import { PropsWithChildren } from 'react';
import { ReactNode } from 'react';
import { RouteRef as RouteRef_2 } from '@backstage/frontend-plugin-api';
import { SwappableComponentRef as SwappableComponentRef_2 } from '@backstage/frontend-plugin-api';
import type { z } from 'zod';
@@ -271,30 +269,6 @@ export const AppRootElementBlueprint: ExtensionBlueprint_2<{
dataRefs: never;
}>;
// @public @deprecated
export const AppRootWrapperBlueprint: ExtensionBlueprint_2<{
kind: 'app-root-wrapper';
params: {
Component?: [error: 'Use the `component` parameter instead'];
component: (props: { children: ReactNode }) => JSX.Element | null;
};
output: ExtensionDataRef_2<
(props: { children: ReactNode }) => JSX.Element | null,
'app.root.wrapper',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
component: ConfigurableExtensionDataRef_2<
(props: { children: ReactNode }) => JSX.Element | null,
'app.root.wrapper',
{}
>;
};
}>;
// @public
export type AppTheme = {
id: string;
@@ -604,7 +578,7 @@ export function createExtensionDataRef<TData>(): {
}): ConfigurableExtensionDataRef<TData, TId>;
};
// @public (undocumented)
// @public
export function createExtensionInput<
UExtensionData extends ExtensionDataRef<
unknown,
@@ -616,6 +590,7 @@ export function createExtensionInput<
TConfig extends {
singleton?: boolean;
optional?: boolean;
internal?: boolean;
},
>(
extensionData: Array<UExtensionData>,
@@ -630,6 +605,7 @@ export function createExtensionInput<
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
internal: TConfig['internal'] extends true ? true : false;
}
>;
@@ -1288,9 +1264,11 @@ export interface ExtensionInput<
TConfig extends {
singleton: boolean;
optional: boolean;
internal?: boolean;
} = {
singleton: boolean;
optional: boolean;
internal?: boolean;
},
> {
// (undocumented)
@@ -1457,33 +1435,6 @@ export const googleAuthApiRef: ApiRef<
SessionApi
>;
// @public @deprecated (undocumented)
export const IconBundleBlueprint: ExtensionBlueprint_2<{
kind: 'icon-bundle';
params: {
icons: { [key in string]: IconComponent };
};
output: ExtensionDataRef_2<
{
[x: string]: IconComponent;
},
'core.icons',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
icons: ConfigurableExtensionDataRef_2<
{
[x: string]: IconComponent;
},
'core.icons',
{}
>;
};
}>;
// @public
export type IconComponent = ComponentType<{
fontSize?: 'medium' | 'large' | 'small' | 'inherit';
@@ -1522,45 +1473,6 @@ export const microsoftAuthApiRef: ApiRef<
SessionApi
>;
// @public @deprecated
export const NavContentBlueprint: ExtensionBlueprint_2<{
kind: 'nav-content';
params: {
component: NavContentComponent;
};
output: ExtensionDataRef_2<
NavContentComponent,
'core.nav-content.component',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
component: ConfigurableExtensionDataRef_2<
NavContentComponent,
'core.nav-content.component',
{}
>;
};
}>;
// @public
export type NavContentComponent = (
props: NavContentComponentProps,
) => JSX.Element | null;
// @public
export interface NavContentComponentProps {
items: Array<{
icon: IconComponent_2;
title: string;
routeRef: RouteRef_2<undefined>;
to: string;
text: string;
}>;
}
// @public
export const NavItemBlueprint: ExtensionBlueprint_2<{
kind: 'nav-item';
@@ -1923,30 +1835,6 @@ export type RouteFunc<TParams extends AnyRouteRefParams> = (
: readonly [params: TParams]
) => string;
// @public @deprecated (undocumented)
export const RouterBlueprint: ExtensionBlueprint_2<{
kind: 'app-router-component';
params: {
Component?: [error: 'Use the `component` parameter instead'];
component: (props: { children: ReactNode }) => JSX.Element | null;
};
output: ExtensionDataRef_2<
(props: { children: ReactNode }) => JSX.Element | null,
'app.router.wrapper',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
component: ConfigurableExtensionDataRef_2<
(props: { children: ReactNode }) => JSX.Element | null,
'app.router.wrapper',
{}
>;
};
}>;
// @public
export interface RouteRef<
TParams extends AnyRouteRefParams = AnyRouteRefParams,
@@ -1998,35 +1886,6 @@ export namespace SessionState {
export type SignedOut = typeof SessionState.SignedOut;
}
// @public @deprecated
export const SignInPageBlueprint: ExtensionBlueprint_2<{
kind: 'sign-in-page';
params: {
loader: () => Promise<ComponentType<SignInPageProps>>;
};
output: ExtensionDataRef_2<
ComponentType<SignInPageProps>,
'core.sign-in-page.component',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
component: ConfigurableExtensionDataRef_2<
ComponentType<SignInPageProps>,
'core.sign-in-page.component',
{}
>;
};
}>;
// @public
export type SignInPageProps = {
onSignInSuccess(identityApi: IdentityApi): void;
children?: ReactNode;
};
// @public
export interface StorageApi {
forBucket(name: string): StorageApi;
@@ -2066,65 +1925,6 @@ export interface SubRouteRef<
readonly T: TParams;
}
// @public @deprecated
export const SwappableComponentBlueprint: ExtensionBlueprint_2<{
kind: 'component';
params: <Ref extends SwappableComponentRef<any>>(params: {
component: Ref extends SwappableComponentRef<
any,
infer IExternalComponentProps
>
? {
ref: Ref;
} & ((props: IExternalComponentProps) => JSX.Element | null)
: never;
loader: Ref extends SwappableComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}) => ExtensionBlueprintParams_2<{
component: Ref extends SwappableComponentRef<
any,
infer IExternalComponentProps
>
? {
ref: Ref;
} & ((props: IExternalComponentProps) => JSX.Element | null)
: never;
loader: Ref extends SwappableComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}>;
output: ExtensionDataRef_2<
{
ref: SwappableComponentRef;
loader:
| (() => (props: {}) => JSX.Element | null)
| (() => Promise<(props: {}) => JSX.Element | null>);
},
'core.swappableComponent',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
component: ConfigurableExtensionDataRef_2<
{
ref: SwappableComponentRef;
loader:
| (() => (props: {}) => JSX.Element | null)
| (() => Promise<(props: {}) => JSX.Element | null>);
},
'core.swappableComponent',
{}
>;
};
}>;
// @public (undocumented)
export type SwappableComponentRef<
TInnerComponentProps extends {} = {},
@@ -2150,21 +1950,6 @@ export interface SwappableComponentsApi {
// @public
export const swappableComponentsApiRef: ApiRef_2<SwappableComponentsApi>;
// @public @deprecated
export const ThemeBlueprint: ExtensionBlueprint_2<{
kind: 'theme';
params: {
theme: AppTheme;
};
output: ExtensionDataRef_2<AppTheme, 'core.theme.theme', {}>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
theme: ConfigurableExtensionDataRef_2<AppTheme, 'core.theme.theme', {}>;
};
}>;
// @public (undocumented)
export type TranslationApi = {
getTranslation<
@@ -2186,43 +1971,6 @@ export type TranslationApi = {
// @public (undocumented)
export const translationApiRef: ApiRef<TranslationApi>;
// @public @deprecated
export const TranslationBlueprint: ExtensionBlueprint_2<{
kind: 'translation';
params: {
resource: TranslationResource | TranslationMessages;
};
output: ExtensionDataRef_2<
| TranslationResource<string>
| TranslationMessages<
string,
{
[x: string]: string;
},
boolean
>,
'core.translation.translation',
{}
>;
inputs: {};
config: {};
configInput: {};
dataRefs: {
translation: ConfigurableExtensionDataRef_2<
| TranslationResource<string>
| TranslationMessages<
string,
{
[x: string]: string;
},
boolean
>,
'core.translation.translation',
{}
>;
};
}>;
// @public (undocumented)
export type TranslationFunction<
TMessages extends {
@@ -200,6 +200,7 @@ describe('ApiBlueprint', () => {
"test": {
"$$type": "@backstage/ExtensionInput",
"config": {
"internal": false,
"optional": false,
"singleton": false,
},
@@ -20,20 +20,5 @@ export {
} from './AnalyticsImplementationBlueprint';
export { ApiBlueprint } from './ApiBlueprint';
export { AppRootElementBlueprint } from './AppRootElementBlueprint';
export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint';
export { IconBundleBlueprint } from './IconBundleBlueprint';
export {
NavContentBlueprint,
type NavContentComponent,
type NavContentComponentProps,
} from './NavContentBlueprint';
export { NavItemBlueprint } from './NavItemBlueprint';
export { PageBlueprint } from './PageBlueprint';
export { RouterBlueprint } from './RouterBlueprint';
export {
type SignInPageProps,
SignInPageBlueprint,
} from './SignInPageBlueprint';
export { ThemeBlueprint } from './ThemeBlueprint';
export { TranslationBlueprint } from './TranslationBlueprint';
export { SwappableComponentBlueprint } from './SwappableComponentBlueprint';
@@ -29,28 +29,28 @@ describe('createExtensionInput', () => {
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: false, optional: false },
config: { singleton: false, optional: false, internal: false },
withContext: expect.any(Function),
});
const x1: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x2: ExtensionInput<
typeof stringDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x3: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: true; optional: false }
{ singleton: true; optional: false; internal: false }
> = input;
// @ts-expect-error
const x4: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: true }
{ singleton: false; optional: true; internal: false }
> = input;
unused(x1, x2, x3, x4);
@@ -64,7 +64,7 @@ describe('createExtensionInput', () => {
expect(inputWithContext).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: false, optional: false },
config: { singleton: false, optional: false, internal: false },
withContext: expect.any(Function),
context,
});
@@ -77,28 +77,28 @@ describe('createExtensionInput', () => {
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: true, optional: false },
config: { singleton: true, optional: false, internal: false },
withContext: expect.any(Function),
});
const x1: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: true; optional: false }
{ singleton: true; optional: false; internal: false }
> = input;
// @ts-expect-error
const x2: ExtensionInput<
typeof stringDataRef,
{ singleton: true; optional: false }
{ singleton: true; optional: false; internal: false }
> = input;
// @ts-expect-error
const x3: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x4: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: true }
{ singleton: false; optional: true; internal: false }
> = input;
unused(x1, x2, x3, x4);
@@ -112,28 +112,28 @@ describe('createExtensionInput', () => {
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef, numberDataRef],
config: { singleton: true, optional: true },
config: { singleton: true, optional: true, internal: false },
withContext: expect.any(Function),
});
const x1: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: true; optional: true }
{ singleton: true; optional: true; internal: false }
> = input;
// @ts-expect-error
const x2: ExtensionInput<
typeof stringDataRef,
{ singleton: true; optional: true }
{ singleton: true; optional: true; internal: false }
> = input;
// @ts-expect-error
const x3: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: false }
{ singleton: false; optional: false; internal: false }
> = input;
// @ts-expect-error
const x4: ExtensionInput<
typeof stringDataRef | typeof numberDataRef,
{ singleton: false; optional: true }
{ singleton: false; optional: true; internal: false }
> = input;
unused(x1, x2, x3, x4);
@@ -144,4 +144,14 @@ describe('createExtensionInput', () => {
createExtensionInput([stringDataRef, stringDataRef], { singleton: true }),
).toThrow("ExtensionInput may not have duplicate data refs: 'str'");
});
it('should create an internal input', () => {
const input = createExtensionInput([stringDataRef], { internal: true });
expect(input).toEqual({
$$type: '@backstage/ExtensionInput',
extensionData: [stringDataRef],
config: { singleton: false, optional: false, internal: true },
withContext: expect.any(Function),
});
});
});
@@ -27,9 +27,14 @@ export interface ExtensionInput<
string,
{ optional?: true }
> = ExtensionDataRef,
TConfig extends { singleton: boolean; optional: boolean } = {
TConfig extends {
singleton: boolean;
optional: boolean;
internal?: boolean;
} = {
singleton: boolean;
optional: boolean;
internal?: boolean;
},
> {
readonly $$type: '@backstage/ExtensionInput';
@@ -38,10 +43,60 @@ export interface ExtensionInput<
readonly replaces?: Array<{ id: string; input: string }>;
}
/** @public */
/**
* Creates a new extension input to be passed to the input map of an extension.
*
* @remarks
*
* Extension inputs created with this function can be passed to any `inputs` map
* as part of creating or overriding an extension.
*
* The array of extension data references defines the data this input expects.
* If the required data is not provided by the attached extension, the
* attachment will fail.
*
* The `config` object can be used to restrict the behavior and shape of the
* input. By default an input will accept zero or more extensions from any
* plugin. The following options are available:
*
* - `singleton`: If set to `true`, only one extension can be attached to the
* input at a time. Additional extensions will trigger an app error and be
* ignored.
* - `optional`: If set to `true`, the input is optional and can be omitted,
* this only has an effect if the `singleton` is set to `true`.
* - `internal`: If set to `true`, only extensions from the same plugin will be
* allowed to attach to this input. Other extensions will trigger an app error
* and be ignored.
*
* @param extensionData - The array of extension data references that this input
* expects.
* @param config - The configuration object for the input.
* @returns An extension input declaration.
* @example
* ```ts
* const extension = createExtension({
* attachTo: { id: 'example-parent', input: 'example-input' },
* inputs: {
* content: createExtensionInput([coreExtensionData.reactElement], {
* singleton: true,
* }),
* },
* output: [coreExtensionData.reactElement],
* *factory({ inputs }) {
* const content = inputs.content?.get(coreExtensionData.reactElement);
* yield coreExtensionData.reactElement(<ContentWrapper>{content}</ContentWrapper>);
* },
* });
* ```
* @public
*/
export function createExtensionInput<
UExtensionData extends ExtensionDataRef<unknown, string, { optional?: true }>,
TConfig extends { singleton?: boolean; optional?: boolean },
TConfig extends {
singleton?: boolean;
optional?: boolean;
internal?: boolean;
},
>(
extensionData: Array<UExtensionData>,
config?: TConfig & { replaces?: Array<{ id: string; input: string }> },
@@ -50,6 +105,7 @@ export function createExtensionInput<
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
internal: TConfig['internal'] extends true ? true : false;
}
> {
if (process.env.NODE_ENV !== 'production') {
@@ -81,6 +137,9 @@ export function createExtensionInput<
optional: Boolean(config?.optional) as TConfig['optional'] extends true
? true
: false,
internal: Boolean(config?.internal) as TConfig['internal'] extends true
? true
: false,
},
replaces: config?.replaces,
};
@@ -90,6 +149,7 @@ export function createExtensionInput<
{
singleton: TConfig['singleton'] extends true ? true : false;
optional: TConfig['optional'] extends true ? true : false;
internal: TConfig['internal'] extends true ? true : false;
}
> {
return OpaqueExtensionInput.createInstance(undefined, {
@@ -37,6 +37,7 @@ export type ResolvedInputValueOverrides<
{
optional: infer IOptional extends boolean;
singleton: boolean;
internal?: boolean;
}
>
? IOptional extends true
@@ -44,7 +45,11 @@ export type ResolvedInputValueOverrides<
: KName
: never]: TInputs[KName] extends ExtensionInput<
infer IDataRefs,
{ optional: boolean; singleton: infer ISingleton extends boolean }
{
optional: boolean;
singleton: infer ISingleton extends boolean;
internal?: boolean;
}
>
? ISingleton extends true
? Iterable<ExtensionDataRefToValue<IDataRefs>>
@@ -56,6 +61,7 @@ export type ResolvedInputValueOverrides<
{
optional: infer IOptional extends boolean;
singleton: boolean;
internal?: boolean;
}
>
? IOptional extends true
@@ -63,7 +69,11 @@ export type ResolvedInputValueOverrides<
: never
: never]?: TInputs[KName] extends ExtensionInput<
infer IDataRefs,
{ optional: boolean; singleton: infer ISingleton extends boolean }
{
optional: boolean;
singleton: infer ISingleton extends boolean;
internal?: boolean;
}
>
? ISingleton extends true
? Iterable<ExtensionDataRefToValue<IDataRefs>>
+23 -22
View File
@@ -10,10 +10,9 @@ import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ExtensionBlueprintParams } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { IconComponent } from '@backstage/frontend-plugin-api';
import { NavContentComponent } from '@backstage/frontend-plugin-api';
import { NavContentComponentProps } from '@backstage/frontend-plugin-api';
import { IdentityApi } from '@backstage/frontend-plugin-api';
import { ReactNode } from 'react';
import { SignInPageProps } from '@backstage/frontend-plugin-api';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SwappableComponentRef } from '@backstage/frontend-plugin-api';
import { TranslationMessages } from '@backstage/frontend-plugin-api';
import { TranslationResource } from '@backstage/frontend-plugin-api';
@@ -92,9 +91,21 @@ export const NavContentBlueprint: ExtensionBlueprint<{
};
}>;
export { NavContentComponent };
// @public
export type NavContentComponent = (
props: NavContentComponentProps,
) => JSX.Element | null;
export { NavContentComponentProps };
// @public
export interface NavContentComponentProps {
items: Array<{
icon: IconComponent;
title: string;
routeRef: RouteRef<undefined>;
to: string;
text: string;
}>;
}
// @public
export const RouterBlueprint: ExtensionBlueprint<{
@@ -143,7 +154,11 @@ export const SignInPageBlueprint: ExtensionBlueprint<{
};
}>;
export { SignInPageProps };
// @public
export type SignInPageProps = {
onSignInSuccess(identityApi: IdentityApi): void;
children?: ReactNode;
};
// @public
export const SwappableComponentBlueprint: ExtensionBlueprint<{
@@ -160,14 +175,7 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{
loader: Ref extends SwappableComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<
(props: IInnerComponentProps) => JSX.Element
/**
* Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin.
*
* @public
*/ | null
>)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}) => ExtensionBlueprintParams<{
component: Ref extends SwappableComponentRef<
@@ -181,14 +189,7 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{
loader: Ref extends SwappableComponentRef<infer IInnerComponentProps, any>
?
| (() => (props: IInnerComponentProps) => JSX.Element | null)
| (() => Promise<
(props: IInnerComponentProps) => JSX.Element
/**
* Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin.
*
* @public
*/ | null
>)
| (() => Promise<(props: IInnerComponentProps) => JSX.Element | null>)
: never;
}>;
output: ExtensionDataRef<
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -20,7 +20,8 @@ import {
coreExtensionData,
createExtension,
createExtensionInput,
} from '../wiring';
createFrontendModule,
} from '@backstage/frontend-plugin-api';
import { renderTestApp } from '@backstage/frontend-test-utils';
describe('AppRootWrapperBlueprint', () => {
@@ -63,7 +64,11 @@ describe('AppRootWrapperBlueprint', () => {
},
});
renderTestApp({ extensions: [extension] });
renderTestApp({
features: [
createFrontendModule({ pluginId: 'app', extensions: [extension] }),
],
});
await waitFor(() => expect(screen.getByText('Hello')).toBeInTheDocument());
});
@@ -95,20 +100,28 @@ describe('AppRootWrapperBlueprint', () => {
});
renderTestApp({
extensions: [
extension,
createExtension({
name: 'test-child',
attachTo: { id: 'app-root-wrapper:test', input: 'children' },
output: [coreExtensionData.reactElement],
factory: () => [coreExtensionData.reactElement(<div>Its Me</div>)],
extensions: [],
features: [
createFrontendModule({
pluginId: 'app',
extensions: [
extension,
createExtension({
name: 'test-child',
attachTo: extension.inputs.children,
output: [coreExtensionData.reactElement],
factory: () => [
coreExtensionData.reactElement(<div>Its Me</div>),
],
}),
],
}),
],
config: {
app: {
extensions: [
{
'app-root-wrapper:test': { config: { name: 'Robin' } },
'app-root-wrapper:app': { config: { name: 'Robin' } },
},
],
},
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -15,7 +15,10 @@
*/
import { ReactNode } from 'react';
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
import {
createExtensionBlueprint,
createExtensionDataRef,
} from '@backstage/frontend-plugin-api';
const componentDataRef = createExtensionDataRef<
(props: { children: ReactNode }) => JSX.Element | null
@@ -24,12 +27,9 @@ const componentDataRef = createExtensionDataRef<
/**
* Creates a extensions that render a React wrapper at the app root, enclosing
* the app layout. This is useful for example for adding global React contexts
* and similar.
* and similar. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#AppRootWrapperBlueprint} instead.
* If you were using this blueprint to provide a context for your plugin,
* use `PluginWrapperBlueprint` from `@backstage/frontend-plugin-api/alpha` instead.
*/
export const AppRootWrapperBlueprint = createExtensionBlueprint({
kind: 'app-root-wrapper',
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -14,16 +14,20 @@
* limitations under the License.
*/
import { IconComponent } from '../icons';
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
import { IconComponent } from '@backstage/frontend-plugin-api';
import {
createExtensionBlueprint,
createExtensionDataRef,
} from '@backstage/frontend-plugin-api';
const iconsDataRef = createExtensionDataRef<{
[key in string]: IconComponent;
}>().with({ id: 'core.icons' });
/**
* Creates an extension that adds icon bundles to your app. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#IconBundleBlueprint} instead.
*/
export const IconBundleBlueprint = createExtensionBlueprint({
kind: 'icon-bundle',
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -15,7 +15,10 @@
*/
import { IconComponent, RouteRef } from '@backstage/frontend-plugin-api';
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
import {
createExtensionBlueprint,
createExtensionDataRef,
} from '@backstage/frontend-plugin-api';
/**
* The props for the {@link NavContentComponent}.
@@ -25,7 +28,7 @@ import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
export interface NavContentComponentProps {
/**
* The nav items available to the component. These are all the items created
* with the {@link NavItemBlueprint} in the app.
* with the {@link @backstage/frontend-plugin-api#NavItemBlueprint} in the app.
*
* In addition to the original properties from the nav items, these also
* include a resolved route path as `to`, and duplicated `title` as `text` to
@@ -57,10 +60,9 @@ const componentDataRef = createExtensionDataRef<NavContentComponent>().with({
});
/**
* Creates an extension that replaces the entire nav bar with your own component.
* Creates an extension that replaces the entire nav bar with your own component. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#NavContentBlueprint} instead.
*/
export const NavContentBlueprint = createExtensionBlueprint({
kind: 'nav-content',
@@ -20,7 +20,7 @@ import {
coreExtensionData,
createExtension,
createExtensionInput,
} from '../wiring';
} from '@backstage/frontend-plugin-api';
import { createExtensionTester } from '@backstage/frontend-test-utils';
describe('RouterBlueprint', () => {
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -15,15 +15,19 @@
*/
import { ReactNode } from 'react';
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
import {
createExtensionBlueprint,
createExtensionDataRef,
} from '@backstage/frontend-plugin-api';
const componentDataRef = createExtensionDataRef<
(props: { children: ReactNode }) => JSX.Element | null
>().with({ id: 'app.router.wrapper' });
/**
* Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#RouterBlueprint} instead.
*/
export const RouterBlueprint = createExtensionBlueprint({
kind: 'app-router-component',
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -15,9 +15,12 @@
*/
import { ComponentType, lazy, ReactNode } from 'react';
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
import { ExtensionBoundary } from '../components';
import { IdentityApi } from '../apis';
import {
createExtensionBlueprint,
createExtensionDataRef,
ExtensionBoundary,
IdentityApi,
} from '@backstage/frontend-plugin-api';
/**
* Props for the `SignInPage` component.
@@ -41,10 +44,9 @@ const componentDataRef = createExtensionDataRef<
>().with({ id: 'core.sign-in-page.component' });
/**
* Creates an extension that replaces the sign in page.
* Creates an extension that replaces the sign in page. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#SignInPageBlueprint} instead.
*/
export const SignInPageBlueprint = createExtensionBlueprint({
kind: 'sign-in-page',
@@ -14,9 +14,11 @@
* limitations under the License.
*/
import { renderTestApp } from '@backstage/frontend-test-utils';
import { createSwappableComponent } from '../components';
import {
createSwappableComponent,
PageBlueprint,
} from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from './SwappableComponentBlueprint';
import { PageBlueprint } from './PageBlueprint';
import { screen } from '@testing-library/react';
describe('SwappableComponentBlueprint', () => {
@@ -1,5 +1,5 @@
/*
* Copyright 2025 The Backstage Authors
* Copyright 2026 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.
@@ -13,12 +13,13 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SwappableComponentRef } from '../components';
import { SwappableComponentRef } from '@backstage/frontend-plugin-api';
import {
createExtensionBlueprint,
createExtensionBlueprintParams,
createExtensionDataRef,
} from '../wiring';
} from '@backstage/frontend-plugin-api';
export const componentDataRef = createExtensionDataRef<{
ref: SwappableComponentRef;
@@ -28,10 +29,9 @@ export const componentDataRef = createExtensionDataRef<{
}>().with({ id: 'core.swappableComponent' });
/**
* Blueprint for creating swappable components from a SwappableComponentRef and a loader
* Blueprint for creating swappable components from a SwappableComponentRef and a loader. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#SwappableComponentBlueprint} instead.
*/
export const SwappableComponentBlueprint = createExtensionBlueprint({
kind: 'component',
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { AppTheme } from '../apis/definitions/AppThemeApi';
import { AppTheme } from '@backstage/frontend-plugin-api';
import { ThemeBlueprint } from './ThemeBlueprint';
import { createExtensionTester } from '@backstage/frontend-test-utils';
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -14,18 +14,20 @@
* limitations under the License.
*/
import { AppTheme } from '../apis/definitions/AppThemeApi';
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
import { AppTheme } from '@backstage/frontend-plugin-api';
import {
createExtensionBlueprint,
createExtensionDataRef,
} from '@backstage/frontend-plugin-api';
const themeDataRef = createExtensionDataRef<AppTheme>().with({
id: 'core.theme.theme',
});
/**
* Creates an extension that adds/replaces an app theme.
* Creates an extension that adds/replaces an app theme. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#ThemeBlueprint} instead.
*/
export const ThemeBlueprint = createExtensionBlueprint({
kind: 'theme',
@@ -17,7 +17,7 @@ import { createExtensionTester } from '@backstage/frontend-test-utils';
import {
createTranslationMessages,
createTranslationRef,
} from '../translation';
} from '@backstage/frontend-plugin-api';
import { TranslationBlueprint } from './TranslationBlueprint';
describe('TranslationBlueprint', () => {
@@ -1,5 +1,5 @@
/*
* Copyright 2024 The Backstage Authors
* Copyright 2026 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.
@@ -14,18 +14,21 @@
* limitations under the License.
*/
import { createExtensionBlueprint, createExtensionDataRef } from '../wiring';
import { TranslationMessages, TranslationResource } from '../translation';
import {
createExtensionBlueprint,
createExtensionDataRef,
TranslationMessages,
TranslationResource,
} from '@backstage/frontend-plugin-api';
const translationDataRef = createExtensionDataRef<
TranslationResource | TranslationMessages
>().with({ id: 'core.translation.translation' });
/**
* Creates an extension that adds translations to your app.
* Creates an extension that adds translations to your app. This blueprint is limited to use by the app plugin.
*
* @public
* @deprecated Use {@link @backstage/plugin-app-react#TranslationBlueprint} instead.
*/
export const TranslationBlueprint = createExtensionBlueprint({
kind: 'translation',
+13 -91
View File
@@ -14,94 +14,16 @@
* limitations under the License.
*/
import {
AppRootWrapperBlueprint as _AppRootWrapperBlueprint,
IconBundleBlueprint as _IconBundleBlueprint,
NavContentBlueprint as _NavContentBlueprint,
type NavContentComponent,
type NavContentComponentProps,
RouterBlueprint as _RouterBlueprint,
SignInPageBlueprint as _SignInPageBlueprint,
type SignInPageProps,
SwappableComponentBlueprint as _SwappableComponentBlueprint,
ThemeBlueprint as _ThemeBlueprint,
TranslationBlueprint as _TranslationBlueprint,
} from '@backstage/frontend-plugin-api';
/**
* Creates an extension that renders a React wrapper at the app root, enclosing
* the app layout. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const AppRootWrapperBlueprint = _AppRootWrapperBlueprint;
/**
* Creates an extension that adds/replaces an app theme. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const ThemeBlueprint = _ThemeBlueprint;
/**
* Blueprint for creating swappable components from a SwappableComponentRef and a loader. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const SwappableComponentBlueprint = _SwappableComponentBlueprint;
/**
* Creates an extension that replaces the sign in page. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const SignInPageBlueprint = _SignInPageBlueprint;
/**
* Creates an extension that replaces the router component. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const RouterBlueprint = _RouterBlueprint;
/**
* Creates an extension that replaces the entire nav bar with your own component. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const NavContentBlueprint = _NavContentBlueprint;
/**
* Creates an extension that adds icon bundles to your app. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const IconBundleBlueprint = _IconBundleBlueprint;
/**
* Props for the `SignInPage` component.
*
* @public
*/
export type { SignInPageProps };
/**
* The props for the {@link NavContentComponent}.
*
* @public
*/
export type { NavContentComponentProps };
/**
* A component that renders the nav bar content, to be passed to the {@link NavContentBlueprint}.
*
* @public
*/
export type { NavContentComponent };
/**
* Creates an extension that adds translations to your app. This blueprint is limited to use by the app plugin.
*
* @public
*/
export const TranslationBlueprint = _TranslationBlueprint;
export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint';
export { IconBundleBlueprint } from './IconBundleBlueprint';
export { NavContentBlueprint } from './NavContentBlueprint';
export type {
NavContentComponent,
NavContentComponentProps,
} from './NavContentBlueprint';
export { RouterBlueprint } from './RouterBlueprint';
export { SignInPageBlueprint } from './SignInPageBlueprint';
export type { SignInPageProps } from './SignInPageBlueprint';
export { SwappableComponentBlueprint } from './SwappableComponentBlueprint';
export { ThemeBlueprint } from './ThemeBlueprint';
export { TranslationBlueprint } from './TranslationBlueprint';
@@ -21,7 +21,6 @@ import {
coreExtensionData,
ApiBlueprint,
NavItemBlueprint,
ThemeBlueprint,
useApi,
routeResolutionApiRef,
} from '@backstage/frontend-plugin-api';
@@ -79,7 +78,6 @@ const getOutputColor = createOutputColorGenerator(
[coreExtensionData.routePath.id]: '#ffeb3b',
[coreExtensionData.routeRef.id]: '#9c27b0',
[ApiBlueprint.dataRefs.factory.id]: '#2196f3',
[ThemeBlueprint.dataRefs.theme.id]: '#cddc39',
[NavItemBlueprint.dataRefs.target.id]: '#ff9800',
},
@@ -330,7 +328,6 @@ const legendMap = {
'Route Path': coreExtensionData.routePath,
'Route Ref': coreExtensionData.routeRef,
'Nav Target': NavItemBlueprint.dataRefs.target,
Theme: ThemeBlueprint.dataRefs.theme,
};
function Legend() {
+19 -2
View File
@@ -15,12 +15,12 @@ import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ExtensionInput } from '@backstage/frontend-plugin-api';
import { IconComponent } from '@backstage/frontend-plugin-api';
import { JSX as JSX_2 } from 'react';
import { NavContentComponent } from '@backstage/frontend-plugin-api';
import { NavContentComponent } from '@backstage/plugin-app-react';
import { OverridableExtensionDefinition } from '@backstage/frontend-plugin-api';
import { OverridableFrontendPlugin } from '@backstage/frontend-plugin-api';
import { ReactNode } from 'react';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { SignInPageProps } from '@backstage/frontend-plugin-api';
import { SignInPageProps } from '@backstage/plugin-app-react';
import { SwappableComponentRef } from '@backstage/frontend-plugin-api';
import { TranslationMessages } from '@backstage/frontend-plugin-api';
import { TranslationResource } from '@backstage/frontend-plugin-api';
@@ -40,6 +40,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: true;
optional: false;
internal: false;
}
>;
};
@@ -57,6 +58,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: true;
optional: false;
internal: false;
}
>;
content: ExtensionInput<
@@ -64,6 +66,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: true;
optional: false;
internal: false;
}
>;
};
@@ -89,6 +92,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
content: ExtensionInput<
@@ -100,6 +104,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: true;
optional: true;
internal: true;
}
>;
};
@@ -121,6 +126,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: true;
optional: true;
internal: true;
}
>;
signInPage: ExtensionInput<
@@ -132,6 +138,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: true;
optional: true;
internal: true;
}
>;
children: ExtensionInput<
@@ -139,6 +146,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: true;
optional: false;
internal: false;
}
>;
elements: ExtensionInput<
@@ -146,6 +154,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
wrappers: ExtensionInput<
@@ -157,6 +166,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: true;
}
>;
};
@@ -182,6 +192,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -218,6 +229,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -262,6 +274,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: true;
}
>;
};
@@ -471,6 +484,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: true;
}
>;
};
@@ -592,6 +606,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -669,6 +684,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: true;
}
>;
};
@@ -703,6 +719,7 @@ const appPlugin: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: true;
}
>;
};
@@ -14,10 +14,8 @@
* limitations under the License.
*/
import {
SignInPageBlueprint,
createFrontendModule,
} from '@backstage/frontend-plugin-api';
import { createFrontendModule } from '@backstage/frontend-plugin-api';
import { SignInPageBlueprint } from '@backstage/plugin-app-react';
import { render, screen, waitFor } from '@testing-library/react';
import { useEffect } from 'react';
import { appModulePublicSignIn } from './appModulePublicSignIn';
@@ -20,9 +20,9 @@ import {
createExtensionInput,
createFrontendModule,
createSwappableComponent,
SwappableComponentBlueprint,
swappableComponentsApiRef,
} from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import { DefaultSwappableComponentsApi } from './DefaultSwappableComponentsApi';
import { render, screen } from '@testing-library/react';
import { renderInTestApp, renderTestApp } from '@backstage/frontend-test-utils';
@@ -17,8 +17,8 @@
import {
SwappableComponentRef,
SwappableComponentsApi,
SwappableComponentBlueprint,
} from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import { OpaqueSwappableComponentRef } from '@internal/frontend';
import { lazy } from 'react';
+6 -11
View File
@@ -19,14 +19,16 @@ import {
coreExtensionData,
createExtensionInput,
NavItemBlueprint,
NavContentBlueprint,
NavContentComponentProps,
routeResolutionApiRef,
IconComponent,
RouteRef,
useApi,
NavContentComponent,
} from '@backstage/frontend-plugin-api';
import {
NavContentBlueprint,
NavContentComponent,
NavContentComponentProps,
} from '@backstage/plugin-app-react';
import { Sidebar, SidebarItem } from '@backstage/core-components';
import { useMemo } from 'react';
@@ -90,18 +92,11 @@ export const AppNav = createExtension({
content: createExtensionInput([NavContentBlueprint.dataRefs.component], {
singleton: true,
optional: true,
internal: true,
}),
},
output: [coreExtensionData.reactElement],
*factory({ inputs }) {
if (inputs.content && inputs.content.node.spec.plugin?.id !== 'app') {
// eslint-disable-next-line no-console
console.warn(
`DEPRECATION WARNING: NavContent should only be installed as an extension in the app plugin. ` +
`You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${inputs.content.node.spec.id}`,
);
}
const Content =
inputs.content?.get(NavContentBlueprint.dataRefs.component) ??
DefaultNavContent;
+13 -30
View File
@@ -22,9 +22,6 @@ import {
JSX,
} from 'react';
import {
AppRootWrapperBlueprint,
RouterBlueprint,
SignInPageBlueprint,
coreExtensionData,
discoveryApiRef,
fetchApiRef,
@@ -33,6 +30,11 @@ import {
createExtensionInput,
routeResolutionApiRef,
} from '@backstage/frontend-plugin-api';
import {
AppRootWrapperBlueprint,
RouterBlueprint,
SignInPageBlueprint,
} from '@backstage/plugin-app-react';
import {
DiscoveryApi,
ErrorApi,
@@ -59,37 +61,26 @@ export const AppRoot = createExtension({
router: createExtensionInput([RouterBlueprint.dataRefs.component], {
singleton: true,
optional: true,
internal: true,
}),
signInPage: createExtensionInput([SignInPageBlueprint.dataRefs.component], {
singleton: true,
optional: true,
internal: true,
}),
children: createExtensionInput([coreExtensionData.reactElement], {
singleton: true,
}),
elements: createExtensionInput([coreExtensionData.reactElement]),
wrappers: createExtensionInput([
AppRootWrapperBlueprint.dataRefs.component,
]),
wrappers: createExtensionInput(
[AppRootWrapperBlueprint.dataRefs.component],
{
internal: true,
},
),
},
output: [coreExtensionData.reactElement],
factory({ inputs, apis }) {
if (inputs.router && inputs.router.node.spec.plugin?.id !== 'app') {
// eslint-disable-next-line no-console
console.warn(
`DEPRECATION WARNING: Router should only be installed as an extension in the app plugin. ` +
`You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${inputs.router.node.spec.id}`,
);
}
if (inputs.signInPage && inputs.signInPage.node.spec.plugin?.id !== 'app') {
// eslint-disable-next-line no-console
console.warn(
`DEPRECATION WARNING: SignInPage should only be installed as an extension in the app plugin. ` +
`You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${inputs.signInPage.node.spec.id}`,
);
}
if (isProtectedApp()) {
const identityApi = apis.get(identityApiRef);
if (!identityApi) {
@@ -117,16 +108,8 @@ export const AppRoot = createExtension({
for (const wrapper of inputs.wrappers) {
const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component);
const pluginId = wrapper.node.spec.plugin.id;
if (Component) {
content = <Component>{content}</Component>;
if (pluginId !== 'app') {
// eslint-disable-next-line no-console
console.warn(
`DEPRECATION WARNING: AppRootWrappers should only be installed as an extension in the app plugin. ` +
`You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${wrapper.node.spec.id}`,
);
}
}
}
+2 -14
View File
@@ -22,10 +22,10 @@ import DarkIcon from '@material-ui/icons/Brightness2';
import LightIcon from '@material-ui/icons/WbSunny';
import {
createExtensionInput,
ThemeBlueprint,
ApiBlueprint,
appThemeApiRef,
} from '@backstage/frontend-plugin-api';
import { ThemeBlueprint } from '@backstage/plugin-app-react';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { AppThemeSelector } from '../../../../packages/core-app-api/src/apis/implementations';
@@ -37,6 +37,7 @@ export const AppThemeApi = ApiBlueprint.makeWithOverrides({
inputs: {
themes: createExtensionInput([ThemeBlueprint.dataRefs.theme], {
replaces: [{ id: 'app', input: 'themes' }],
internal: true,
}),
},
factory: (originalFactory, { inputs }) => {
@@ -45,19 +46,6 @@ export const AppThemeApi = ApiBlueprint.makeWithOverrides({
api: appThemeApiRef,
deps: {},
factory: () => {
const nonAppExtensions = inputs.themes.filter(
i => i.node.spec.plugin?.id !== 'app',
);
if (nonAppExtensions.length > 0) {
const list = nonAppExtensions.map(i => i.node.spec.id).join(', ');
// eslint-disable-next-line no-console
console.warn(
`DEPRECATION WARNING: Theme should only be installed as an extension in the app plugin. ` +
`You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${list}`,
);
}
return AppThemeSelector.createWithStorage(
inputs.themes.map(i => i.get(ThemeBlueprint.dataRefs.theme)),
);
@@ -13,7 +13,7 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { SignInPageBlueprint } from '@backstage/frontend-plugin-api';
import { SignInPageBlueprint } from '@backstage/plugin-app-react';
import { SignInPage } from '@backstage/core-components';
export const DefaultSignInPage = SignInPageBlueprint.make({
+2 -14
View File
@@ -16,10 +16,10 @@
import {
createExtensionInput,
IconBundleBlueprint,
ApiBlueprint,
iconsApiRef,
} from '@backstage/frontend-plugin-api';
import { IconBundleBlueprint } from '@backstage/plugin-app-react';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
import { DefaultIconsApi } from '../../../../packages/frontend-app-api/src/apis/implementations/IconsApi';
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
@@ -33,6 +33,7 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({
inputs: {
icons: createExtensionInput([IconBundleBlueprint.dataRefs.icons], {
replaces: [{ id: 'app', input: 'icons' }],
internal: true,
}),
},
factory: (originalFactory, { inputs }) => {
@@ -41,19 +42,6 @@ export const IconsApi = ApiBlueprint.makeWithOverrides({
api: iconsApiRef,
deps: {},
factory: () => {
const nonAppExtensions = inputs.icons.filter(
i => i.node.spec.plugin?.id !== 'app',
);
if (nonAppExtensions.length > 0) {
const list = nonAppExtensions.map(i => i.node.spec.id).join(', ');
// eslint-disable-next-line no-console
console.warn(
`DEPRECATION WARNING: IconBundle should only be installed as an extension in the app plugin. ` +
`You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${list}`,
);
}
return new DefaultIconsApi(
inputs.icons
.map(i => i.get(IconBundleBlueprint.dataRefs.icons))
@@ -15,11 +15,11 @@
*/
import {
SwappableComponentBlueprint,
createExtensionInput,
ApiBlueprint,
swappableComponentsApiRef,
} from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import { DefaultSwappableComponentsApi } from '../apis/SwappableComponentsApi';
/**
@@ -28,9 +28,12 @@ import { DefaultSwappableComponentsApi } from '../apis/SwappableComponentsApi';
export const SwappableComponentsApi = ApiBlueprint.makeWithOverrides({
name: 'swappable-components',
inputs: {
components: createExtensionInput([
SwappableComponentBlueprint.dataRefs.component,
]),
components: createExtensionInput(
[SwappableComponentBlueprint.dataRefs.component],
{
internal: true,
},
),
},
factory: (originalFactory, { inputs }) => {
return originalFactory(defineParams =>
@@ -38,25 +41,8 @@ export const SwappableComponentsApi = ApiBlueprint.makeWithOverrides({
api: swappableComponentsApiRef,
deps: {},
factory: () => {
const nonAppExtensions = inputs.components.filter(
i => i.node.spec.plugin?.id !== 'app',
);
if (nonAppExtensions.length > 0) {
// eslint-disable-next-line no-console
console.warn(
`SwappableComponents should only be installed as an extension in the app plugin. You can either use appPlugin.override(), or provide a module for the app-plugin with the extension there instead. Invalid extensions: ${nonAppExtensions
.map(i => i.node.spec.id)
.join(', ')}`,
);
}
const appExtensions = inputs.components.filter(
i => i.node.spec.plugin?.id === 'app',
);
return DefaultSwappableComponentsApi.fromComponents(
appExtensions.map(i =>
inputs.components.map(i =>
i.get(SwappableComponentBlueprint.dataRefs.component),
),
);
+2 -15
View File
@@ -15,9 +15,9 @@
*/
import {
ApiBlueprint,
TranslationBlueprint,
createExtensionInput,
} from '@backstage/frontend-plugin-api';
import { TranslationBlueprint } from '@backstage/plugin-app-react';
import {
appLanguageApiRef,
translationApiRef,
@@ -34,7 +34,7 @@ export const TranslationsApi = ApiBlueprint.makeWithOverrides({
inputs: {
translations: createExtensionInput(
[TranslationBlueprint.dataRefs.translation],
{ replaces: [{ id: 'app', input: 'translations' }] },
{ replaces: [{ id: 'app', input: 'translations' }], internal: true },
),
},
factory: (originalFactory, { inputs }) => {
@@ -43,19 +43,6 @@ export const TranslationsApi = ApiBlueprint.makeWithOverrides({
api: translationApiRef,
deps: { languageApi: appLanguageApiRef },
factory: ({ languageApi }) => {
const nonAppExtensions = inputs.translations.filter(
i => i.node.spec.plugin?.id !== 'app',
);
if (nonAppExtensions.length > 0) {
const list = nonAppExtensions.map(i => i.node.spec.id).join(', ');
// eslint-disable-next-line no-console
console.warn(
`DEPRECATION WARNING: Translations should only be installed as an extension in the app plugin. ` +
`You can either use appPlugin.override(), or a module for the app plugin. The following extension will be ignored in the future: ${list}`,
);
}
return I18nextTranslationApi.create({
languageApi,
resources: inputs.translations.map(i =>
+1 -2
View File
@@ -17,9 +17,8 @@ import {
NotFoundErrorPage as SwappableNotFoundErrorPage,
Progress as SwappableProgress,
ErrorDisplay as SwappableErrorDisplay,
SwappableComponentBlueprint,
} from '@backstage/frontend-plugin-api';
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import {
ErrorPage,
ErrorPanel,
+7
View File
@@ -345,6 +345,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -794,6 +795,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
cards: ExtensionInput<
@@ -822,6 +824,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -1000,6 +1003,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -1064,6 +1068,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
contents: ExtensionInput<
@@ -1105,6 +1110,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
contextMenuItems: ExtensionInput<
@@ -1119,6 +1125,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
+1
View File
@@ -92,6 +92,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: true;
internal: false;
}
>;
};
+1
View File
@@ -85,6 +85,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: true;
optional: true;
internal: false;
}
>;
};
@@ -219,6 +219,7 @@ export const formFieldsApi: OverridableExtensionDefinition<{
{
singleton: false;
optional: false;
internal: false;
}
>;
};
+4
View File
@@ -91,6 +91,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -118,6 +119,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -218,6 +220,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -411,6 +414,7 @@ export const formDecoratorsApi: OverridableExtensionDefinition<{
{
singleton: false;
optional: false;
internal: false;
}
>;
};
+6
View File
@@ -97,6 +97,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
resultTypes: ExtensionInput<
@@ -112,6 +113,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
searchFilters: ExtensionInput<
@@ -125,6 +127,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -215,6 +218,7 @@ export const searchPage: OverridableExtensionDefinition<{
{
singleton: false;
optional: false;
internal: false;
}
>;
resultTypes: ExtensionInput<
@@ -230,6 +234,7 @@ export const searchPage: OverridableExtensionDefinition<{
{
singleton: false;
optional: false;
internal: false;
}
>;
searchFilters: ExtensionInput<
@@ -243,6 +248,7 @@ export const searchPage: OverridableExtensionDefinition<{
{
singleton: false;
optional: false;
internal: false;
}
>;
};
+4
View File
@@ -84,6 +84,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: boolean;
optional: boolean;
internal?: boolean;
}
>;
};
@@ -146,6 +147,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
emptyState: ExtensionInput<
@@ -159,6 +161,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: true;
optional: true;
internal: false;
}
>;
};
@@ -290,6 +293,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: false;
optional: false;
internal: false;
}
>;
};
@@ -66,6 +66,7 @@ const _default: OverridableFrontendPlugin<
{
singleton: true;
optional: true;
internal: false;
}
>;
};
+3
View File
@@ -3571,6 +3571,7 @@ __metadata:
"@backstage/frontend-app-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/frontend-test-utils": "workspace:^"
"@backstage/plugin-app-react": "workspace:^"
"@backstage/plugin-catalog": "workspace:^"
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
@@ -3861,6 +3862,7 @@ __metadata:
"@backstage/frontend-app-api": "workspace:^"
"@backstage/frontend-plugin-api": "workspace:^"
"@backstage/plugin-app": "workspace:^"
"@backstage/plugin-app-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@react-hookz/web": "npm:^24.0.0"
"@testing-library/jest-dom": "npm:^6.0.0"
@@ -30697,6 +30699,7 @@ __metadata:
"@backstage/integration-react": "workspace:^"
"@backstage/plugin-api-docs": "workspace:^"
"@backstage/plugin-app": "workspace:^"
"@backstage/plugin-app-react": "workspace:^"
"@backstage/plugin-app-visualizer": "workspace:^"
"@backstage/plugin-auth": "workspace:^"
"@backstage/plugin-auth-react": "workspace:^"