diff --git a/.changeset/common-heads-start.md b/.changeset/common-heads-start.md index 15ef218b12..2b0d5c473d 100644 --- a/.changeset/common-heads-start.md +++ b/.changeset/common-heads-start.md @@ -4,6 +4,7 @@ Moved the following blueprints from `@backstage/frontend-plugin-api`: +- `AppRootWrapperBlueprint` - `IconBundleBlueprint` - `NavContentBlueprint` - `RouterBlueprint` diff --git a/.changeset/fine-ears-yell.md b/.changeset/fine-ears-yell.md deleted file mode 100644 index 06c5df58c4..0000000000 --- a/.changeset/fine-ears-yell.md +++ /dev/null @@ -1,5 +0,0 @@ ---- -'@backstage/plugin-app-react': minor ---- - -Added `AppRootWrapperBlueprint` that replaces the blueprint with the same name that was deprecated in `@backstage/frontend-plugin-api`. These blueprints are not directly interchangeable, the new one exported from this package can only be used in `app` plugin modules and is restricted from use in other plugins. diff --git a/.changeset/moody-wasps-travel.md b/.changeset/moody-wasps-travel.md index fdb46dcc2d..30a526345d 100644 --- a/.changeset/moody-wasps-travel.md +++ b/.changeset/moody-wasps-travel.md @@ -2,4 +2,4 @@ '@backstage/plugin-app': patch --- -Implemented support for the new `PluginWrapperBlueprint` from `@backstage/frontend-plugin-api` and `AppRootWrapperBlueprint` from the new `@backstage/plugin-app-react` library. +Implemented support for the new `PluginWrapperBlueprint` from `@backstage/frontend-plugin-api/alpha`. diff --git a/.changeset/slick-beans-relax.md b/.changeset/slick-beans-relax.md index b4fc3c3175..1bc6443d80 100644 --- a/.changeset/slick-beans-relax.md +++ b/.changeset/slick-beans-relax.md @@ -4,6 +4,7 @@ The following blueprints are being restricted to only be used in app plugin overrides and modules. They are being moved to the `@backstage/plugin-app-react` package and have been deprecated: +- `AppRootWrapperBlueprint` - `IconBundleBlueprint` - `NavContentBlueprint` - `RouterBlueprint` diff --git a/.changeset/violet-otters-share.md b/.changeset/violet-otters-share.md index e60ba5041d..85f0aaf028 100644 --- a/.changeset/violet-otters-share.md +++ b/.changeset/violet-otters-share.md @@ -2,4 +2,4 @@ '@backstage/frontend-plugin-api': patch --- -Added an alpha `PluginWrapperBlueprint` exported from `@backstage/frontend-plugin-api/alpha`, which can install components that will wrap all plugin elements. The `AppRootWrapperBlueprint` has also been deprecated and should be replaced either with the new plugin wrapper, or for app overrides, the new blueprint with the same name from `@backstage/plugin-app-react`. +Added an alpha `PluginWrapperBlueprint` exported from `@backstage/frontend-plugin-api/alpha`, which can install components that will wrap all plugin elements. diff --git a/.changeset/warm-pets-accept.md b/.changeset/warm-pets-accept.md index 5bf62cbb1a..5fc7bcd6d3 100644 --- a/.changeset/warm-pets-accept.md +++ b/.changeset/warm-pets-accept.md @@ -4,6 +4,7 @@ The following blueprints are being restricted to only be used in app plugin overrides and modules. They will now produce a deprecation warning when used outside of the app plugin: +- `AppRootWrapperBlueprint` - `IconBundleBlueprint` - `NavContentBlueprint` - `RouterBlueprint` diff --git a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx index 0fbefa7d08..184a8c3327 100644 --- a/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx +++ b/packages/frontend-plugin-api/src/blueprints/AppRootWrapperBlueprint.tsx @@ -27,11 +27,9 @@ const componentDataRef = createExtensionDataRef< * and similar. * * @public - * @deprecated Use `PluginWrapperBlueprint` from - * `@backstage/frontend-plugin-api/alpha` instead if you want to wrap all plugin - * components in the same wrapper. If you want to wrap the entire app, use the - * `AppRootWrapperBlueprint` from `@backstage/plugin-app-react` instead, - * although note that only app modules are able to use that blueprint. + * @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', diff --git a/plugins/app-react/report.api.md b/plugins/app-react/report.api.md index 415ffe51bd..251152a9e6 100644 --- a/plugins/app-react/report.api.md +++ b/plugins/app-react/report.api.md @@ -22,13 +22,12 @@ import { TranslationResource } from '@backstage/frontend-plugin-api'; export const AppRootWrapperBlueprint: ExtensionBlueprint<{ kind: 'app-root-wrapper'; params: { + Component?: [error: 'Use the `component` parameter instead']; component: (props: { children: ReactNode }) => JSX.Element | null; }; output: ExtensionDataRef< - ComponentType<{ - children: ReactNode; - }>, - 'app.root-wrapper-component', + (props: { children: ReactNode }) => JSX.Element | null, + 'app.root.wrapper', {} >; inputs: {}; @@ -36,10 +35,8 @@ export const AppRootWrapperBlueprint: ExtensionBlueprint<{ configInput: {}; dataRefs: { component: ConfigurableExtensionDataRef< - ComponentType<{ - children: ReactNode; - }>, - 'app.root-wrapper-component', + (props: { children: ReactNode }) => JSX.Element | null, + 'app.root.wrapper', {} >; }; @@ -163,7 +160,14 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{ loader: Ref extends SwappableComponentRef ? | (() => (props: IInnerComponentProps) => JSX.Element | null) - | (() => Promise<(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 + >) : never; }) => ExtensionBlueprintParams<{ component: Ref extends SwappableComponentRef< @@ -177,7 +181,14 @@ export const SwappableComponentBlueprint: ExtensionBlueprint<{ loader: Ref extends SwappableComponentRef ? | (() => (props: IInnerComponentProps) => JSX.Element | null) - | (() => Promise<(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 + >) : never; }>; output: ExtensionDataRef< diff --git a/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.test.tsx b/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.test.tsx deleted file mode 100644 index 32aaeddeba..0000000000 --- a/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.test.tsx +++ /dev/null @@ -1,138 +0,0 @@ -/* - * Copyright 2024 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 { Fragment } from 'react'; -import { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; -import { screen, waitFor } from '@testing-library/react'; -import { - coreExtensionData, - createExtension, - createExtensionInput, - createFrontendModule, -} from '@backstage/frontend-plugin-api'; -import { renderTestApp } from '@backstage/frontend-test-utils'; - -describe('AppRootWrapperBlueprint', () => { - it('should return an extension with sensible defaults', () => { - const extension = AppRootWrapperBlueprint.make({ - params: { - component: () =>
Hello
, - }, - }); - - expect(extension).toMatchInlineSnapshot(` - { - "$$type": "@backstage/ExtensionDefinition", - "T": undefined, - "attachTo": { - "id": "app/root", - "input": "wrappers", - }, - "configSchema": undefined, - "disabled": false, - "factory": [Function], - "inputs": {}, - "kind": "app-root-wrapper", - "name": undefined, - "output": [ - [Function], - ], - "override": [Function], - "toString": [Function], - "version": "v2", - } - `); - }); - - it('should render a simple component wrapper', async () => { - const extension = AppRootWrapperBlueprint.make({ - name: 'test', - params: { - component: () =>
Hello
, - }, - }); - - renderTestApp({ - features: [ - createFrontendModule({ - pluginId: 'app', - extensions: [extension], - }), - ], - }); - - await waitFor(() => expect(screen.getByText('Hello')).toBeInTheDocument()); - }); - - it('should render a complex component wrapper', async () => { - const extension = AppRootWrapperBlueprint.makeWithOverrides({ - config: { - schema: { - name: z => z.string(), - }, - }, - inputs: { - children: createExtensionInput([coreExtensionData.reactElement]), - }, - *factory(originalFactory, { inputs, config }) { - yield* originalFactory({ - component: ({ children }) => ( -
- {inputs.children.flatMap((c, index) => ( - - {c.get(coreExtensionData.reactElement)} - - ))} - {children} -
- ), - }); - }, - }); - - renderTestApp({ - features: [ - createFrontendModule({ - pluginId: 'app', - extensions: [ - extension, - createExtension({ - name: 'test-child', - attachTo: extension.inputs.children, - output: [coreExtensionData.reactElement], - factory: () => [ - coreExtensionData.reactElement(
Its Me
), - ], - }), - ], - }), - ], - config: { - app: { - extensions: [ - { - 'app-root-wrapper:app': { config: { name: 'Robin' } }, - }, - ], - }, - }, - }); - - await waitFor(() => { - expect(screen.getByTestId('Robin-1')).toBeInTheDocument(); - expect(screen.getByText('Its Me')).toBeInTheDocument(); - }); - }); -}); diff --git a/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.tsx b/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.tsx deleted file mode 100644 index 79a35e2bc0..0000000000 --- a/plugins/app-react/src/blueprints/AppRootWrapperBlueprint.tsx +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2024 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 { ComponentType, ReactNode } from 'react'; -import { - createExtensionBlueprint, - createExtensionDataRef, -} from '@backstage/frontend-plugin-api'; - -const componentDataRef = createExtensionDataRef< - ComponentType<{ children: ReactNode }> ->().with({ id: 'app.root-wrapper-component' }); - -/** - * Creates an extension that renders a React wrapper at the app root, enclosing - * the app layout. - * - * @remarks - * - * This is useful for example for adding global React contexts and similar. This - * extension is only available for use by app modules, plugins should use the - * {@link @backstage/frontend-plugin-api#PluginWrapperBlueprint} instead. - * - * @public - */ -export const AppRootWrapperBlueprint = createExtensionBlueprint({ - kind: 'app-root-wrapper', - attachTo: { id: 'app/root', input: 'wrappers' }, - output: [componentDataRef], - dataRefs: { - component: componentDataRef, - }, - *factory(params: { - component: (props: { children: ReactNode }) => JSX.Element | null; - }) { - yield componentDataRef(params.component); - }, -}); diff --git a/plugins/app-react/src/blueprints/index.ts b/plugins/app-react/src/blueprints/index.ts index cf036aa060..f5e4d7ac4a 100644 --- a/plugins/app-react/src/blueprints/index.ts +++ b/plugins/app-react/src/blueprints/index.ts @@ -15,6 +15,7 @@ */ import { + AppRootWrapperBlueprint as _AppRootWrapperBlueprint, IconBundleBlueprint as _IconBundleBlueprint, NavContentBlueprint as _NavContentBlueprint, type NavContentComponent, @@ -27,7 +28,13 @@ import { TranslationBlueprint as _TranslationBlueprint, } from '@backstage/frontend-plugin-api'; -export { AppRootWrapperBlueprint } from './AppRootWrapperBlueprint'; +/** + * 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. diff --git a/plugins/app/report.api.md b/plugins/app/report.api.md index 57103b8000..ea34a1731f 100644 --- a/plugins/app/report.api.md +++ b/plugins/app/report.api.md @@ -149,22 +149,11 @@ const appPlugin: OverridableFrontendPlugin< } >; wrappers: ExtensionInput< - | ConfigurableExtensionDataRef< - (props: { children: ReactNode }) => JSX.Element | null, - 'app.root.wrapper', - { - optional: true; - } - > - | ConfigurableExtensionDataRef< - ComponentType<{ - children: ReactNode; - }>, - 'app.root-wrapper-component', - { - optional: true; - } - >, + ConfigurableExtensionDataRef< + (props: { children: ReactNode }) => JSX.Element | null, + 'app.root.wrapper', + {} + >, { singleton: false; optional: false; diff --git a/plugins/app/src/extensions/AppRoot.tsx b/plugins/app/src/extensions/AppRoot.tsx index e51c63ca28..c9f3035e5f 100644 --- a/plugins/app/src/extensions/AppRoot.tsx +++ b/plugins/app/src/extensions/AppRoot.tsx @@ -22,7 +22,7 @@ import { JSX, } from 'react'; import { - AppRootWrapperBlueprint as OldAppRootWrapperBlueprint, + AppRootWrapperBlueprint, RouterBlueprint, SignInPageBlueprint, coreExtensionData, @@ -44,7 +44,6 @@ import { identityApiRef, useApi, } from '@backstage/core-plugin-api'; -import { AppRootWrapperBlueprint } from '@backstage/plugin-app-react'; // eslint-disable-next-line @backstage/no-relative-monorepo-imports import { isProtectedApp } from '../../../../packages/core-app-api/src/app/isProtectedApp'; import { BrowserRouter } from 'react-router-dom'; @@ -70,8 +69,7 @@ export const AppRoot = createExtension({ }), elements: createExtensionInput([coreExtensionData.reactElement]), wrappers: createExtensionInput([ - OldAppRootWrapperBlueprint.dataRefs.component.optional(), - AppRootWrapperBlueprint.dataRefs.component.optional(), + AppRootWrapperBlueprint.dataRefs.component, ]), }, output: [coreExtensionData.reactElement], @@ -121,25 +119,14 @@ export const AppRoot = createExtension({ const Component = wrapper.get(AppRootWrapperBlueprint.dataRefs.component); const pluginId = wrapper.node.spec.plugin.id; if (Component) { - if (pluginId === 'app') { - content = {content}; - } else { + content = {content}; + if (pluginId !== 'app') { // eslint-disable-next-line no-console console.warn( - `Warning: The app root wrapper extension from the '${pluginId}' plugin was ignored, only the 'app' plugin is allowed to provide app root wrappers.`, + `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}`, ); } - } else { - const OldComponent = wrapper.get( - OldAppRootWrapperBlueprint.dataRefs.component, - ); - if (OldComponent) { - // eslint-disable-next-line no-console - console.warn( - `DEPRECATION WARNING: The ${pluginId} plugin provided a deprecated app root wrapper extension using the AppRootWrapperBlueprint from @backstage/frontend-plugin-api. This will break in a future release.`, - ); - content = {content}; - } } }