frontend-plugin-api: simplify deprecation of AppRootWrapperBlueprint
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -4,6 +4,7 @@
|
||||
|
||||
Moved the following blueprints from `@backstage/frontend-plugin-api`:
|
||||
|
||||
- `AppRootWrapperBlueprint`
|
||||
- `IconBundleBlueprint`
|
||||
- `NavContentBlueprint`
|
||||
- `RouterBlueprint`
|
||||
|
||||
@@ -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.
|
||||
@@ -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`.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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`
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<infer IInnerComponentProps, any>
|
||||
?
|
||||
| (() => (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<infer IInnerComponentProps, any>
|
||||
?
|
||||
| (() => (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<
|
||||
|
||||
@@ -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: () => <div>Hello</div>,
|
||||
},
|
||||
});
|
||||
|
||||
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: () => <div>Hello</div>,
|
||||
},
|
||||
});
|
||||
|
||||
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 }) => (
|
||||
<div data-testid={`${config.name}-${inputs.children.length}`}>
|
||||
{inputs.children.flatMap((c, index) => (
|
||||
<Fragment key={index}>
|
||||
{c.get(coreExtensionData.reactElement)}
|
||||
</Fragment>
|
||||
))}
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
renderTestApp({
|
||||
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:app': { config: { name: 'Robin' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
await waitFor(() => {
|
||||
expect(screen.getByTestId('Robin-1')).toBeInTheDocument();
|
||||
expect(screen.getByText('Its Me')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
},
|
||||
});
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 = <Component>{content}</Component>;
|
||||
} else {
|
||||
content = <Component>{content}</Component>;
|
||||
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 = <OldComponent>{content}</OldComponent>;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user