Merge pull request #19937 from backstage/rugvip/input
frontend-plugin-api: add createExtensionInput + validation
This commit is contained in:
@@ -17,17 +17,16 @@
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
export const Core = createExtension({
|
||||
id: 'core',
|
||||
at: 'root',
|
||||
inputs: {
|
||||
apis: {
|
||||
extensionData: {
|
||||
api: coreExtensionData.apiFactory,
|
||||
},
|
||||
},
|
||||
apis: createExtensionInput({
|
||||
api: coreExtensionData.apiFactory,
|
||||
}),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
|
||||
@@ -18,6 +18,7 @@ import React from 'react';
|
||||
import {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { SidebarPage } from '@backstage/core-components';
|
||||
|
||||
@@ -25,39 +26,28 @@ export const CoreLayout = createExtension({
|
||||
id: 'core.layout',
|
||||
at: 'root',
|
||||
inputs: {
|
||||
nav: {
|
||||
extensionData: {
|
||||
nav: createExtensionInput(
|
||||
{
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
},
|
||||
content: {
|
||||
extensionData: {
|
||||
{ singleton: true },
|
||||
),
|
||||
content: createExtensionInput(
|
||||
{
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
// TODO: Support this as part of the core system
|
||||
if (inputs.nav.length !== 1) {
|
||||
throw Error(
|
||||
`Extension 'core.layout' did not receive exactly one 'nav' input, got ${inputs.nav.length}`,
|
||||
);
|
||||
}
|
||||
if (inputs.content.length !== 1) {
|
||||
throw Error(
|
||||
`Extension 'core.layout' did not receive exactly one 'content' input, got ${inputs.content.length}`,
|
||||
);
|
||||
}
|
||||
|
||||
bind({
|
||||
// TODO: set base path using the logic from AppRouter
|
||||
element: (
|
||||
<SidebarPage>
|
||||
{inputs.nav[0].element}
|
||||
{inputs.content[0].element}
|
||||
{inputs.nav.element}
|
||||
{inputs.content.element}
|
||||
</SidebarPage>
|
||||
),
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import React from 'react';
|
||||
import {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { makeStyles } from '@material-ui/core';
|
||||
import {
|
||||
@@ -66,11 +67,9 @@ export const CoreNav = createExtension({
|
||||
id: 'core.nav',
|
||||
at: 'core.layout/nav',
|
||||
inputs: {
|
||||
targets: {
|
||||
extensionData: {
|
||||
path: coreExtensionData.navTarget,
|
||||
},
|
||||
},
|
||||
items: createExtensionInput({
|
||||
path: coreExtensionData.navTarget,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
|
||||
@@ -18,6 +18,7 @@ import React from 'react';
|
||||
import {
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { useRoutes } from 'react-router-dom';
|
||||
|
||||
@@ -25,13 +26,11 @@ export const CoreRoutes = createExtension({
|
||||
id: 'core.routes',
|
||||
at: 'core.layout/content',
|
||||
inputs: {
|
||||
routes: {
|
||||
extensionData: {
|
||||
path: coreExtensionData.routePath,
|
||||
ref: coreExtensionData.routeRef,
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
},
|
||||
routes: createExtensionInput({
|
||||
path: coreExtensionData.routePath,
|
||||
ref: coreExtensionData.routeRef.optional(),
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
|
||||
@@ -0,0 +1,370 @@
|
||||
/*
|
||||
* 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,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createExtensionInstance } from './createExtensionInstance';
|
||||
|
||||
const testDataRef = createExtensionDataRef<string>('test');
|
||||
const otherDataRef = createExtensionDataRef<number>('other');
|
||||
const inputMirrorDataRef = createExtensionDataRef<unknown>('mirror');
|
||||
|
||||
const simpleExtension = createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
output: {
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
output: z.string().default('test'),
|
||||
other: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
factory({ bind, config }) {
|
||||
bind({ test: config.output, other: config.other });
|
||||
},
|
||||
});
|
||||
|
||||
describe('createExtensionInstance', () => {
|
||||
it('should create a simple extension instance', () => {
|
||||
const attachments = new Map();
|
||||
const instance = createExtensionInstance({
|
||||
attachments,
|
||||
config: undefined,
|
||||
extension: simpleExtension,
|
||||
});
|
||||
|
||||
expect(instance.id).toBe('core.test');
|
||||
expect(instance.attachments).toBe(attachments);
|
||||
expect(instance.getData(testDataRef)).toEqual('test');
|
||||
});
|
||||
|
||||
it('should create an extension with different kind of inputs', () => {
|
||||
const attachments = new Map([
|
||||
[
|
||||
'optionalSingletonPresent',
|
||||
[
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'optionalSingletonPresent' },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
],
|
||||
],
|
||||
[
|
||||
'singleton',
|
||||
[
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'singleton', other: 2 },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
],
|
||||
],
|
||||
[
|
||||
'many',
|
||||
[
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'many1' },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'many2', other: 3 },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
],
|
||||
],
|
||||
]);
|
||||
const instance = createExtensionInstance({
|
||||
attachments,
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
inputs: {
|
||||
optionalSingletonPresent: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
optionalSingletonMissing: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
many: createExtensionInput({
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ bind, inputs }) {
|
||||
bind({ inputMirror: inputs });
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
expect(instance.id).toBe('core.test');
|
||||
expect(instance.attachments).toBe(attachments);
|
||||
expect(instance.getData(inputMirrorDataRef)).toEqual({
|
||||
optionalSingletonPresent: { test: 'optionalSingletonPresent' },
|
||||
singleton: { test: 'singleton', other: 2 },
|
||||
many: [{ test: 'many1' }, { test: 'many2', other: 3 }],
|
||||
});
|
||||
});
|
||||
|
||||
it('should refuse to create an extension with invalid config', () => {
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { other: 'not-a-number' },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
).toThrow(
|
||||
"Invalid configuration for extension 'core.test'; caused by Error: Expected number, received string at 'other'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should forward extension factory errors', () => {
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { other: 'not-a-number' },
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
output: {},
|
||||
factory() {
|
||||
const error = new Error('NOPE');
|
||||
error.name = 'NopeError';
|
||||
throw error;
|
||||
},
|
||||
}),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test'; caused by NopeError: NOPE",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with duplicate output', () => {
|
||||
const attachments = new Map();
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments,
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
output: {
|
||||
test1: testDataRef,
|
||||
test2: testDataRef,
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({ test1: 'test', test2: 'test2' });
|
||||
},
|
||||
}),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', duplicate extension data 'test' received via output 'test2'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with disconnected output data', () => {
|
||||
const attachments = new Map();
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments,
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
output: {
|
||||
test: testDataRef,
|
||||
},
|
||||
factory({ bind }) {
|
||||
bind({ nonexistent: 'test' } as any);
|
||||
},
|
||||
}),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', unknown output provided via 'nonexistent'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with missing required input', () => {
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', input 'singleton' is required but was not received",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs for required singleton', () => {
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
[
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'many1' },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'many2' },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
],
|
||||
],
|
||||
]),
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', expected exactly one 'singleton' input but received multiple: 'core.test', 'core.test'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs for optional singleton', () => {
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
[
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'many1' },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: { output: 'many2' },
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
],
|
||||
],
|
||||
]),
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', expected at most one 'singleton' input but received multiple: 'core.test', 'core.test'",
|
||||
);
|
||||
});
|
||||
|
||||
it('should refuse to create an instance with multiple inputs that did not provide required data', () => {
|
||||
expect(() =>
|
||||
createExtensionInstance({
|
||||
attachments: new Map([
|
||||
[
|
||||
'singleton',
|
||||
[
|
||||
createExtensionInstance({
|
||||
attachments: new Map(),
|
||||
config: undefined,
|
||||
extension: simpleExtension,
|
||||
}),
|
||||
],
|
||||
],
|
||||
]),
|
||||
config: undefined,
|
||||
extension: createExtension({
|
||||
id: 'core.test',
|
||||
at: 'ignored',
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
other: otherDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory() {},
|
||||
}),
|
||||
}),
|
||||
).toThrow(
|
||||
"Failed to instantiate extension 'core.test', input 'singleton' did not receive required extension data 'other' from extension 'core.test'",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -15,6 +15,8 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AnyExtensionDataMap,
|
||||
AnyExtensionInputMap,
|
||||
BackstagePlugin,
|
||||
Extension,
|
||||
ExtensionDataRef,
|
||||
@@ -36,6 +38,56 @@ export interface ExtensionInstance {
|
||||
readonly attachments: Map<string, ExtensionInstance[]>;
|
||||
}
|
||||
|
||||
function resolveInputData(
|
||||
dataMap: AnyExtensionDataMap,
|
||||
attachment: ExtensionInstance,
|
||||
inputName: string,
|
||||
) {
|
||||
return mapValues(dataMap, ref => {
|
||||
const value = attachment.getData(ref);
|
||||
if (value === undefined && !ref.config.optional) {
|
||||
throw new Error(
|
||||
`input '${inputName}' did not receive required extension data '${ref.id}' from extension '${attachment.id}'`,
|
||||
);
|
||||
}
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
function resolveInputs(
|
||||
inputMap: AnyExtensionInputMap,
|
||||
attachments: Map<string, ExtensionInstance[]>,
|
||||
) {
|
||||
return mapValues(inputMap, (input, inputName) => {
|
||||
const attachedInstances = attachments.get(inputName) ?? [];
|
||||
if (input.config.singleton) {
|
||||
if (attachedInstances.length > 1) {
|
||||
throw Error(
|
||||
`expected ${
|
||||
input.config.optional ? 'at most' : 'exactly'
|
||||
} one '${inputName}' input but received multiple: '${attachedInstances
|
||||
.map(e => e.id)
|
||||
.join("', '")}'`,
|
||||
);
|
||||
} else if (attachedInstances.length === 0) {
|
||||
if (input.config.optional) {
|
||||
return undefined;
|
||||
}
|
||||
throw Error(`input '${inputName}' is required but was not received`);
|
||||
}
|
||||
return resolveInputData(
|
||||
input.extensionData,
|
||||
attachedInstances[0],
|
||||
inputName,
|
||||
);
|
||||
}
|
||||
|
||||
return attachedInstances.map(attachment =>
|
||||
resolveInputData(input.extensionData, attachment, inputName),
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/** @internal */
|
||||
export function createExtensionInstance(options: {
|
||||
extension: Extension<unknown>;
|
||||
@@ -51,7 +103,7 @@ export function createExtensionInstance(options: {
|
||||
parsedConfig = extension.configSchema?.parse(config ?? {});
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Invalid configuration for extension instance '${extension.id}', ${e}`,
|
||||
`Invalid configuration for extension '${extension.id}'; caused by ${e}`,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -63,26 +115,23 @@ export function createExtensionInstance(options: {
|
||||
for (const [name, output] of Object.entries(namedOutputs)) {
|
||||
const ref = extension.output[name];
|
||||
if (!ref) {
|
||||
throw new Error(`unknown output provided via '${name}'`);
|
||||
}
|
||||
if (extensionData.has(ref.id)) {
|
||||
throw new Error(
|
||||
`Extension instance '${extension.id}' tried to bind unknown output '${name}'`,
|
||||
`duplicate extension data '${ref.id}' received via output '${name}'`,
|
||||
);
|
||||
}
|
||||
extensionData.set(ref.id, output);
|
||||
}
|
||||
},
|
||||
inputs: mapValues(
|
||||
extension.inputs,
|
||||
({ extensionData: pointData }, inputName) => {
|
||||
// TODO: validation
|
||||
return (attachments.get(inputName) ?? []).map(attachment =>
|
||||
mapValues(pointData, ref => attachment.getData(ref)),
|
||||
);
|
||||
},
|
||||
),
|
||||
inputs: resolveInputs(extension.inputs, attachments),
|
||||
});
|
||||
} catch (e) {
|
||||
throw new Error(
|
||||
`Failed to instantiate extension instance '${extension.id}', ${e}`,
|
||||
`Failed to instantiate extension '${extension.id}'${
|
||||
e.name === 'Error' ? `, ${e.message}` : `; caused by ${e}`
|
||||
}`,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -19,7 +19,23 @@ import { ZodTypeDef } from 'zod';
|
||||
|
||||
// @public (undocumented)
|
||||
export type AnyExtensionDataMap = {
|
||||
[name in string]: ExtensionDataRef<any, any>;
|
||||
[name in string]: ExtensionDataRef<
|
||||
unknown,
|
||||
{
|
||||
optional?: true;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type AnyExtensionInputMap = {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataMap,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
@@ -60,19 +76,14 @@ export const coreExtensionData: {
|
||||
// @public (undocumented)
|
||||
export function createApiExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends Record<
|
||||
string,
|
||||
{
|
||||
extensionData: AnyExtensionDataMap;
|
||||
}
|
||||
>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
api: AnyApiRef;
|
||||
factory: (options: {
|
||||
config: TConfig;
|
||||
inputs: ExtensionDataInputValues<TInputs>;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => AnyApiFactory;
|
||||
}
|
||||
| {
|
||||
@@ -87,12 +98,7 @@ export function createApiExtension<
|
||||
// @public (undocumented)
|
||||
export function createExtension<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends Record<
|
||||
string,
|
||||
{
|
||||
extensionData: AnyExtensionDataMap;
|
||||
}
|
||||
>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig = never,
|
||||
>(
|
||||
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
|
||||
@@ -103,15 +109,28 @@ export function createExtensionDataRef<TData>(
|
||||
id: string,
|
||||
): ConfigurableExtensionDataRef<TData>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionInput<
|
||||
TExtensionData extends AnyExtensionDataMap,
|
||||
TConfig extends {
|
||||
singleton?: boolean;
|
||||
optional?: boolean;
|
||||
},
|
||||
>(
|
||||
extensionData: TExtensionData,
|
||||
config?: TConfig,
|
||||
): ExtensionInput<
|
||||
TExtensionData,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export interface CreateExtensionOptions<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends Record<
|
||||
string,
|
||||
{
|
||||
extensionData: AnyExtensionDataMap;
|
||||
}
|
||||
>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
> {
|
||||
// (undocumented)
|
||||
@@ -123,9 +142,9 @@ export interface CreateExtensionOptions<
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind: ExtensionDataBind<TOutput>;
|
||||
bind(values: Expand<ExtensionDataValues<TOutput>>): void;
|
||||
config: TConfig;
|
||||
inputs: ExtensionDataInputValues<TInputs>;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}): void;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
@@ -150,12 +169,7 @@ export function createPageExtension<
|
||||
TConfig extends {
|
||||
path: string;
|
||||
},
|
||||
TInputs extends Record<
|
||||
string,
|
||||
{
|
||||
extensionData: AnyExtensionDataMap;
|
||||
}
|
||||
>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
@@ -172,7 +186,7 @@ export function createPageExtension<
|
||||
routeRef?: RouteRef;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: ExtensionDataInputValues<TInputs>;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
},
|
||||
): Extension<TConfig>;
|
||||
@@ -198,19 +212,17 @@ export interface Extension<TConfig> {
|
||||
// (undocumented)
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind: ExtensionDataBind<AnyExtensionDataMap>;
|
||||
bind(values: ExtensionInputValues<any>): void;
|
||||
config: TConfig;
|
||||
inputs: Record<string, Array<Record<string, unknown>>>;
|
||||
inputs: Record<
|
||||
string,
|
||||
undefined | Record<string, unknown> | Array<Record<string, unknown>>
|
||||
>;
|
||||
}): void;
|
||||
// (undocumented)
|
||||
id: string;
|
||||
// (undocumented)
|
||||
inputs: Record<
|
||||
string,
|
||||
{
|
||||
extensionData: AnyExtensionDataMap;
|
||||
}
|
||||
>;
|
||||
inputs: AnyExtensionInputMap;
|
||||
// (undocumented)
|
||||
output: AnyExtensionDataMap;
|
||||
}
|
||||
@@ -228,48 +240,6 @@ export interface ExtensionBoundaryProps {
|
||||
source?: BackstagePlugin;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
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<
|
||||
TData,
|
||||
@@ -283,6 +253,52 @@ export type ExtensionDataRef<
|
||||
$$type: '@backstage/ExtensionDataRef';
|
||||
};
|
||||
|
||||
// @public
|
||||
export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
|
||||
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
|
||||
optional: true;
|
||||
}
|
||||
? never
|
||||
: DataName]: TExtensionData[DataName]['T'];
|
||||
} & {
|
||||
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
|
||||
optional: true;
|
||||
}
|
||||
? DataName
|
||||
: never]?: TExtensionData[DataName]['T'];
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export interface ExtensionInput<
|
||||
TExtensionData extends AnyExtensionDataMap,
|
||||
TConfig extends {
|
||||
singleton: boolean;
|
||||
optional: boolean;
|
||||
},
|
||||
> {
|
||||
// (undocumented)
|
||||
$$type: '@backstage/ExtensionInput';
|
||||
// (undocumented)
|
||||
config: TConfig;
|
||||
// (undocumented)
|
||||
extensionData: TExtensionData;
|
||||
}
|
||||
|
||||
// @public
|
||||
export type ExtensionInputValues<
|
||||
TInputs extends {
|
||||
[name in string]: ExtensionInput<any, any>;
|
||||
},
|
||||
> = {
|
||||
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
|
||||
? Array<Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>>
|
||||
: false extends TInputs[InputName]['config']['optional']
|
||||
? Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>
|
||||
: Expand<
|
||||
ExtensionDataValues<TInputs[InputName]['extensionData']> | undefined
|
||||
>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type NavTarget = {
|
||||
title: string;
|
||||
|
||||
@@ -17,23 +17,23 @@
|
||||
import { AnyApiFactory, AnyApiRef } from '@backstage/core-plugin-api';
|
||||
import { PortableSchema } from '../schema';
|
||||
import {
|
||||
AnyExtensionDataMap,
|
||||
ExtensionDataInputValues,
|
||||
ExtensionInputValues,
|
||||
createExtension,
|
||||
coreExtensionData,
|
||||
} from '../wiring';
|
||||
import { AnyExtensionInputMap, Expand } from '../wiring/createExtension';
|
||||
|
||||
/** @public */
|
||||
export function createApiExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
api: AnyApiRef;
|
||||
factory: (options: {
|
||||
config: TConfig;
|
||||
inputs: ExtensionDataInputValues<TInputs>;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => AnyApiFactory;
|
||||
}
|
||||
| {
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import React from 'react';
|
||||
import { PortableSchema } from '../schema';
|
||||
import { coreExtensionData } from '../wiring';
|
||||
import { coreExtensionData, createExtensionInput } from '../wiring';
|
||||
import { createPageExtension } from './createPageExtension';
|
||||
|
||||
describe('createPageExtension', () => {
|
||||
@@ -54,9 +54,9 @@ describe('createPageExtension', () => {
|
||||
disabled: true,
|
||||
configSchema,
|
||||
inputs: {
|
||||
first: {
|
||||
extensionData: { element: coreExtensionData.reactElement },
|
||||
},
|
||||
first: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
@@ -67,9 +67,9 @@ describe('createPageExtension', () => {
|
||||
configSchema: expect.anything(),
|
||||
disabled: true,
|
||||
inputs: {
|
||||
first: {
|
||||
extensionData: { element: coreExtensionData.reactElement },
|
||||
},
|
||||
first: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: expect.anything(),
|
||||
|
||||
@@ -19,12 +19,12 @@ import React from 'react';
|
||||
import { ExtensionBoundary } from '../components';
|
||||
import { createSchemaFromZod, PortableSchema } from '../schema';
|
||||
import {
|
||||
AnyExtensionDataMap,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
Extension,
|
||||
ExtensionDataInputValues,
|
||||
ExtensionInputValues,
|
||||
} from '../wiring';
|
||||
import { AnyExtensionInputMap, Expand } from '../wiring/createExtension';
|
||||
|
||||
/**
|
||||
* Helper for creating extensions for a routable React page component.
|
||||
@@ -33,7 +33,7 @@ import {
|
||||
*/
|
||||
export function createPageExtension<
|
||||
TConfig extends { path: string },
|
||||
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
@@ -50,7 +50,7 @@ export function createPageExtension<
|
||||
routeRef?: RouteRef;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: ExtensionDataInputValues<TInputs>;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
},
|
||||
): Extension<TConfig> {
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
import { createExtension } from './createExtension';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import { createExtensionInput } from './createExtensionInput';
|
||||
|
||||
const stringData = createExtensionDataRef<string>('string');
|
||||
|
||||
@@ -95,22 +96,16 @@ describe('createExtension', () => {
|
||||
id: 'test',
|
||||
at: 'root',
|
||||
inputs: {
|
||||
mixed: {
|
||||
extensionData: {
|
||||
required: stringData,
|
||||
optional: stringData.optional(),
|
||||
},
|
||||
},
|
||||
onlyRequired: {
|
||||
extensionData: {
|
||||
required: stringData,
|
||||
},
|
||||
},
|
||||
onlyOptional: {
|
||||
extensionData: {
|
||||
optional: stringData.optional(),
|
||||
},
|
||||
},
|
||||
mixed: createExtensionInput({
|
||||
required: stringData,
|
||||
optional: stringData.optional(),
|
||||
}),
|
||||
onlyRequired: createExtensionInput({
|
||||
required: stringData,
|
||||
}),
|
||||
onlyOptional: createExtensionInput({
|
||||
optional: stringData.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
foo: stringData,
|
||||
|
||||
@@ -15,51 +15,68 @@
|
||||
*/
|
||||
|
||||
import { PortableSchema } from '../schema';
|
||||
import { ExtensionDataRef } from './createExtensionDataRef';
|
||||
import { ExtensionInput } from './createExtensionInput';
|
||||
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'];
|
||||
}
|
||||
>;
|
||||
export type AnyExtensionDataMap = {
|
||||
[name in string]: ExtensionDataRef<unknown, { optional?: true }>;
|
||||
};
|
||||
|
||||
/** @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;
|
||||
export type AnyExtensionInputMap = {
|
||||
[inputName in string]: ExtensionInput<
|
||||
AnyExtensionDataMap,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
};
|
||||
|
||||
// TODO(Rugvip): This might be a quite useful utility type, maybe add to @backstage/types?
|
||||
/**
|
||||
* Utility type to expand type aliases into their equivalent type.
|
||||
* @ignore
|
||||
*/
|
||||
export type Expand<T> = T extends infer O ? { [K in keyof O]: O[K] } : never;
|
||||
|
||||
/**
|
||||
* Converts an extension data map into the matching concrete data values type.
|
||||
* @public
|
||||
*/
|
||||
export type ExtensionDataValues<TExtensionData extends AnyExtensionDataMap> = {
|
||||
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
|
||||
optional: true;
|
||||
}
|
||||
? never
|
||||
: DataName]: TExtensionData[DataName]['T'];
|
||||
} & {
|
||||
[DataName in keyof TExtensionData as TExtensionData[DataName]['config'] extends {
|
||||
optional: true;
|
||||
}
|
||||
? DataName
|
||||
: never]?: TExtensionData[DataName]['T'];
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an extension input map into the matching concrete input values type.
|
||||
* @public
|
||||
*/
|
||||
export type ExtensionInputValues<
|
||||
TInputs extends { [name in string]: ExtensionInput<any, any> },
|
||||
> = {
|
||||
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
|
||||
? Array<Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>>
|
||||
: false extends TInputs[InputName]['config']['optional']
|
||||
? Expand<ExtensionDataValues<TInputs[InputName]['extensionData']>>
|
||||
: Expand<
|
||||
ExtensionDataValues<TInputs[InputName]['extensionData']> | undefined
|
||||
>;
|
||||
};
|
||||
|
||||
/** @public */
|
||||
export interface CreateExtensionOptions<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
> {
|
||||
id: string;
|
||||
@@ -70,16 +87,36 @@ export interface CreateExtensionOptions<
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind: ExtensionDataBind<TOutput>;
|
||||
bind(values: Expand<ExtensionDataValues<TOutput>>): void;
|
||||
config: TConfig;
|
||||
inputs: ExtensionDataInputValues<TInputs>;
|
||||
inputs: Expand<ExtensionInputValues<TInputs>>;
|
||||
}): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export interface Extension<TConfig> {
|
||||
$$type: '@backstage/Extension';
|
||||
id: string;
|
||||
at: string;
|
||||
disabled: boolean;
|
||||
inputs: AnyExtensionInputMap;
|
||||
output: AnyExtensionDataMap;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
factory(options: {
|
||||
source?: BackstagePlugin;
|
||||
bind(values: ExtensionInputValues<any>): void;
|
||||
config: TConfig;
|
||||
inputs: Record<
|
||||
string,
|
||||
undefined | Record<string, unknown> | Array<Record<string, unknown>>
|
||||
>;
|
||||
}): void;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createExtension<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends Record<string, { extensionData: AnyExtensionDataMap }>,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig = never,
|
||||
>(
|
||||
options: CreateExtensionOptions<TOutput, TInputs, TConfig>,
|
||||
@@ -94,7 +131,7 @@ export function createExtension<
|
||||
return options.factory({
|
||||
bind,
|
||||
config,
|
||||
inputs: inputs as ExtensionDataInputValues<TInputs>,
|
||||
inputs: inputs as Expand<ExtensionInputValues<TInputs>>,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* 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 { AnyExtensionDataMap } from './createExtension';
|
||||
|
||||
/** @public */
|
||||
export interface ExtensionInput<
|
||||
TExtensionData extends AnyExtensionDataMap,
|
||||
TConfig extends { singleton: boolean; optional: boolean },
|
||||
> {
|
||||
$$type: '@backstage/ExtensionInput';
|
||||
extensionData: TExtensionData;
|
||||
config: TConfig;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export function createExtensionInput<
|
||||
TExtensionData extends AnyExtensionDataMap,
|
||||
TConfig extends { singleton?: boolean; optional?: boolean },
|
||||
>(
|
||||
extensionData: TExtensionData,
|
||||
config?: TConfig,
|
||||
): ExtensionInput<
|
||||
TExtensionData,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
> {
|
||||
return {
|
||||
$$type: '@backstage/ExtensionInput',
|
||||
extensionData,
|
||||
config: {
|
||||
singleton: Boolean(config?.singleton) as TConfig['singleton'] extends true
|
||||
? true
|
||||
: false,
|
||||
optional: Boolean(config?.optional) as TConfig['optional'] extends true
|
||||
? true
|
||||
: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import { createExtension } from './createExtension';
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import { coreExtensionData } from './coreExtensionData';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { createExtensionInput } from './createExtensionInput';
|
||||
|
||||
const nameExtensionDataRef = createExtensionDataRef<string>('name');
|
||||
|
||||
@@ -70,11 +71,9 @@ const TechDocsPage = createExtension({
|
||||
id: 'plugin.techdocs.page',
|
||||
at: 'test.output/names',
|
||||
inputs: {
|
||||
addons: {
|
||||
extensionData: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
},
|
||||
addons: createExtensionInput({
|
||||
name: nameExtensionDataRef,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
@@ -88,11 +87,9 @@ const outputExtension = createExtension({
|
||||
id: 'test.output',
|
||||
at: 'root',
|
||||
inputs: {
|
||||
names: {
|
||||
extensionData: {
|
||||
name: nameExtensionDataRef,
|
||||
},
|
||||
},
|
||||
names: createExtensionInput({
|
||||
name: nameExtensionDataRef,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Extension } from './types';
|
||||
import { Extension } from './createExtension';
|
||||
|
||||
/** @public */
|
||||
export interface PluginOptions {
|
||||
|
||||
@@ -17,10 +17,17 @@
|
||||
export { coreExtensionData, type NavTarget } from './coreExtensionData';
|
||||
export {
|
||||
createExtension,
|
||||
type Extension,
|
||||
type CreateExtensionOptions,
|
||||
type ExtensionDataBind,
|
||||
type ExtensionDataInputValues,
|
||||
type ExtensionDataValues,
|
||||
type ExtensionInputValues,
|
||||
type AnyExtensionInputMap,
|
||||
type AnyExtensionDataMap,
|
||||
} from './createExtension';
|
||||
export {
|
||||
createExtensionInput,
|
||||
type ExtensionInput,
|
||||
} from './createExtensionInput';
|
||||
export {
|
||||
createExtensionDataRef,
|
||||
type ExtensionDataRef,
|
||||
@@ -31,4 +38,3 @@ export {
|
||||
type BackstagePlugin,
|
||||
type PluginOptions,
|
||||
} from './createPlugin';
|
||||
export type { AnyExtensionDataMap, Extension } from './types';
|
||||
|
||||
@@ -1,42 +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 { 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: '@backstage/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;
|
||||
}
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
createApiExtension,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
createNavItemExtension,
|
||||
createPageExtension,
|
||||
createPlugin,
|
||||
@@ -64,11 +65,9 @@ const endpointDataRef = createExtensionDataRef<GraphQLEndpoint>(
|
||||
export const graphiqlBrowseApi = createApiExtension({
|
||||
api: graphQlBrowseApiRef, // apis.plugin.graphiql.browse
|
||||
inputs: {
|
||||
endpoints: {
|
||||
extensionData: {
|
||||
endpoint: endpointDataRef,
|
||||
},
|
||||
},
|
||||
endpoints: createExtensionInput({
|
||||
endpoint: endpointDataRef,
|
||||
}),
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return createApiFactory(
|
||||
|
||||
Reference in New Issue
Block a user