Merge pull request #19785 from backstage/mob/optional-data

frontend-plugin-api: optional extension data
This commit is contained in:
Patrik Oldsberg
2023-09-05 19:15:36 +02:00
committed by GitHub
21 changed files with 587 additions and 228 deletions
@@ -54,9 +54,17 @@ export function createExtensionInstance(options: {
extension.factory({
source,
config: parsedConfig,
bind: mapValues(extension.output, ref => {
return (value: unknown) => extensionData.set(ref.id, value);
}),
bind: namedOutputs => {
for (const [name, output] of Object.entries(namedOutputs)) {
const ref = extension.output[name];
if (!ref) {
throw new Error(
`Extension instance '${extension.id}' tried to bind unknown output '${name}'`,
);
}
extensionData.set(ref.id, output);
}
},
inputs: mapValues(
extension.inputs,
({ extensionData: pointData }, inputName) => {
@@ -47,10 +47,12 @@ export const CoreRouter = createExtension({
return element;
};
bind.component(() => (
<BrowserRouter>
<Routes />
</BrowserRouter>
));
bind({
component: () => (
<BrowserRouter>
<Routes />
</BrowserRouter>
),
});
},
});
+88 -37
View File
@@ -17,7 +17,9 @@ import { ZodSchema } from 'zod';
import { ZodTypeDef } from 'zod';
// @public (undocumented)
export type AnyExtensionDataMap = Record<string, ExtensionDataRef<any>>;
export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<any, any>;
};
// @public (undocumented)
export interface BackstagePlugin {
@@ -29,12 +31,28 @@ export interface BackstagePlugin {
id: string;
}
// @public (undocumented)
export interface ConfigurableExtensionDataRef<
TData,
TConfig extends {
optional?: true;
} = {},
> extends ExtensionDataRef<TData, TConfig> {
// (undocumented)
optional(): ConfigurableExtensionDataRef<
TData,
TData & {
optional: true;
}
>;
}
// @public (undocumented)
export const coreExtensionData: {
reactComponent: ExtensionDataRef<ComponentType<{}>>;
routePath: ExtensionDataRef<string>;
apiFactory: ExtensionDataRef<AnyApiFactory>;
routeRef: ExtensionDataRef<RouteRef<any>>;
reactComponent: ConfigurableExtensionDataRef<ComponentType<{}>, {}>;
routePath: ConfigurableExtensionDataRef<string, {}>;
apiFactory: ConfigurableExtensionDataRef<AnyApiFactory, {}>;
routeRef: ConfigurableExtensionDataRef<RouteRef<any>, {}>;
};
// @public (undocumented)
@@ -52,11 +70,7 @@ export function createApiExtension<
api: AnyApiRef;
factory: (options: {
config: TConfig;
inputs: {
[pointName in keyof TInputs]: ExtensionDataValue<
TInputs[pointName]['extensionData']
>[];
};
inputs: ExtensionDataInputValues<TInputs>;
}) => AnyApiFactory;
}
| {
@@ -70,20 +84,27 @@ export function createApiExtension<
// @public (undocumented)
export function createExtension<
TData extends AnyExtensionDataMap,
TPoint extends Record<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
}
>,
TConfig = never,
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig>;
>(
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
): Extension<TConfig>;
// @public (undocumented)
export function createExtensionDataRef<TData>(
id: string,
): ConfigurableExtensionDataRef<TData>;
// @public (undocumented)
export interface CreateExtensionOptions<
TData extends AnyExtensionDataMap,
TPoint extends Record<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<
string,
{
extensionData: AnyExtensionDataMap;
@@ -100,20 +121,16 @@ export interface CreateExtensionOptions<
// (undocumented)
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<TData>;
bind: ExtensionDataBind<TOutput>;
config: TConfig;
inputs: {
[pointName in keyof TPoint]: ExtensionDataValue<
TPoint[pointName]['extensionData']
>[];
};
inputs: ExtensionDataInputValues<TInputs>;
}): void;
// (undocumented)
id: string;
// (undocumented)
inputs?: TPoint;
inputs?: TInputs;
// (undocumented)
output: TData;
output: TOutput;
}
// @public
@@ -143,11 +160,7 @@ export function createPageExtension<
routeRef?: RouteRef;
component: (props: {
config: TConfig;
inputs: {
[pointName in keyof TInputs]: ExtensionDataValue<
TInputs[pointName]['extensionData']
>[];
};
inputs: ExtensionDataInputValues<TInputs>;
}) => Promise<JSX.Element>;
},
): Extension<TConfig>;
@@ -204,22 +217,60 @@ export interface ExtensionBoundaryProps {
}
// @public (undocumented)
export type ExtensionDataBind<TData extends AnyExtensionDataMap> = {
[K in keyof TData]: (value: TData[K]['T']) => void;
export type ExtensionDataBind<TMap extends AnyExtensionDataMap> = (
values: {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TMap[DataName]['T'];
} & {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TMap[DataName]['T'];
},
) => void;
// @public (undocumented)
export type ExtensionDataInputValues<
TInputs extends {
[name in string]: {
extensionData: AnyExtensionDataMap;
};
},
> = {
[InputName in keyof TInputs]: Array<
{
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TInputs[InputName]['extensionData'][DataName]['T'];
} & {
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TInputs[InputName]['extensionData'][DataName]['T'];
}
>;
};
// @public (undocumented)
export type ExtensionDataRef<T> = {
export type ExtensionDataRef<
TData,
TConfig extends {
optional?: true;
} = {},
> = {
id: string;
T: T;
T: TData;
config: TConfig;
$$type: 'extension-data';
};
// @public (undocumented)
export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
[K in keyof TData]: TData[K]['T'];
};
// @public (undocumented)
export interface PluginOptions {
// (undocumented)
@@ -38,10 +38,11 @@ describe('createApiExtension', () => {
configSchema: undefined,
inputs: {},
output: {
api: {
api: expect.objectContaining({
$$type: 'extension-data',
id: 'core.api.factory',
},
config: {},
}),
},
factory: expect.any(Function),
});
@@ -71,10 +72,11 @@ describe('createApiExtension', () => {
configSchema: undefined,
inputs: {},
output: {
api: {
api: expect.objectContaining({
$$type: 'extension-data',
id: 'core.api.factory',
},
config: {},
}),
},
factory: expect.any(Function),
});
@@ -15,13 +15,13 @@
*/
import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api';
import { PortableSchema } from '../createSchemaFromZod';
import { PortableSchema } from '../schema';
import {
AnyExtensionDataMap,
coreExtensionData,
ExtensionDataInputValues,
createExtension,
ExtensionDataValue,
} from '../types';
coreExtensionData,
} from '../wiring';
/** @public */
export function createApiExtension<
@@ -33,11 +33,7 @@ export function createApiExtension<
api: AnyApiRef;
factory: (options: {
config: TConfig;
inputs: {
[pointName in keyof TInputs]: ExtensionDataValue<
TInputs[pointName]['extensionData']
>[];
};
inputs: ExtensionDataInputValues<TInputs>;
}) => AnyApiFactory;
}
| {
@@ -63,9 +59,9 @@ export function createApiExtension<
},
factory({ bind, config, inputs }) {
if (typeof factory === 'function') {
bind.api(factory({ config, inputs }));
bind({ api: factory({ config, inputs }) });
} else {
bind.api(factory);
bind({ api: factory });
}
},
});
@@ -15,8 +15,8 @@
*/
import React from 'react';
import { PortableSchema } from '../createSchemaFromZod';
import { coreExtensionData } from '../types';
import { PortableSchema } from '../schema';
import { coreExtensionData } from '../wiring';
import { createPageExtension } from './createPageExtension';
describe('createPageExtension', () => {
@@ -42,6 +42,7 @@ describe('createPageExtension', () => {
output: {
component: expect.anything(),
path: expect.anything(),
routeRef: expect.anything(),
},
factory: expect.any(Function),
});
@@ -73,6 +74,7 @@ describe('createPageExtension', () => {
output: {
component: expect.anything(),
path: expect.anything(),
routeRef: expect.anything(),
},
factory: expect.any(Function),
});
@@ -93,6 +95,7 @@ describe('createPageExtension', () => {
output: {
component: expect.anything(),
path: expect.anything(),
routeRef: expect.anything(),
},
factory: expect.any(Function),
});
@@ -17,14 +17,14 @@
import { RouteRef } from '@backstage/core-plugin-api';
import React from 'react';
import { ExtensionBoundary } from '../components';
import { createSchemaFromZod, PortableSchema } from '../createSchemaFromZod';
import { createSchemaFromZod, PortableSchema } from '../schema';
import {
AnyExtensionDataMap,
coreExtensionData,
createExtension,
Extension,
ExtensionDataValue,
} from '../types';
ExtensionDataInputValues,
} from '../wiring';
/**
* Helper for creating extensions for a routable React page component.
@@ -50,11 +50,7 @@ export function createPageExtension<
routeRef?: RouteRef;
component: (props: {
config: TConfig;
inputs: {
[pointName in keyof TInputs]: ExtensionDataValue<
TInputs[pointName]['extensionData']
>[];
};
inputs: ExtensionDataInputValues<TInputs>;
}) => Promise<JSX.Element>;
},
): Extension<TConfig> {
@@ -72,7 +68,7 @@ export function createPageExtension<
output: {
component: coreExtensionData.reactComponent,
path: coreExtensionData.routePath,
...(options.routeRef && { routeRef: coreExtensionData.routeRef }),
routeRef: coreExtensionData.routeRef.optional(),
},
inputs: options.inputs,
configSchema,
@@ -82,17 +78,18 @@ export function createPageExtension<
.component({ config, inputs })
.then(element => ({ default: () => element })),
);
bind.path(config.path);
bind.component(() => (
<ExtensionBoundary source={source}>
<React.Suspense fallback="...">
<LazyComponent />
</React.Suspense>
</ExtensionBoundary>
));
if (options.routeRef) {
bind.routeRef!(options.routeRef);
}
bind({
path: config.path,
component: () => (
<ExtensionBoundary source={source}>
<React.Suspense fallback="...">
<LazyComponent />
</React.Suspense>
</ExtensionBoundary>
),
routeRef: options.routeRef,
});
},
});
}
+1 -14
View File
@@ -20,21 +20,8 @@
* @packageDocumentation
*/
export {
createSchemaFromZod,
type PortableSchema,
} from './createSchemaFromZod';
export * from './components';
export * from './extensions';
export {
coreExtensionData,
createExtension,
type AnyExtensionDataMap,
type CreateExtensionOptions,
type Extension,
type ExtensionDataBind,
type ExtensionDataRef,
type ExtensionDataValue,
} from './types';
export * from './schema';
export * from './wiring';
export * from './routing';
@@ -17,12 +17,7 @@
import { JsonObject } from '@backstage/types';
import { z, ZodSchema, ZodTypeDef } from 'zod';
import zodToJsonSchema from 'zod-to-json-schema';
/** @public */
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
schema: JsonObject;
};
import { PortableSchema } from './types';
/** @public */
export function createSchemaFromZod<TOutput, TInput>(
@@ -0,0 +1,18 @@
/*
* 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 { createSchemaFromZod } from './createSchemaFromZod';
export { type PortableSchema } from './types';
@@ -0,0 +1,23 @@
/*
* 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 { JsonObject } from '@backstage/types';
/** @public */
export type PortableSchema<TOutput> = {
parse: (input: unknown) => TOutput;
schema: JsonObject;
};
-110
View File
@@ -1,110 +0,0 @@
/*
* 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 { AnyApiFactory } from '@backstage/core-plugin-api';
import { RouteRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { PortableSchema } from './createSchemaFromZod';
import { BackstagePlugin } from './wiring';
/** @public */
export type ExtensionDataRef<T> = {
id: string;
T: T;
$$type: 'extension-data';
};
/** @public */
// TODO: change to options object with ID.
export function createExtensionDataRef<T>(id: string): ExtensionDataRef<T> {
return { id, $$type: 'extension-data' } as ExtensionDataRef<T>;
}
/** @public */
export const coreExtensionData = {
reactComponent: createExtensionDataRef<ComponentType>('core.reactComponent'),
routePath: createExtensionDataRef<string>('core.routing.path'),
apiFactory: createExtensionDataRef<AnyApiFactory>('core.api.factory'),
routeRef: createExtensionDataRef<RouteRef>('core.routing.ref'),
};
/** @public */
export type AnyExtensionDataMap = Record<string, ExtensionDataRef<any>>;
/** @public */
export type ExtensionDataBind<TData extends AnyExtensionDataMap> = {
[K in keyof TData]: (value: TData[K]['T']) => void;
};
/** @public */
export type ExtensionDataValue<TData extends AnyExtensionDataMap> = {
[K in keyof TData]: TData[K]['T'];
};
/** @public */
export interface CreateExtensionOptions<
TData extends AnyExtensionDataMap,
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig,
> {
id: string;
at: string;
disabled?: boolean;
inputs?: TPoint;
output: TData;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<TData>;
config: TConfig;
inputs: {
[pointName in keyof TPoint]: ExtensionDataValue<
TPoint[pointName]['extensionData']
>[];
};
}): void;
}
/** @public */
export interface Extension<TConfig> {
$$type: 'extension';
id: string;
at: string;
disabled: boolean;
inputs: Record<string, { extensionData: AnyExtensionDataMap }>;
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<AnyExtensionDataMap>;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
}): void;
}
/** @public */
export function createExtension<
TData extends AnyExtensionDataMap,
TPoint extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig = never,
>(options: CreateExtensionOptions<TData, TPoint, TConfig>): Extension<TConfig> {
return {
...options,
disabled: options.disabled ?? false,
$$type: 'extension',
inputs: options.inputs ?? {},
};
}
@@ -0,0 +1,27 @@
/*
* 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 { AnyApiFactory, RouteRef } from '@backstage/core-plugin-api';
import { ComponentType } from 'react';
import { createExtensionDataRef } from './createExtensionDataRef';
/** @public */
export const coreExtensionData = {
reactComponent: createExtensionDataRef<ComponentType>('core.reactComponent'),
routePath: createExtensionDataRef<string>('core.routing.path'),
apiFactory: createExtensionDataRef<AnyApiFactory>('core.api.factory'),
routeRef: createExtensionDataRef<RouteRef>('core.routing.ref'),
};
@@ -0,0 +1,156 @@
/*
* 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 { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
const stringData = createExtensionDataRef<string>('string');
function unused(..._any: any[]) {}
describe('createExtension', () => {
it('should create an extension with a simple output', () => {
const extension = createExtension({
id: 'test',
at: 'root',
output: {
foo: stringData,
},
factory({ bind }) {
bind({
foo: 'bar',
});
bind({
// @ts-expect-error
foo: 3,
});
bind({
// @ts-expect-error
bar: 'bar',
});
// @ts-expect-error
bind({});
// @ts-expect-error
bind();
// @ts-expect-error
bind('bar');
},
});
expect(extension.id).toBe('test');
});
it('should create an extension with a some optional output', () => {
const extension = createExtension({
id: 'test',
at: 'root',
output: {
foo: stringData,
bar: stringData.optional(),
},
factory({ bind }) {
bind({
foo: 'bar',
});
bind({
foo: 'bar',
bar: 'baz',
});
bind({
// @ts-expect-error
foo: 3,
});
bind({
foo: 'bar',
// @ts-expect-error
bar: 3,
});
// @ts-expect-error
bind({ bar: 'bar' });
// @ts-expect-error
bind({});
// @ts-expect-error
bind();
// @ts-expect-error
bind('bar');
},
});
expect(extension.id).toBe('test');
});
it('should create an extension with input', () => {
const extension = createExtension({
id: 'test',
at: 'root',
inputs: {
mixed: {
extensionData: {
required: stringData,
optional: stringData.optional(),
},
},
onlyRequired: {
extensionData: {
required: stringData,
},
},
onlyOptional: {
extensionData: {
optional: stringData.optional(),
},
},
},
output: {
foo: stringData,
},
factory({ bind, inputs }) {
const a1: string = inputs.mixed?.[0].required;
// @ts-expect-error
const a2: number = inputs.mixed?.[0].required;
// @ts-expect-error
const a3: any = inputs.mixed?.[0].nonExistent;
unused(a1, a2, a3);
const b1: string | undefined = inputs.mixed?.[0].optional;
// @ts-expect-error
const b2: string = inputs.mixed?.[0].optional;
// @ts-expect-error
const b3: number = inputs.mixed?.[0].optional;
// @ts-expect-error
const b4: number | undefined = inputs.mixed?.[0].optional;
unused(b1, b2, b3, b4);
const c1: string = inputs.onlyRequired?.[0].required;
// @ts-expect-error
const c2: number = inputs.onlyRequired?.[0].required;
unused(c1, c2);
const d1: string | undefined = inputs.onlyOptional?.[0].optional;
// @ts-expect-error
const d2: string = inputs.onlyOptional?.[0].optional;
// @ts-expect-error
const d3: number = inputs.onlyOptional?.[0].optional;
// @ts-expect-error
const d4: number | undefined = inputs.onlyOptional?.[0].optional;
unused(d1, d2, d3, d4);
bind({
foo: 'bar',
});
},
});
expect(extension.id).toBe('test');
});
});
@@ -0,0 +1,101 @@
/*
* 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 { PortableSchema } from '../schema';
import { BackstagePlugin } from './createPlugin';
import { AnyExtensionDataMap, Extension } from './types';
/** @public */
export type ExtensionDataInputValues<
TInputs extends { [name in string]: { extensionData: AnyExtensionDataMap } },
> = {
[InputName in keyof TInputs]: Array<
{
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TInputs[InputName]['extensionData'][DataName]['T'];
} & {
[DataName in keyof TInputs[InputName]['extensionData'] as TInputs[InputName]['extensionData'][DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TInputs[InputName]['extensionData'][DataName]['T'];
}
>;
};
/** @public */
export type ExtensionDataBind<TMap extends AnyExtensionDataMap> = (
values: {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? never
: DataName]: TMap[DataName]['T'];
} & {
[DataName in keyof TMap as TMap[DataName]['config'] extends {
optional: true;
}
? DataName
: never]?: TMap[DataName]['T'];
},
) => void;
/** @public */
export interface CreateExtensionOptions<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig,
> {
id: string;
at: string;
disabled?: boolean;
inputs?: TInputs;
output: TOutput;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<TOutput>;
config: TConfig;
inputs: ExtensionDataInputValues<TInputs>;
}): void;
}
/** @public */
export function createExtension<
TOutput extends AnyExtensionDataMap,
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
TConfig = never,
>(
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
): Extension<TConfig> {
return {
...options,
disabled: options.disabled ?? false,
$$type: 'extension',
inputs: options.inputs ?? {},
factory({ bind, config, inputs }) {
// TODO: Simplify this, but TS wouldn't infer the input type for some reason
return options.factory({
bind,
config,
inputs: inputs as ExtensionDataInputValues<TInputs>,
});
},
};
}
@@ -0,0 +1,49 @@
/*
* 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.
*/
/** @public */
export type ExtensionDataRef<
TData,
TConfig extends { optional?: true } = {},
> = {
id: string;
T: TData;
config: TConfig;
$$type: 'extension-data';
};
/** @public */
export interface ConfigurableExtensionDataRef<
TData,
TConfig extends { optional?: true } = {},
> extends ExtensionDataRef<TData, TConfig> {
optional(): ConfigurableExtensionDataRef<TData, TData & { optional: true }>;
}
// TODO: change to options object with ID.
/** @public */
export function createExtensionDataRef<TData>(
id: string,
): ConfigurableExtensionDataRef<TData> {
return {
id,
$$type: 'extension-data',
config: {},
optional() {
return { ...this, config: { ...this.config, optional: true } };
},
} as ConfigurableExtensionDataRef<TData>;
}
@@ -17,14 +17,12 @@
import React from 'react';
import { createApp } from '@backstage/frontend-app-api';
import { render, screen } from '@testing-library/react';
import { createSchemaFromZod } from '../createSchemaFromZod';
import {
createExtension,
coreExtensionData,
createExtensionDataRef,
} from '../types';
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
import { createPlugin, BackstagePlugin } from './createPlugin';
import { JsonObject } from '@backstage/types';
import { createExtension } from './createExtension';
import { createExtensionDataRef } from './createExtensionDataRef';
import { coreExtensionData } from './coreExtensionData';
const nameExtensionDataRef = createExtensionDataRef<string>('name');
@@ -35,7 +33,7 @@ const TechRadarPage = createExtension({
name: nameExtensionDataRef,
},
factory({ bind }) {
bind.name('TechRadar');
bind({ name: 'TechRadar' });
},
});
@@ -49,7 +47,7 @@ const CatalogPage = createExtension({
z.object({ name: z.string().default('Catalog') }),
),
factory({ bind, config }) {
bind.name(config.name);
bind({ name: config.name });
},
});
@@ -63,7 +61,7 @@ const TechDocsAddon = createExtension({
z.object({ name: z.string().default('TechDocsAddon') }),
),
factory({ bind, config }) {
bind.name(config.name);
bind({ name: config.name });
},
});
@@ -81,7 +79,7 @@ const TechDocsPage = createExtension({
name: nameExtensionDataRef,
},
factory({ bind, inputs }) {
bind.name(`TechDocs-${inputs.addons.map(n => n.name).join('-')}`);
bind({ name: `TechDocs-${inputs.addons.map(n => n.name).join('-')}` });
},
});
@@ -99,11 +97,12 @@ const outputExtension = createExtension({
component: coreExtensionData.reactComponent,
},
factory({ bind, inputs }) {
bind.component(() =>
React.createElement('span', {}, [
`Names: ${inputs.names.map(n => n.name).join(', ')}`,
]),
);
bind({
component: () =>
React.createElement('span', {}, [
`Names: ${inputs.names.map(n => n.name).join(', ')}`,
]),
});
},
});
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Extension } from '../types';
import { Extension } from './types';
/** @public */
export interface PluginOptions {
@@ -14,8 +14,21 @@
* limitations under the License.
*/
export { coreExtensionData } from './coreExtensionData';
export {
createExtension,
type CreateExtensionOptions,
type ExtensionDataBind,
type ExtensionDataInputValues,
} from './createExtension';
export {
createExtensionDataRef,
type ExtensionDataRef,
type ConfigurableExtensionDataRef,
} from './createExtensionDataRef';
export {
createPlugin,
type BackstagePlugin,
type PluginOptions,
} from './createPlugin';
export type { AnyExtensionDataMap, Extension } from './types';
@@ -0,0 +1,42 @@
/*
* 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 { PortableSchema } from '../schema';
import { ExtensionDataBind } from './createExtension';
import { ExtensionDataRef } from './createExtensionDataRef';
import { BackstagePlugin } from './createPlugin';
/** @public */
export type AnyExtensionDataMap = {
[name in string]: ExtensionDataRef<any, any>;
};
/** @public */
export interface Extension<TConfig> {
$$type: 'extension';
id: string;
at: string;
disabled: boolean;
inputs: Record<string, { extensionData: AnyExtensionDataMap }>;
output: AnyExtensionDataMap;
configSchema?: PortableSchema<TConfig>;
factory(options: {
source?: BackstagePlugin;
bind: ExtensionDataBind<AnyExtensionDataMap>;
config: TConfig;
inputs: Record<string, Array<Record<string, unknown>>>;
}): void;
}