Merge pull request #19785 from backstage/mob/optional-data
frontend-plugin-api: optional extension data
This commit is contained in:
@@ -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>
|
||||
),
|
||||
});
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
});
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
+1
-6
@@ -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;
|
||||
};
|
||||
@@ -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;
|
||||
}
|
||||
Reference in New Issue
Block a user