Merge pull request #19768 from backstage/mob/plugin-connection

frontend-*-api: introduce ExtensionBoundary and pass source to extension factories
This commit is contained in:
Patrik Oldsberg
2023-09-04 14:43:00 +02:00
committed by GitHub
10 changed files with 154 additions and 46 deletions
+6 -7
View File
@@ -41,13 +41,11 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
// pull in default extension instance from discovered packages
// apply config to adjust default extension instances and add more
const extensionParams = mergeExtensionParameters(
[
...options.plugins.flatMap(plugin => plugin.extensions),
...builtinExtensions,
],
readAppExtensionParameters(appConfig),
);
const extensionParams = mergeExtensionParameters({
sources: options.plugins,
builtinExtensions,
parameters: readAppExtensionParameters(appConfig),
});
// TODO: validate the config of all extension instances
// We do it at this point to ensure that merging (if any) of config has already happened
@@ -96,6 +94,7 @@ export function createApp(options: { plugins: BackstagePlugin[] }): {
return createExtensionInstance({
extension: instanceParams.extension,
source: instanceParams.source,
config: instanceParams.config,
attachments,
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Extension } from '@backstage/frontend-plugin-api';
import { BackstagePlugin, Extension } from '@backstage/frontend-plugin-api';
import mapValues from 'lodash/mapValues';
/** @internal */
@@ -28,9 +28,10 @@ export interface ExtensionInstance {
export function createExtensionInstance(options: {
extension: Extension<unknown>;
config: unknown;
source?: BackstagePlugin;
attachments: Record<string, ExtensionInstance[]>;
}): ExtensionInstance {
const { extension, config, attachments } = options;
const { extension, config, source, attachments } = options;
const extensionData = new Map<string, unknown>();
let parsedConfig: unknown;
@@ -44,6 +45,7 @@ export function createExtensionInstance(options: {
try {
extension.factory({
source,
config: parsedConfig,
bind: mapValues(extension.output, ref => {
return (value: unknown) => extensionData.set(ref.id, value);
@@ -15,7 +15,7 @@
*/
import { ConfigReader } from '@backstage/config';
import { Extension } from '@backstage/frontend-plugin-api';
import { createPlugin, Extension } from '@backstage/frontend-plugin-api';
import { JsonValue } from '@backstage/types';
import {
expandShorthandExtensionParameters,
@@ -33,15 +33,25 @@ function makeExt(id: string, status: 'disabled' | 'enabled' = 'enabled') {
describe('mergeExtensionParameters', () => {
it('should filter out disabled extension instances', () => {
expect(mergeExtensionParameters([makeExt('a', 'disabled')], [])).toEqual(
[],
);
expect(
mergeExtensionParameters({
sources: [],
builtinExtensions: [makeExt('a', 'disabled')],
parameters: [],
}),
).toEqual([]);
});
it('should pass through extension instances', () => {
const a = makeExt('a');
const b = makeExt('b');
expect(mergeExtensionParameters([a, b], [])).toEqual([
expect(
mergeExtensionParameters({
sources: [],
builtinExtensions: [a, b],
parameters: [],
}),
).toEqual([
{ extension: a, at: 'root' },
{ extension: b, at: 'root' },
]);
@@ -50,18 +60,20 @@ describe('mergeExtensionParameters', () => {
it('should override attachment points', () => {
const a = makeExt('a');
const b = makeExt('b');
const pluginA = createPlugin({ id: 'test', extensions: [a] });
expect(
mergeExtensionParameters(
[a, b],
[
mergeExtensionParameters({
sources: [pluginA],
builtinExtensions: [b],
parameters: [
{
id: 'b',
at: 'derp',
},
],
),
}),
).toEqual([
{ extension: a, at: 'root' },
{ extension: a, at: 'root', source: pluginA },
{ extension: b, at: 'derp' },
]);
});
@@ -69,10 +81,12 @@ describe('mergeExtensionParameters', () => {
it('should fully override configuration and duplicate', () => {
const a = makeExt('a');
const b = makeExt('b');
const plugin = createPlugin({ id: 'test', extensions: [a, b] });
expect(
mergeExtensionParameters(
[a, b],
[
mergeExtensionParameters({
sources: [plugin],
builtinExtensions: [],
parameters: [
{
id: 'a',
config: { foo: { bar: 1 } },
@@ -86,10 +100,10 @@ describe('mergeExtensionParameters', () => {
config: { foo: { qux: 3 } },
},
],
),
}),
).toEqual([
{ extension: a, at: 'root', config: { foo: { bar: 1 } } },
{ extension: b, at: 'root', config: { foo: { qux: 3 } } },
{ extension: a, at: 'root', source: plugin, config: { foo: { bar: 1 } } },
{ extension: b, at: 'root', source: plugin, config: { foo: { qux: 3 } } },
]);
});
@@ -97,9 +111,10 @@ describe('mergeExtensionParameters', () => {
const a = makeExt('a', 'disabled');
const b = makeExt('b', 'disabled');
expect(
mergeExtensionParameters(
[a, b],
[
mergeExtensionParameters({
sources: [createPlugin({ id: 'empty', extensions: [] })],
builtinExtensions: [a, b],
parameters: [
{
id: 'b',
disabled: false,
@@ -109,7 +124,7 @@ describe('mergeExtensionParameters', () => {
disabled: false,
},
],
),
}),
).toEqual([
{ extension: b, at: 'root' },
{ extension: a, at: 'root' },
@@ -15,7 +15,7 @@
*/
import { Config } from '@backstage/config';
import { Extension } from '@backstage/frontend-plugin-api';
import { BackstagePlugin, Extension } from '@backstage/frontend-plugin-api';
import { JsonValue } from '@backstage/types';
export interface ExtensionParameters {
@@ -183,23 +183,41 @@ export function expandShorthandExtensionParameters(
export interface ExtensionInstanceParameters {
extension: Extension<unknown>;
source?: BackstagePlugin;
at: string;
config?: unknown;
}
/** @internal */
export function mergeExtensionParameters(
base: Extension<unknown>[],
parameters: Array<ExtensionParameters>,
): ExtensionInstanceParameters[] {
const overrides = base.map(extension => ({
extension,
params: {
at: extension.at,
disabled: extension.disabled,
config: undefined as unknown,
},
}));
export function mergeExtensionParameters(options: {
sources: BackstagePlugin[];
builtinExtensions: Extension<unknown>[];
parameters: Array<ExtensionParameters>;
}): ExtensionInstanceParameters[] {
const { sources, builtinExtensions, parameters } = options;
const overrides = [
...sources.flatMap(plugin =>
plugin.extensions.map(extension => ({
extension,
params: {
source: plugin,
at: extension.at,
disabled: extension.disabled,
config: undefined as unknown,
},
})),
),
...builtinExtensions.map(extension => ({
extension,
params: {
source: undefined,
at: extension.at,
disabled: extension.disabled,
config: undefined as unknown,
},
})),
];
for (const overrideParam of parameters) {
const existingIndex = overrides.findIndex(
@@ -234,6 +252,7 @@ export function mergeExtensionParameters(
.map(param => ({
extension: param.extension,
at: param.params.at,
source: param.params.source,
config: param.params.config,
}));
}
@@ -9,6 +9,8 @@ import { AnyApiFactory } from '@backstage/core-plugin-api';
import { AnyApiRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { JsonObject } from '@backstage/types';
import { default as React_2 } from 'react';
import { ReactNode } from 'react';
import { z } from 'zod';
import { ZodSchema } from 'zod';
import { ZodTypeDef } from 'zod';
@@ -95,6 +97,7 @@ export interface CreateExtensionOptions<
disabled?: boolean;
// (undocumented)
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<TData>;
config: TConfig;
inputs: {
@@ -166,6 +169,7 @@ export interface Extension<TConfig> {
disabled: boolean;
// (undocumented)
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<AnyExtensionDataMap>;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
@@ -183,6 +187,19 @@ export interface Extension<TConfig> {
output: AnyExtensionDataMap;
}
// @public (undocumented)
export function ExtensionBoundary(
props: ExtensionBoundaryProps,
): React_2.JSX.Element;
// @public (undocumented)
export interface ExtensionBoundaryProps {
// (undocumented)
children: ReactNode;
// (undocumented)
source?: BackstagePlugin;
}
// @public (undocumented)
export type ExtensionDataBind<TData extends AnyExtensionDataMap> = {
[K in keyof TData]: (value: TData[K]['T']) => void;
@@ -0,0 +1,29 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import React, { ReactNode } from 'react';
import { BackstagePlugin } from '../wiring';
/** @public */
export interface ExtensionBoundaryProps {
children: ReactNode;
source?: BackstagePlugin;
}
/** @public */
export function ExtensionBoundary(props: ExtensionBoundaryProps) {
return <>{props.children}</>;
}
@@ -0,0 +1,20 @@
/*
* Copyright 2023 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
export {
ExtensionBoundary,
type ExtensionBoundaryProps,
} from './ExtensionBoundary';
@@ -15,6 +15,7 @@
*/
import React from 'react';
import { ExtensionBoundary } from '../components';
import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod';
import {
AnyExtensionDataMap,
@@ -72,7 +73,7 @@ export function createPageExtension<
},
inputs: options.inputs,
configSchema,
factory({ bind, config, inputs }) {
factory({ bind, config, inputs, source }) {
const LazyComponent = React.lazy(() =>
options
.component({ config, inputs })
@@ -80,9 +81,11 @@ export function createPageExtension<
);
bind.path(config.path);
bind.component(() => (
<React.Suspense fallback="...">
<LazyComponent />
</React.Suspense>
<ExtensionBoundary source={source}>
<React.Suspense fallback="...">
<LazyComponent />
</React.Suspense>
</ExtensionBoundary>
));
},
});
@@ -24,6 +24,7 @@ export {
createSchemaFromZod,
type PortableSchema,
} from './createSchemaFromZod';
export * from './components';
export * from './extensions';
export {
coreExtensionData,
@@ -17,6 +17,7 @@
import { AnyApiFactory } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { PortableSchema } from './createSchemaFromZod';
import { BackstagePlugin } from './wiring';
/** @public */
export type ExtensionDataRef<T> = {
@@ -64,6 +65,7 @@ export interface CreateExtensionOptions<
output: TData;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<TData>;
config: TConfig;
inputs: {
@@ -84,6 +86,7 @@ export interface Extension<TConfig> {
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<AnyExtensionDataMap>;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;