Merge pull request #26076 from backstage/rugvip/nold
frontend-plugin-api: remove support for v1 extensions
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-plugin-api': minor
|
||||
---
|
||||
|
||||
**BREAKING**: Removed support for "v1" extensions. This means that it is no longer possible to declare inputs and outputs as objects when using `createExtension`. In addition, all extension creators except for `createComponentExtension` have been removed, use the equivalent blueprint instead. See the [1.30 migration documentation](https://backstage.io/docs/frontend-system/architecture/migrations/#130) for more information on this change.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-test-utils': minor
|
||||
---
|
||||
|
||||
Removed support for testing "v1" extensions, where outputs are defined as an object rather than an array.
|
||||
@@ -0,0 +1,7 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-react': patch
|
||||
'@backstage/plugin-search-react': patch
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
The `/alpha` export no longer export extension creators for the new frontend system, existing usage should be switched to use the equivalent extension blueprint instead. For more information see the [new frontend system 1.30 migration documentation](https://backstage.io/docs/frontend-system/architecture/migrations#130).
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/frontend-app-api': patch
|
||||
---
|
||||
|
||||
Internal refactor following removal of v1 extension support. The app implementation itself still supports v1 extensions at runtime.
|
||||
@@ -29,12 +29,8 @@ const authRedirectExtension = createExtension({
|
||||
namespace: 'app',
|
||||
name: 'layout',
|
||||
attachTo: { id: 'app/root', input: 'children' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory: () => ({
|
||||
element: <CookieAuthRedirect />,
|
||||
}),
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory: () => [coreExtensionData.reactElement(<CookieAuthRedirect />)],
|
||||
});
|
||||
|
||||
const app = createApp({
|
||||
|
||||
@@ -20,8 +20,8 @@ import {
|
||||
coreExtensionData,
|
||||
createExtensionInput,
|
||||
useRouteRef,
|
||||
createNavItemExtension,
|
||||
createNavLogoExtension,
|
||||
NavItemBlueprint,
|
||||
NavLogoBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { makeStyles } from '@material-ui/core/styles';
|
||||
import {
|
||||
@@ -53,7 +53,7 @@ const useSidebarLogoStyles = makeStyles({
|
||||
});
|
||||
|
||||
const SidebarLogo = (
|
||||
props: (typeof createNavLogoExtension.logoElementsDataRef)['T'],
|
||||
props: (typeof NavLogoBlueprint.dataRefs.logoElements)['T'],
|
||||
) => {
|
||||
const classes = useSidebarLogoStyles();
|
||||
const { isOpen } = useSidebarOpenState();
|
||||
@@ -70,7 +70,7 @@ const SidebarLogo = (
|
||||
};
|
||||
|
||||
const SidebarNavItem = (
|
||||
props: (typeof createNavItemExtension.targetDataRef)['T'],
|
||||
props: (typeof NavItemBlueprint.dataRefs.target)['T'],
|
||||
) => {
|
||||
const { icon: Icon, title, routeRef } = props;
|
||||
const link = useRouteRef(routeRef);
|
||||
@@ -86,8 +86,8 @@ export const AppNav = createExtension({
|
||||
name: 'nav',
|
||||
attachTo: { id: 'app/layout', input: 'nav' },
|
||||
inputs: {
|
||||
items: createExtensionInput([createNavItemExtension.targetDataRef]),
|
||||
logos: createExtensionInput([createNavLogoExtension.logoElementsDataRef], {
|
||||
items: createExtensionInput([NavItemBlueprint.dataRefs.target]),
|
||||
logos: createExtensionInput([NavLogoBlueprint.dataRefs.logoElements], {
|
||||
singleton: true,
|
||||
optional: true,
|
||||
}),
|
||||
@@ -97,12 +97,12 @@ export const AppNav = createExtension({
|
||||
coreExtensionData.reactElement(
|
||||
<Sidebar>
|
||||
<SidebarLogo
|
||||
{...inputs.logos?.get(createNavLogoExtension.logoElementsDataRef)}
|
||||
{...inputs.logos?.get(NavLogoBlueprint.dataRefs.logoElements)}
|
||||
/>
|
||||
<SidebarDivider />
|
||||
{inputs.items.map((item, index) => (
|
||||
<SidebarNavItem
|
||||
{...item.get(createNavItemExtension.targetDataRef)}
|
||||
{...item.get(NavItemBlueprint.dataRefs.target)}
|
||||
key={index}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -15,14 +15,15 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AnyExtensionDataRef,
|
||||
AppNode,
|
||||
Extension,
|
||||
ExtensionInput,
|
||||
PortableSchema,
|
||||
ResolvedExtensionInput,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
createExtensionInput,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
createAppNodeInstance,
|
||||
@@ -31,7 +32,12 @@ import {
|
||||
import { AppNodeSpec } from '@backstage/frontend-plugin-api';
|
||||
import { resolveAppTree } from './resolveAppTree';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { resolveExtensionDefinition } from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
import {
|
||||
InternalExtension,
|
||||
resolveExtensionDefinition,
|
||||
} from '../../../frontend-plugin-api/src/wiring/resolveExtensionDefinition';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { createSchemaFromZod } from '../../../frontend-plugin-api/src/schema/createSchemaFromZod';
|
||||
import { withLogCollector } from '@backstage/test-utils';
|
||||
|
||||
const testDataRef = createExtensionDataRef<string>().with({ id: 'test' });
|
||||
@@ -80,28 +86,63 @@ function makeInstanceWithId<TConfig, TConfigInput>(
|
||||
};
|
||||
}
|
||||
|
||||
function createV1ExtensionInput(
|
||||
extensionData: Record<string, AnyExtensionDataRef>,
|
||||
options: { singleton?: boolean; optional?: boolean } = {},
|
||||
) {
|
||||
return {
|
||||
$$type: '@backstage/ExtensionInput' as const,
|
||||
extensionData,
|
||||
config: {
|
||||
singleton: options.singleton ?? false,
|
||||
optional: options.optional ?? false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function createV1Extension(opts: {
|
||||
namespace: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
inputs?: Record<string, ReturnType<typeof createV1ExtensionInput>>;
|
||||
output: Record<string, AnyExtensionDataRef>;
|
||||
configSchema?: PortableSchema<any, any>;
|
||||
factory: (ctx: { inputs: any; config: any }) => any;
|
||||
}): Extension<any, any> {
|
||||
const ext: InternalExtension<any, any> = {
|
||||
$$type: '@backstage/Extension',
|
||||
version: 'v1',
|
||||
id: opts.name ? `${opts.namespace}/${opts.name}` : opts.namespace,
|
||||
disabled: false,
|
||||
attachTo: opts.attachTo ?? { id: 'ignored', input: 'ignored' },
|
||||
inputs: opts.inputs ?? {},
|
||||
output: opts.output,
|
||||
configSchema: opts.configSchema,
|
||||
factory: opts.factory,
|
||||
};
|
||||
return ext;
|
||||
}
|
||||
|
||||
describe('instantiateAppNodeTree', () => {
|
||||
describe('v1', () => {
|
||||
const simpleExtension = resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
output: z.string().default('test'),
|
||||
other: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { test: config.output, other: config.other };
|
||||
},
|
||||
}),
|
||||
);
|
||||
const simpleExtension = createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
output: z.string().default('test'),
|
||||
other: z.number().optional(),
|
||||
}),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { test: config.output, other: config.other };
|
||||
},
|
||||
});
|
||||
|
||||
it('should instantiate a single node', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
@@ -129,21 +170,19 @@ describe('instantiateAppNodeTree', () => {
|
||||
it('should instantiate a node with attachments', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
makeSpec(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createV1ExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
makeSpec(simpleExtension, {
|
||||
id: 'child-node',
|
||||
@@ -175,21 +214,19 @@ describe('instantiateAppNodeTree', () => {
|
||||
const tree = resolveAppTree('root-node', [
|
||||
{
|
||||
...makeSpec(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'root-node',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
test: createV1ExtensionInput({ test: testDataRef }),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
{
|
||||
@@ -262,46 +299,44 @@ describe('instantiateAppNodeTree', () => {
|
||||
const instance = createAppNodeInstance({
|
||||
attachments,
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: '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({
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
optionalSingletonPresent: createV1ExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
optionalSingletonMissing: createV1ExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
singleton: createV1ExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
many: createV1ExtensionInput({
|
||||
test: testDataRef,
|
||||
other: otherDataRef.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
inputMirror: inputMirrorDataRef,
|
||||
},
|
||||
factory({ inputs }) {
|
||||
return { inputMirror: inputs };
|
||||
},
|
||||
}),
|
||||
),
|
||||
});
|
||||
|
||||
@@ -344,19 +379,17 @@ describe('instantiateAppNodeTree', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory() {
|
||||
const error = new Error('NOPE');
|
||||
error.name = 'NopeError';
|
||||
throw error;
|
||||
},
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory() {
|
||||
const error = new Error('NOPE');
|
||||
error.name = 'NopeError';
|
||||
throw error;
|
||||
},
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
@@ -369,20 +402,18 @@ describe('instantiateAppNodeTree', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test1: testDataRef,
|
||||
test2: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { test1: 'test', test2: 'test2' };
|
||||
},
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test1: testDataRef,
|
||||
test2: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { test1: 'test', test2: 'test2' };
|
||||
},
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
@@ -395,19 +426,17 @@ describe('instantiateAppNodeTree', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { nonexistent: 'test' } as any;
|
||||
},
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
test: testDataRef,
|
||||
},
|
||||
factory({}) {
|
||||
return { nonexistent: 'test' } as any;
|
||||
},
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
@@ -420,23 +449,21 @@ describe('instantiateAppNodeTree', () => {
|
||||
expect(() =>
|
||||
createAppNodeInstance({
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createV1ExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
attachments: new Map(),
|
||||
}),
|
||||
@@ -467,20 +494,18 @@ describe('instantiateAppNodeTree', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'parent',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
declared: createExtensionInput({
|
||||
test: testDataRef,
|
||||
}),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'parent',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
declared: createV1ExtensionInput({
|
||||
test: testDataRef,
|
||||
}),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -507,15 +532,13 @@ describe('instantiateAppNodeTree', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'parent',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'parent',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
);
|
||||
@@ -539,23 +562,21 @@ describe('instantiateAppNodeTree', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createV1ExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
@@ -576,23 +597,21 @@ describe('instantiateAppNodeTree', () => {
|
||||
],
|
||||
]),
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createV1ExtensionInput(
|
||||
{
|
||||
test: testDataRef,
|
||||
},
|
||||
{ singleton: true, optional: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrow(
|
||||
@@ -607,23 +626,21 @@ describe('instantiateAppNodeTree', () => {
|
||||
['singleton', [makeInstanceWithId(simpleExtension, undefined)]],
|
||||
]),
|
||||
node: makeNode(
|
||||
resolveExtensionDefinition(
|
||||
createExtension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createExtensionInput(
|
||||
{
|
||||
other: otherDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
createV1Extension({
|
||||
namespace: 'app',
|
||||
name: 'test',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
inputs: {
|
||||
singleton: createV1ExtensionInput(
|
||||
{
|
||||
other: otherDataRef,
|
||||
},
|
||||
{ singleton: true },
|
||||
),
|
||||
},
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
}),
|
||||
),
|
||||
}),
|
||||
).toThrowErrorMatchingInlineSnapshot(
|
||||
|
||||
@@ -15,9 +15,7 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
AnyExtensionDataMap,
|
||||
AnyExtensionDataRef,
|
||||
AnyExtensionInputMap,
|
||||
ExtensionDataContainer,
|
||||
ExtensionDataRef,
|
||||
ExtensionInput,
|
||||
@@ -32,8 +30,10 @@ type Mutable<T> = {
|
||||
-readonly [P in keyof T]: T[P];
|
||||
};
|
||||
|
||||
function resolveInputDataMap(
|
||||
dataMap: AnyExtensionDataMap,
|
||||
function resolveV1InputDataMap(
|
||||
dataMap: {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
},
|
||||
attachment: AppNode,
|
||||
inputName: string,
|
||||
) {
|
||||
@@ -134,9 +134,17 @@ function reportUndeclaredAttachments(
|
||||
}
|
||||
|
||||
function resolveV1Inputs(
|
||||
inputMap: AnyExtensionInputMap,
|
||||
inputMap: {
|
||||
[inputName in string]: {
|
||||
$$type: '@backstage/ExtensionInput';
|
||||
extensionData: {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
config: { optional: boolean; singleton: boolean };
|
||||
};
|
||||
},
|
||||
attachments: ReadonlyMap<string, AppNode[]>,
|
||||
): ResolvedExtensionInputs<AnyExtensionInputMap> {
|
||||
) {
|
||||
return mapValues(inputMap, (input, inputName) => {
|
||||
const attachedNodes = attachments.get(inputName) ?? [];
|
||||
|
||||
@@ -158,7 +166,7 @@ function resolveV1Inputs(
|
||||
}
|
||||
return {
|
||||
node: attachedNodes[0],
|
||||
output: resolveInputDataMap(
|
||||
output: resolveV1InputDataMap(
|
||||
input.extensionData,
|
||||
attachedNodes[0],
|
||||
inputName,
|
||||
@@ -168,9 +176,16 @@ function resolveV1Inputs(
|
||||
|
||||
return attachedNodes.map(attachment => ({
|
||||
node: attachment,
|
||||
output: resolveInputDataMap(input.extensionData, attachment, inputName),
|
||||
output: resolveV1InputDataMap(input.extensionData, attachment, inputName),
|
||||
}));
|
||||
}) as ResolvedExtensionInputs<AnyExtensionInputMap>;
|
||||
}) as {
|
||||
[inputName in string]: {
|
||||
node: AppNode;
|
||||
output: {
|
||||
[name in string]: unknown;
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
function resolveV2Inputs(
|
||||
|
||||
@@ -89,8 +89,6 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
import { vmwareCloudAuthApiRef } from '@backstage/core-plugin-api';
|
||||
import { withApis } from '@backstage/core-plugin-api';
|
||||
import { z } from 'zod';
|
||||
import { ZodSchema } from 'zod';
|
||||
import { ZodTypeDef } from 'zod';
|
||||
|
||||
export { AlertApi };
|
||||
|
||||
@@ -147,11 +145,6 @@ export { AnyApiFactory };
|
||||
|
||||
export { AnyApiRef };
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type AnyExtensionDataMap = {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type AnyExtensionDataRef = ExtensionDataRef<
|
||||
unknown,
|
||||
@@ -161,17 +154,6 @@ export type AnyExtensionDataRef = ExtensionDataRef<
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export type AnyExtensionInputMap = {
|
||||
[inputName in string]: LegacyExtensionInput<
|
||||
AnyExtensionDataMap,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
||||
// @public (undocumented)
|
||||
export type AnyExternalRoutes = {
|
||||
[name in string]: ExternalRouteRef;
|
||||
@@ -456,141 +438,50 @@ export type CoreNotFoundErrorPageProps = {
|
||||
// @public (undocumented)
|
||||
export type CoreProgressProps = {};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function createApiExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
api: AnyApiRef;
|
||||
factory: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => AnyApiFactory;
|
||||
}
|
||||
| {
|
||||
factory: AnyApiFactory;
|
||||
}
|
||||
) & {
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
inputs?: TInputs;
|
||||
},
|
||||
): ExtensionDefinition<
|
||||
TConfig,
|
||||
TConfig,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createApiExtension {
|
||||
const // @deprecated (undocumented)
|
||||
factoryDataRef: ConfigurableExtensionDataRef<
|
||||
AnyApiFactory,
|
||||
'core.api.factory',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
export { createApiFactory };
|
||||
|
||||
export { createApiRef };
|
||||
|
||||
// @public @deprecated
|
||||
export function createAppRootElementExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
element:
|
||||
| JSX_2.Element
|
||||
| ((options: {
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
config: TConfig;
|
||||
}) => JSX_2.Element);
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public @deprecated
|
||||
export function createAppRootWrapperExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
Component: ComponentType<
|
||||
PropsWithChildren<{
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
config: TConfig;
|
||||
}>
|
||||
>;
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createAppRootWrapperExtension {
|
||||
const // @deprecated (undocumented)
|
||||
componentDataRef: ConfigurableExtensionDataRef<
|
||||
React_2.ComponentType<{
|
||||
children?: React_2.ReactNode;
|
||||
}>,
|
||||
'app.root.wrapper',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
// @public (undocumented)
|
||||
export function createComponentExtension<
|
||||
TProps extends {},
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
export function createComponentExtension<TProps extends {}>(options: {
|
||||
ref: ComponentRef<TProps>;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
loader:
|
||||
| {
|
||||
lazy: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<ComponentType<TProps>>;
|
||||
lazy: () => Promise<ComponentType<TProps>>;
|
||||
}
|
||||
| {
|
||||
sync: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => ComponentType<TProps>;
|
||||
sync: () => ComponentType<TProps>;
|
||||
};
|
||||
}): ExtensionDefinition<
|
||||
TConfig,
|
||||
TConfig,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
[x: string]: any;
|
||||
},
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
ConfigurableExtensionDataRef<
|
||||
{
|
||||
ref: ComponentRef;
|
||||
impl: ComponentType;
|
||||
},
|
||||
'core.component.component',
|
||||
{}
|
||||
>,
|
||||
{
|
||||
[x: string]: ExtensionInput<
|
||||
AnyExtensionDataRef,
|
||||
{
|
||||
optional: boolean;
|
||||
singleton: boolean;
|
||||
}
|
||||
>;
|
||||
},
|
||||
{
|
||||
kind: 'component';
|
||||
namespace: string;
|
||||
name: string;
|
||||
}
|
||||
>;
|
||||
|
||||
@@ -659,21 +550,6 @@ export function createExtension<
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function createExtension<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
>(
|
||||
options: LegacyCreateExtensionOptions<
|
||||
TOutput,
|
||||
TInputs,
|
||||
TConfig,
|
||||
TConfigInput
|
||||
>,
|
||||
): ExtensionDefinition<TConfig, TConfigInput, never, never>;
|
||||
|
||||
// @public
|
||||
export function createExtensionBlueprint<
|
||||
TParams,
|
||||
@@ -795,24 +671,6 @@ export function createExtensionDataRef<TData>(): {
|
||||
}): ConfigurableExtensionDataRef<TData, TId>;
|
||||
};
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function createExtensionInput<
|
||||
TExtensionDataMap extends AnyExtensionDataMap,
|
||||
TConfig extends {
|
||||
singleton?: boolean;
|
||||
optional?: boolean;
|
||||
},
|
||||
>(
|
||||
extensionData: TExtensionDataMap,
|
||||
config?: TConfig,
|
||||
): LegacyExtensionInput<
|
||||
TExtensionDataMap,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public (undocumented)
|
||||
export function createExtensionInput<
|
||||
UExtensionData extends ExtensionDataRef<
|
||||
@@ -926,105 +784,6 @@ export function createFrontendPlugin<
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated
|
||||
export function createNavItemExtension(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
routeRef: RouteRef<undefined>;
|
||||
title: string;
|
||||
icon: IconComponent_2;
|
||||
}): ExtensionDefinition<
|
||||
{
|
||||
title: string;
|
||||
},
|
||||
{
|
||||
title?: string | undefined;
|
||||
},
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createNavItemExtension {
|
||||
const // @deprecated (undocumented)
|
||||
targetDataRef: ConfigurableExtensionDataRef<
|
||||
{
|
||||
title: string;
|
||||
icon: IconComponent_2;
|
||||
routeRef: RouteRef<undefined>;
|
||||
},
|
||||
'core.nav-item.target',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export function createNavLogoExtension(options: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
logoIcon: JSX.Element;
|
||||
logoFull: JSX.Element;
|
||||
}): ExtensionDefinition<
|
||||
unknown,
|
||||
unknown,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createNavLogoExtension {
|
||||
const // @deprecated (undocumented)
|
||||
logoElementsDataRef: ConfigurableExtensionDataRef<
|
||||
{
|
||||
logoIcon?: JSX.Element | undefined;
|
||||
logoFull?: JSX.Element | undefined;
|
||||
},
|
||||
'core.nav-logo.logo-elements',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
// @public @deprecated
|
||||
export function createPageExtension<
|
||||
TConfig extends {
|
||||
path: string;
|
||||
},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
defaultPath: string;
|
||||
}
|
||||
| {
|
||||
configSchema: PortableSchema<TConfig>;
|
||||
}
|
||||
) & {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
},
|
||||
): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export const createPlugin: typeof createFrontendPlugin;
|
||||
|
||||
@@ -1048,75 +807,6 @@ export function createRouteRef<
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated
|
||||
export function createRouterExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
Component: ComponentType<
|
||||
PropsWithChildren<{
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
config: TConfig;
|
||||
}>
|
||||
>;
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createRouterExtension {
|
||||
const // @deprecated (undocumented)
|
||||
componentDataRef: ConfigurableExtensionDataRef<
|
||||
React_2.ComponentType<{
|
||||
children?: React_2.ReactNode;
|
||||
}>,
|
||||
'app.router.wrapper',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function createSchemaFromZod<TOutput, TInput>(
|
||||
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
|
||||
): PortableSchema<TOutput, TInput>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function createSignInPageExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<ComponentType<SignInPageProps>>;
|
||||
}): ExtensionDefinition<TConfig>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createSignInPageExtension {
|
||||
const // @deprecated (undocumented)
|
||||
componentDataRef: ConfigurableExtensionDataRef<
|
||||
React_2.ComponentType<SignInPageProps>,
|
||||
'core.sign-in-page.component',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
// @public
|
||||
export function createSubRouteRef<
|
||||
Path extends string,
|
||||
@@ -1126,62 +816,6 @@ export function createSubRouteRef<
|
||||
parent: RouteRef<ParentParams>;
|
||||
}): MakeSubRouteRef<PathParams<Path>, ParentParams>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function createThemeExtension(theme: AppTheme): ExtensionDefinition<
|
||||
unknown,
|
||||
unknown,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createThemeExtension {
|
||||
const // (undocumented)
|
||||
themeDataRef: ConfigurableExtensionDataRef<
|
||||
AppTheme,
|
||||
'core.theme.theme',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export function createTranslationExtension(options: {
|
||||
name?: string;
|
||||
resource: TranslationResource | TranslationMessages;
|
||||
}): ExtensionDefinition<
|
||||
unknown,
|
||||
unknown,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export namespace createTranslationExtension {
|
||||
const // (undocumented)
|
||||
translationDataRef: ConfigurableExtensionDataRef<
|
||||
| TranslationResource<string>
|
||||
| TranslationMessages<
|
||||
string,
|
||||
{
|
||||
[x: string]: string;
|
||||
},
|
||||
boolean
|
||||
>,
|
||||
'core.translation.translation',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
export { createTranslationMessages };
|
||||
|
||||
export { createTranslationRef };
|
||||
@@ -1429,21 +1063,6 @@ export type ExtensionDataValue<TData, TId extends string> = {
|
||||
readonly value: TData;
|
||||
};
|
||||
|
||||
// @public @deprecated
|
||||
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 ExtensionDefinition<
|
||||
TConfig,
|
||||
@@ -1696,56 +1315,6 @@ export { IdentityApi };
|
||||
|
||||
export { identityApiRef };
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export interface LegacyCreateExtensionOptions<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
> {
|
||||
// (undocumented)
|
||||
attachTo: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
// (undocumented)
|
||||
configSchema?: PortableSchema<TConfig, TConfigInput>;
|
||||
// (undocumented)
|
||||
disabled?: boolean;
|
||||
// (undocumented)
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
// (undocumented)
|
||||
inputs?: TInputs;
|
||||
// (undocumented)
|
||||
kind?: string;
|
||||
// (undocumented)
|
||||
name?: string;
|
||||
// (undocumented)
|
||||
namespace?: string;
|
||||
// (undocumented)
|
||||
output: TOutput;
|
||||
}
|
||||
|
||||
// @public @deprecated (undocumented)
|
||||
export interface LegacyExtensionInput<
|
||||
TExtensionDataMap extends AnyExtensionDataMap,
|
||||
TConfig extends {
|
||||
singleton: boolean;
|
||||
optional: boolean;
|
||||
},
|
||||
> {
|
||||
// (undocumented)
|
||||
$$type: '@backstage/ExtensionInput';
|
||||
// (undocumented)
|
||||
config: TConfig;
|
||||
// (undocumented)
|
||||
extensionData: TExtensionDataMap;
|
||||
}
|
||||
|
||||
export { microsoftAuthApiRef };
|
||||
|
||||
// @public
|
||||
@@ -1906,17 +1475,12 @@ export type ResolvedExtensionInput<
|
||||
? {
|
||||
node: AppNode;
|
||||
} & ExtensionDataContainer<TExtensionInput['extensionData'][number]>
|
||||
: TExtensionInput['extensionData'] extends AnyExtensionDataMap
|
||||
? {
|
||||
node: AppNode;
|
||||
output: ExtensionDataValues<TExtensionInput['extensionData']>;
|
||||
}
|
||||
: never;
|
||||
|
||||
// @public
|
||||
export type ResolvedExtensionInputs<
|
||||
TInputs extends {
|
||||
[name in string]: ExtensionInput<any, any> | LegacyExtensionInput<any, any>;
|
||||
[name in string]: ExtensionInput<any, any>;
|
||||
},
|
||||
> = {
|
||||
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
|
||||
|
||||
@@ -1,92 +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 { createApiExtension } from './createApiExtension';
|
||||
import { createApiFactory, createApiRef } from '@backstage/core-plugin-api';
|
||||
|
||||
describe('createApiExtension', () => {
|
||||
it('fills in the expected values for an existing factory', () => {
|
||||
const api = createApiRef<{ foo: string }>({ id: 'test' });
|
||||
const factory = createApiFactory({
|
||||
api,
|
||||
deps: {},
|
||||
factory: () => ({ foo: 'bar' }),
|
||||
});
|
||||
|
||||
expect(
|
||||
createApiExtension({
|
||||
factory,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'api',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
disabled: false,
|
||||
configSchema: undefined,
|
||||
inputs: {},
|
||||
output: {
|
||||
api: expect.objectContaining({
|
||||
$$type: '@backstage/ExtensionDataRef',
|
||||
id: 'core.api.factory',
|
||||
config: {},
|
||||
}),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('fills in the expected values for a ref and custom factory', () => {
|
||||
const api = createApiRef<{ foo: string }>({ id: 'test' });
|
||||
const factory = jest.fn(() => ({ foo: 'bar' }));
|
||||
|
||||
const extension = createApiExtension({
|
||||
api,
|
||||
inputs: {},
|
||||
factory({ config: _config, inputs: _inputs }) {
|
||||
return createApiFactory({
|
||||
api,
|
||||
deps: {},
|
||||
factory,
|
||||
});
|
||||
},
|
||||
});
|
||||
// boo
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'api',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
disabled: false,
|
||||
configSchema: undefined,
|
||||
inputs: {},
|
||||
output: {
|
||||
api: expect.objectContaining({
|
||||
$$type: '@backstage/ExtensionDataRef',
|
||||
id: 'core.api.factory',
|
||||
config: {},
|
||||
}),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,82 +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, AnyApiRef } from '@backstage/core-plugin-api';
|
||||
import { PortableSchema } from '../schema';
|
||||
import { ResolvedExtensionInputs, createExtension } from '../wiring';
|
||||
import { AnyExtensionInputMap } from '../wiring/createExtension';
|
||||
import { Expand } from '../types';
|
||||
import { ApiBlueprint } from '../blueprints/ApiBlueprint';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link ApiBlueprint} instead.
|
||||
*/
|
||||
export function createApiExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
api: AnyApiRef;
|
||||
factory: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => AnyApiFactory;
|
||||
}
|
||||
| {
|
||||
factory: AnyApiFactory;
|
||||
}
|
||||
) & {
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
inputs?: TInputs;
|
||||
},
|
||||
) {
|
||||
const { factory, configSchema, inputs: extensionInputs } = options;
|
||||
|
||||
const apiRef =
|
||||
'api' in options ? options.api : (factory as { api: AnyApiRef }).api;
|
||||
|
||||
return createExtension({
|
||||
kind: 'api',
|
||||
// Since ApiRef IDs use a global namespace we use the namespace here in order to override
|
||||
// potential plugin IDs and always end up with the format `api:<api-ref-id>`
|
||||
namespace: apiRef.id,
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
inputs: extensionInputs,
|
||||
configSchema,
|
||||
output: {
|
||||
api: ApiBlueprint.dataRefs.factory,
|
||||
},
|
||||
factory({ config, inputs }) {
|
||||
if (typeof factory === 'function') {
|
||||
return { api: factory({ config, inputs }) };
|
||||
}
|
||||
return { api: factory };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link ApiBlueprint} instead.
|
||||
*/
|
||||
export namespace createApiExtension {
|
||||
/**
|
||||
* @deprecated Use {@link ApiBlueprint} instead.
|
||||
*/
|
||||
export const factoryDataRef = ApiBlueprint.dataRefs.factory;
|
||||
}
|
||||
@@ -1,109 +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 { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
import { coreExtensionData } from '../wiring/coreExtensionData';
|
||||
import { createExtension } from '../wiring/createExtension';
|
||||
import { createExtensionInput } from '../wiring/createExtensionInput';
|
||||
import { createAppRootElementExtension } from './createAppRootElementExtension';
|
||||
|
||||
describe('createAppRootElementExtension', () => {
|
||||
it('works with simple options and just an element', async () => {
|
||||
const extension = createAppRootElementExtension({
|
||||
element: <div>Hello</div>,
|
||||
});
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'app-root-element',
|
||||
attachTo: { id: 'app/root', input: 'elements' },
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
element: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
});
|
||||
|
||||
createExtensionTester(extension).render();
|
||||
|
||||
await expect(screen.findByText('Hello')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works with complex options and a callback', async () => {
|
||||
const schema = createSchemaFromZod(z => z.object({ name: z.string() }));
|
||||
|
||||
const extension = createAppRootElementExtension({
|
||||
namespace: 'ns',
|
||||
name: 'test',
|
||||
configSchema: schema,
|
||||
attachTo: { id: 'other', input: 'slot' },
|
||||
disabled: true,
|
||||
inputs: {
|
||||
children: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
element: ({ inputs, config }) => (
|
||||
<div>
|
||||
Hello, {config.name}, {inputs.children.length}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'app-root-element',
|
||||
namespace: 'ns',
|
||||
name: 'test',
|
||||
attachTo: { id: 'other', input: 'slot' },
|
||||
configSchema: schema,
|
||||
disabled: true,
|
||||
inputs: {
|
||||
children: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
});
|
||||
|
||||
createExtensionTester(extension, { config: { name: 'Robin' } })
|
||||
.add(
|
||||
createExtension({
|
||||
attachTo: { id: 'app-root-element:ns/test', input: 'children' },
|
||||
output: { element: coreExtensionData.reactElement },
|
||||
factory: () => ({ element: <div /> }),
|
||||
}),
|
||||
)
|
||||
.render();
|
||||
|
||||
await expect(
|
||||
screen.findByText('Hello, Robin, 1'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,72 +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 { JSX } from 'react';
|
||||
import { PortableSchema } from '../schema/types';
|
||||
import { Expand } from '../types';
|
||||
import { coreExtensionData } from '../wiring/coreExtensionData';
|
||||
import {
|
||||
AnyExtensionInputMap,
|
||||
ExtensionDefinition,
|
||||
ResolvedExtensionInputs,
|
||||
createExtension,
|
||||
} from '../wiring/createExtension';
|
||||
|
||||
/**
|
||||
* Creates an extension that renders a React element at the app root, outside of
|
||||
* the app layout. This is useful for example for shared popups and similar.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link AppRootElementBlueprint} instead.
|
||||
*/
|
||||
export function createAppRootElementExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
element:
|
||||
| JSX.Element
|
||||
| ((options: {
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
config: TConfig;
|
||||
}) => JSX.Element);
|
||||
}): ExtensionDefinition<TConfig> {
|
||||
return createExtension({
|
||||
kind: 'app-root-element',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? { id: 'app/root', input: 'elements' },
|
||||
configSchema: options.configSchema,
|
||||
disabled: options.disabled,
|
||||
inputs: options.inputs,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory({ inputs, config }) {
|
||||
return {
|
||||
element:
|
||||
typeof options.element === 'function'
|
||||
? options.element({ inputs, config })
|
||||
: options.element,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,121 +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 { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
import { coreExtensionData } from '../wiring/coreExtensionData';
|
||||
import { createExtension } from '../wiring/createExtension';
|
||||
import { createExtensionInput } from '../wiring/createExtensionInput';
|
||||
import { createAppRootWrapperExtension } from './createAppRootWrapperExtension';
|
||||
import { createPageExtension } from './createPageExtension';
|
||||
|
||||
describe('createAppRootWrapperExtension', () => {
|
||||
it('works with simple options and no props', async () => {
|
||||
const extension = createAppRootWrapperExtension({
|
||||
Component: () => <div>Hello</div>,
|
||||
});
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'app-root-wrapper',
|
||||
attachTo: { id: 'app/root', input: 'wrappers' },
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
component: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
});
|
||||
|
||||
createExtensionTester(
|
||||
createPageExtension({
|
||||
defaultPath: '/',
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
)
|
||||
.add(extension)
|
||||
.render();
|
||||
|
||||
await expect(screen.findByText('Hello')).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works with complex options and props', async () => {
|
||||
const schema = createSchemaFromZod(z => z.object({ name: z.string() }));
|
||||
|
||||
const extension = createAppRootWrapperExtension({
|
||||
namespace: 'ns',
|
||||
name: 'test',
|
||||
configSchema: schema,
|
||||
disabled: true,
|
||||
inputs: {
|
||||
children: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
Component: ({ inputs, config, children }) => (
|
||||
<div data-testid={`${config.name}-${inputs.children.length}`}>
|
||||
{children}
|
||||
</div>
|
||||
),
|
||||
});
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'app-root-wrapper',
|
||||
namespace: 'ns',
|
||||
name: 'test',
|
||||
attachTo: { id: 'app/root', input: 'wrappers' },
|
||||
configSchema: schema,
|
||||
disabled: true,
|
||||
inputs: {
|
||||
children: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
component: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
|
||||
createExtensionTester(
|
||||
createPageExtension({
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>Hello</div>,
|
||||
}),
|
||||
)
|
||||
.add(extension, { config: { name: 'Robin' } })
|
||||
.add(
|
||||
createExtension({
|
||||
attachTo: { id: 'app-root-wrapper:ns/test', input: 'children' },
|
||||
output: { element: coreExtensionData.reactElement },
|
||||
factory: () => ({ element: <div /> }),
|
||||
}),
|
||||
)
|
||||
.render();
|
||||
|
||||
await expect(screen.findByText('Hello')).resolves.toBeInTheDocument();
|
||||
await expect(screen.findByTestId('Robin-1')).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,88 +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 React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { PortableSchema } from '../schema/types';
|
||||
import {
|
||||
AnyExtensionInputMap,
|
||||
ExtensionDefinition,
|
||||
ResolvedExtensionInputs,
|
||||
createExtension,
|
||||
} from '../wiring/createExtension';
|
||||
import { Expand } from '../types';
|
||||
import { AppRootWrapperBlueprint } from '../blueprints/AppRootWrapperBlueprint';
|
||||
|
||||
/**
|
||||
* Creates an extension that renders a React wrapper at the app root, enclosing
|
||||
* the app layout. This is useful for example for adding global React contexts
|
||||
* and similar.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link AppRootWrapperBlueprint} instead.
|
||||
*/
|
||||
export function createAppRootWrapperExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
Component: ComponentType<
|
||||
PropsWithChildren<{
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
config: TConfig;
|
||||
}>
|
||||
>;
|
||||
}): ExtensionDefinition<TConfig> {
|
||||
return createExtension({
|
||||
kind: 'app-root-wrapper',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? { id: 'app/root', input: 'wrappers' },
|
||||
configSchema: options.configSchema,
|
||||
disabled: options.disabled,
|
||||
inputs: options.inputs,
|
||||
output: {
|
||||
component: AppRootWrapperBlueprint.dataRefs.component,
|
||||
},
|
||||
factory({ inputs, config }) {
|
||||
const Component = (props: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<options.Component inputs={inputs} config={config}>
|
||||
{props.children}
|
||||
</options.Component>
|
||||
);
|
||||
};
|
||||
return {
|
||||
component: Component,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link AppRootWrapperBlueprint} instead.
|
||||
*/
|
||||
export namespace createAppRootWrapperExtension {
|
||||
/**
|
||||
* @deprecated Use {@link AppRootWrapperBlueprint} instead.
|
||||
*/
|
||||
export const componentDataRef = AppRootWrapperBlueprint.dataRefs.component;
|
||||
}
|
||||
@@ -15,41 +15,20 @@
|
||||
*/
|
||||
|
||||
import { lazy, ComponentType } from 'react';
|
||||
import {
|
||||
AnyExtensionInputMap,
|
||||
ResolvedExtensionInputs,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
} from '../wiring';
|
||||
import { Expand } from '../types';
|
||||
import { PortableSchema } from '../schema';
|
||||
import { createExtension, createExtensionDataRef } from '../wiring';
|
||||
import { ComponentRef } from '../components';
|
||||
|
||||
/** @public */
|
||||
export function createComponentExtension<
|
||||
TProps extends {},
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
export function createComponentExtension<TProps extends {}>(options: {
|
||||
ref: ComponentRef<TProps>;
|
||||
name?: string;
|
||||
disabled?: boolean;
|
||||
/** @deprecated these will be removed in the future */
|
||||
inputs?: TInputs;
|
||||
/** @deprecated these will be removed in the future */
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
loader:
|
||||
| {
|
||||
lazy: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<ComponentType<TProps>>;
|
||||
lazy: () => Promise<ComponentType<TProps>>;
|
||||
}
|
||||
| {
|
||||
sync: (values: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => ComponentType<TProps>;
|
||||
sync: () => ComponentType<TProps>;
|
||||
};
|
||||
}) {
|
||||
return createExtension({
|
||||
@@ -57,34 +36,30 @@ export function createComponentExtension<
|
||||
namespace: options.ref.id,
|
||||
name: options.name,
|
||||
attachTo: { id: 'app', input: 'components' },
|
||||
inputs: options.inputs,
|
||||
disabled: options.disabled,
|
||||
configSchema: options.configSchema,
|
||||
output: {
|
||||
component: createComponentExtension.componentDataRef,
|
||||
},
|
||||
factory({ config, inputs }) {
|
||||
output: [createComponentExtension.componentDataRef],
|
||||
factory() {
|
||||
if ('sync' in options.loader) {
|
||||
return {
|
||||
component: {
|
||||
return [
|
||||
createComponentExtension.componentDataRef({
|
||||
ref: options.ref,
|
||||
impl: options.loader.sync({ config, inputs }) as ComponentType,
|
||||
},
|
||||
};
|
||||
impl: options.loader.sync() as ComponentType,
|
||||
}),
|
||||
];
|
||||
}
|
||||
const lazyLoader = options.loader.lazy;
|
||||
const ExtensionComponent = lazy(() =>
|
||||
lazyLoader({ config, inputs }).then(Component => ({
|
||||
lazyLoader().then(Component => ({
|
||||
default: Component,
|
||||
})),
|
||||
) as unknown as ComponentType;
|
||||
|
||||
return {
|
||||
component: {
|
||||
return [
|
||||
createComponentExtension.componentDataRef({
|
||||
ref: options.ref,
|
||||
impl: ExtensionComponent,
|
||||
},
|
||||
};
|
||||
}),
|
||||
];
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,69 +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 { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
import { createExtension } from '../wiring';
|
||||
import { RouteRef } from '../routing';
|
||||
import { NavItemBlueprint } from '../blueprints/NavItemBlueprint';
|
||||
|
||||
/**
|
||||
* Helper for creating extensions for a nav item.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link NavItemBlueprint} instead.
|
||||
*/
|
||||
export function createNavItemExtension(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
routeRef: RouteRef<undefined>;
|
||||
title: string;
|
||||
icon: IconComponent;
|
||||
}) {
|
||||
const { routeRef, title, icon, namespace, name } = options;
|
||||
return createExtension({
|
||||
namespace,
|
||||
name,
|
||||
kind: 'nav-item',
|
||||
attachTo: { id: 'app/nav', input: 'items' },
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
title: z.string().default(title),
|
||||
}),
|
||||
),
|
||||
output: {
|
||||
navTarget: NavItemBlueprint.dataRefs.target,
|
||||
},
|
||||
factory: ({ config }) => ({
|
||||
navTarget: {
|
||||
title: config.title,
|
||||
icon,
|
||||
routeRef,
|
||||
},
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link NavItemBlueprint} instead.
|
||||
*/
|
||||
export namespace createNavItemExtension {
|
||||
/**
|
||||
* @deprecated Use {@link NavItemBlueprint} instead.
|
||||
*/
|
||||
export const targetDataRef = NavItemBlueprint.dataRefs.target;
|
||||
}
|
||||
@@ -1,48 +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 React from 'react';
|
||||
import { createNavLogoExtension } from './createNavLogoExtension';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
}));
|
||||
|
||||
describe('createNavLogoExtension', () => {
|
||||
it('creates the extension properly', () => {
|
||||
expect(
|
||||
createNavLogoExtension({
|
||||
name: 'test',
|
||||
logoFull: <div>Logo Full</div>,
|
||||
logoIcon: <div>Logo Icon</div>,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'nav-logo',
|
||||
name: 'test',
|
||||
attachTo: { id: 'app/nav', input: 'logos' },
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
logos: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,61 +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 { createExtension } from '../wiring';
|
||||
import { NavLogoBlueprint } from '../blueprints/NavLogoBlueprint';
|
||||
|
||||
/**
|
||||
* Helper for creating extensions for a nav logos.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link NavLogoBlueprint} instead.
|
||||
*/
|
||||
export function createNavLogoExtension(options: {
|
||||
name?: string;
|
||||
namespace?: string;
|
||||
logoIcon: JSX.Element;
|
||||
logoFull: JSX.Element;
|
||||
}) {
|
||||
const { logoIcon, logoFull } = options;
|
||||
return createExtension({
|
||||
kind: 'nav-logo',
|
||||
name: options?.name,
|
||||
namespace: options?.namespace,
|
||||
attachTo: { id: 'app/nav', input: 'logos' },
|
||||
output: {
|
||||
logos: NavLogoBlueprint.dataRefs.logoElements,
|
||||
},
|
||||
factory: () => {
|
||||
return {
|
||||
logos: {
|
||||
logoIcon,
|
||||
logoFull,
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link NavLogoBlueprint} instead.
|
||||
*/
|
||||
export namespace createNavLogoExtension {
|
||||
/**
|
||||
* @deprecated Use {@link NavLogoBlueprint} instead.
|
||||
*/
|
||||
export const logoElementsDataRef = NavLogoBlueprint.dataRefs.logoElements;
|
||||
}
|
||||
@@ -1,145 +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 React from 'react';
|
||||
import { useAnalytics } from '@backstage/core-plugin-api';
|
||||
import { waitFor } from '@testing-library/react';
|
||||
import { PortableSchema } from '../schema';
|
||||
import { coreExtensionData, createExtensionInput } from '../wiring';
|
||||
import { createPageExtension } from './createPageExtension';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
|
||||
jest.mock('@backstage/core-plugin-api', () => ({
|
||||
...jest.requireActual('@backstage/core-plugin-api'),
|
||||
useAnalytics: jest.fn(),
|
||||
}));
|
||||
|
||||
describe('createPageExtension', () => {
|
||||
it('creates the extension properly', () => {
|
||||
const configSchema: PortableSchema<{ path: string }> = {
|
||||
parse: jest.fn(),
|
||||
schema: {} as any,
|
||||
};
|
||||
|
||||
expect(
|
||||
createPageExtension({
|
||||
name: 'test',
|
||||
configSchema,
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
name: 'test',
|
||||
kind: 'page',
|
||||
attachTo: { id: 'app/routes', input: 'routes' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
element: expect.anything(),
|
||||
path: expect.anything(),
|
||||
routeRef: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(
|
||||
createPageExtension({
|
||||
name: 'test',
|
||||
attachTo: { id: 'other', input: 'place' },
|
||||
disabled: true,
|
||||
configSchema,
|
||||
inputs: {
|
||||
first: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
name: 'test',
|
||||
kind: 'page',
|
||||
attachTo: { id: 'other', input: 'place' },
|
||||
configSchema: expect.anything(),
|
||||
override: expect.any(Function),
|
||||
disabled: true,
|
||||
inputs: {
|
||||
first: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: expect.anything(),
|
||||
path: expect.anything(),
|
||||
routeRef: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
|
||||
expect(
|
||||
createPageExtension({
|
||||
name: 'test',
|
||||
defaultPath: '/here',
|
||||
loader: async () => <div />,
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
name: 'test',
|
||||
kind: 'page',
|
||||
attachTo: { id: 'app/routes', input: 'routes' },
|
||||
configSchema: expect.anything(),
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
element: expect.anything(),
|
||||
path: expect.anything(),
|
||||
routeRef: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
});
|
||||
});
|
||||
|
||||
it('capture page view event in analytics', async () => {
|
||||
const captureEvent = jest.fn();
|
||||
|
||||
(useAnalytics as jest.Mock).mockReturnValue({
|
||||
captureEvent,
|
||||
});
|
||||
|
||||
createExtensionTester(
|
||||
createPageExtension({
|
||||
defaultPath: '/',
|
||||
loader: async () => <div>Component</div>,
|
||||
}),
|
||||
).render();
|
||||
|
||||
await waitFor(() =>
|
||||
expect(captureEvent).toHaveBeenCalledWith(
|
||||
'_ROUTABLE-EXTENSION-RENDERED',
|
||||
'',
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1,98 +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 React, { lazy } from 'react';
|
||||
import { ExtensionBoundary } from '../components';
|
||||
import { createSchemaFromZod, PortableSchema } from '../schema';
|
||||
import {
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
ResolvedExtensionInputs,
|
||||
AnyExtensionInputMap,
|
||||
} from '../wiring';
|
||||
import { RouteRef } from '../routing';
|
||||
import { Expand } from '../types';
|
||||
import { ExtensionDefinition } from '../wiring/createExtension';
|
||||
|
||||
/**
|
||||
* Helper for creating extensions for a routable React page component.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link PageBlueprint} instead.
|
||||
*/
|
||||
export function createPageExtension<
|
||||
TConfig extends { path: string },
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(
|
||||
options: (
|
||||
| {
|
||||
defaultPath: string;
|
||||
}
|
||||
| {
|
||||
configSchema: PortableSchema<TConfig>;
|
||||
}
|
||||
) & {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
},
|
||||
): ExtensionDefinition<TConfig> {
|
||||
const configSchema =
|
||||
'configSchema' in options
|
||||
? options.configSchema
|
||||
: (createSchemaFromZod(z =>
|
||||
z.object({ path: z.string().default(options.defaultPath) }),
|
||||
) as PortableSchema<TConfig>);
|
||||
|
||||
return createExtension({
|
||||
kind: 'page',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? { id: 'app/routes', input: 'routes' },
|
||||
configSchema,
|
||||
inputs: options.inputs,
|
||||
disabled: options.disabled,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
},
|
||||
factory({ config, inputs, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ config, inputs })
|
||||
.then(element => ({ default: () => element })),
|
||||
);
|
||||
|
||||
return {
|
||||
path: config.path,
|
||||
routeRef: options.routeRef,
|
||||
element: (
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -1,169 +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 { createSpecializedApp } from '@backstage/frontend-app-api';
|
||||
import { render, screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { MockConfigApi } from '@backstage/test-utils';
|
||||
import { MemoryRouter } from 'react-router-dom';
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
import { coreExtensionData } from '../wiring/coreExtensionData';
|
||||
import { createExtension } from '../wiring/createExtension';
|
||||
import { createExtensionInput } from '../wiring/createExtensionInput';
|
||||
import { createExtensionOverrides } from '../wiring/createExtensionOverrides';
|
||||
import { createPageExtension } from './createPageExtension';
|
||||
import { createRouterExtension } from './createRouterExtension';
|
||||
|
||||
describe('createRouterExtension', () => {
|
||||
it('works with simple options and no props', async () => {
|
||||
const extension = createRouterExtension({
|
||||
namespace: 'test',
|
||||
Component: ({ children }) => (
|
||||
<MemoryRouter>
|
||||
<div data-testid="test-router">{children}</div>
|
||||
</MemoryRouter>
|
||||
),
|
||||
});
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'app-router-component',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'app/root', input: 'router' },
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
component: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
extension,
|
||||
createPageExtension({
|
||||
namespace: 'test',
|
||||
defaultPath: '/',
|
||||
loader: async () => <div data-testid="test-contents" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
render(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('test-contents'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('test-router'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('works with complex options and props', async () => {
|
||||
const schema = createSchemaFromZod(z => z.object({ name: z.string() }));
|
||||
|
||||
const extension = createRouterExtension({
|
||||
namespace: 'test',
|
||||
name: 'test',
|
||||
configSchema: schema,
|
||||
inputs: {
|
||||
children: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
Component: ({ inputs, config, children }) => (
|
||||
<MemoryRouter>
|
||||
<div
|
||||
data-testid={`test-router-${config.name}-${inputs.children.length}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</MemoryRouter>
|
||||
),
|
||||
});
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'app-router-component',
|
||||
namespace: 'test',
|
||||
name: 'test',
|
||||
attachTo: { id: 'app/root', input: 'router' },
|
||||
configSchema: schema,
|
||||
disabled: false,
|
||||
inputs: {
|
||||
children: createExtensionInput({
|
||||
element: coreExtensionData.reactElement,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
component: expect.anything(),
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
override: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
|
||||
const app = createSpecializedApp({
|
||||
features: [
|
||||
createExtensionOverrides({
|
||||
extensions: [
|
||||
extension,
|
||||
createExtension({
|
||||
namespace: 'test',
|
||||
attachTo: {
|
||||
id: 'app-router-component:test/test',
|
||||
input: 'children',
|
||||
},
|
||||
output: { element: coreExtensionData.reactElement }, // doesn't matter
|
||||
factory: () => ({ element: <div /> }),
|
||||
}),
|
||||
createPageExtension({
|
||||
namespace: 'test',
|
||||
defaultPath: '/',
|
||||
loader: async () => <div data-testid="test-contents" />,
|
||||
}),
|
||||
],
|
||||
}),
|
||||
],
|
||||
config: new MockConfigApi({
|
||||
app: {
|
||||
extensions: [
|
||||
{
|
||||
'app-router-component:test/test': { config: { name: 'Robin' } },
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
render(app.createRoot());
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('test-contents'),
|
||||
).resolves.toBeInTheDocument();
|
||||
await expect(
|
||||
screen.findByTestId('test-router-Robin-1'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,88 +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 React, { ComponentType, PropsWithChildren } from 'react';
|
||||
import { PortableSchema } from '../schema/types';
|
||||
import {
|
||||
AnyExtensionInputMap,
|
||||
ExtensionDefinition,
|
||||
ResolvedExtensionInputs,
|
||||
createExtension,
|
||||
} from '../wiring/createExtension';
|
||||
import { Expand } from '../types';
|
||||
import { RouterBlueprint } from '../blueprints/RouterBlueprint';
|
||||
|
||||
/**
|
||||
* Creates an extension that replaces the router implementation at the app root.
|
||||
* This is useful to be able to for example replace the BrowserRouter with a
|
||||
* MemoryRouter in tests, or to add additional props to a BrowserRouter.
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link RouterBlueprint} instead.
|
||||
*/
|
||||
export function createRouterExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
Component: ComponentType<
|
||||
PropsWithChildren<{
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
config: TConfig;
|
||||
}>
|
||||
>;
|
||||
}): ExtensionDefinition<TConfig> {
|
||||
return createExtension({
|
||||
kind: 'app-router-component',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? { id: 'app/root', input: 'router' },
|
||||
configSchema: options.configSchema,
|
||||
disabled: options.disabled,
|
||||
inputs: options.inputs,
|
||||
output: {
|
||||
component: RouterBlueprint.dataRefs.component,
|
||||
},
|
||||
factory({ inputs, config }) {
|
||||
const Component = (props: PropsWithChildren<{}>) => {
|
||||
return (
|
||||
<options.Component inputs={inputs} config={config}>
|
||||
{props.children}
|
||||
</options.Component>
|
||||
);
|
||||
};
|
||||
return {
|
||||
component: Component,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link RouterBlueprint} instead.
|
||||
*/
|
||||
export namespace createRouterExtension {
|
||||
/**
|
||||
* @deprecated Use {@link RouterBlueprint} instead.
|
||||
*/
|
||||
export const componentDataRef = RouterBlueprint.dataRefs.component;
|
||||
}
|
||||
@@ -1,47 +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 React from 'react';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { createSignInPageExtension } from './createSignInPageExtension';
|
||||
import { coreExtensionData, createExtension } from '../wiring';
|
||||
|
||||
describe('createSignInPageExtension', () => {
|
||||
it('renders a sign-in page', async () => {
|
||||
const SignInPage = createSignInPageExtension({
|
||||
name: 'test',
|
||||
loader: async () => () => <div data-testid="sign-in-page" />,
|
||||
});
|
||||
|
||||
createExtensionTester(
|
||||
createExtension({
|
||||
name: 'dummy',
|
||||
attachTo: { id: 'ignored', input: 'ignored' },
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory: () => ({ element: <div /> }),
|
||||
}),
|
||||
)
|
||||
.add(SignInPage)
|
||||
.render();
|
||||
|
||||
await expect(
|
||||
screen.findByTestId('sign-in-page'),
|
||||
).resolves.toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,88 +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 React, { ComponentType, lazy } from 'react';
|
||||
import { ExtensionBoundary } from '../components';
|
||||
import { PortableSchema } from '../schema';
|
||||
import {
|
||||
createExtension,
|
||||
ResolvedExtensionInputs,
|
||||
AnyExtensionInputMap,
|
||||
ExtensionDefinition,
|
||||
} from '../wiring';
|
||||
import { Expand } from '../types';
|
||||
import { SignInPageProps } from '@backstage/core-plugin-api';
|
||||
import { SignInPageBlueprint } from '../blueprints';
|
||||
|
||||
/**
|
||||
*
|
||||
* @public
|
||||
* @deprecated Use {@link SignInPageBlueprint} instead.
|
||||
*/
|
||||
export function createSignInPageExtension<
|
||||
TConfig extends {},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<ComponentType<SignInPageProps>>;
|
||||
}): ExtensionDefinition<TConfig> {
|
||||
return createExtension({
|
||||
kind: 'sign-in-page',
|
||||
namespace: options?.namespace,
|
||||
name: options?.name,
|
||||
attachTo: options.attachTo ?? { id: 'app/root', input: 'signInPage' },
|
||||
configSchema: options.configSchema,
|
||||
inputs: options.inputs,
|
||||
disabled: options.disabled,
|
||||
output: {
|
||||
component: createSignInPageExtension.componentDataRef,
|
||||
},
|
||||
factory({ config, inputs, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ config, inputs })
|
||||
.then(component => ({ default: component })),
|
||||
);
|
||||
|
||||
return {
|
||||
component: props => (
|
||||
<ExtensionBoundary node={node} routable>
|
||||
<ExtensionComponent {...props} />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link SignInPageBlueprint} instead.
|
||||
*/
|
||||
export namespace createSignInPageExtension {
|
||||
/**
|
||||
* @deprecated Use {@link SignInPageBlueprint} instead.
|
||||
*/
|
||||
export const componentDataRef = SignInPageBlueprint.dataRefs.component;
|
||||
}
|
||||
@@ -1,44 +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 { ThemeBlueprint } from '../blueprints/ThemeBlueprint';
|
||||
import { createExtension } from '../wiring';
|
||||
import { AppTheme } from '@backstage/core-plugin-api';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link ThemeBlueprint} instead.
|
||||
*/
|
||||
export function createThemeExtension(theme: AppTheme) {
|
||||
return createExtension({
|
||||
kind: 'theme',
|
||||
namespace: 'app',
|
||||
name: theme.id,
|
||||
attachTo: { id: 'app', input: 'themes' },
|
||||
output: {
|
||||
theme: ThemeBlueprint.dataRefs.theme,
|
||||
},
|
||||
factory: () => ({ theme }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link ThemeBlueprint} instead.
|
||||
*/
|
||||
export namespace createThemeExtension {
|
||||
export const themeDataRef = ThemeBlueprint.dataRefs.theme;
|
||||
}
|
||||
@@ -1,137 +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 {
|
||||
createTranslationRef,
|
||||
createTranslationMessages,
|
||||
createTranslationResource,
|
||||
} from '@backstage/core-plugin-api/alpha';
|
||||
import { createTranslationExtension } from './createTranslationExtension';
|
||||
|
||||
const translationRef = createTranslationRef({
|
||||
id: 'test',
|
||||
messages: {
|
||||
a: 'a',
|
||||
b: 'b',
|
||||
},
|
||||
});
|
||||
|
||||
describe('createTranslationExtension', () => {
|
||||
it('creates a translation message extension', () => {
|
||||
const messages = createTranslationMessages({
|
||||
ref: translationRef,
|
||||
messages: {
|
||||
a: 'A',
|
||||
},
|
||||
});
|
||||
const extension = createTranslationExtension({ resource: messages });
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'translation',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'app', input: 'translations' },
|
||||
disabled: false,
|
||||
override: expect.any(Function),
|
||||
inputs: {},
|
||||
output: {
|
||||
resource: createTranslationExtension.translationDataRef,
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
|
||||
expect((extension as any).factory({} as any)).toEqual({
|
||||
resource: messages,
|
||||
});
|
||||
});
|
||||
|
||||
it('creates a translation resource extension', () => {
|
||||
const resource = createTranslationResource({
|
||||
ref: translationRef,
|
||||
translations: {
|
||||
sv: () =>
|
||||
Promise.resolve({
|
||||
default: createTranslationMessages({
|
||||
ref: translationRef,
|
||||
messages: {
|
||||
a: 'Ä',
|
||||
b: 'Ö',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
});
|
||||
const extension = createTranslationExtension({ resource });
|
||||
|
||||
expect(extension).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'translation',
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'app', input: 'translations' },
|
||||
disabled: false,
|
||||
override: expect.any(Function),
|
||||
inputs: {},
|
||||
output: {
|
||||
resource: createTranslationExtension.translationDataRef,
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
|
||||
expect((extension as any).factory({} as any)).toEqual({ resource });
|
||||
});
|
||||
|
||||
it('creates a translation resource extension with a name', () => {
|
||||
expect(
|
||||
createTranslationExtension({
|
||||
name: 'sv',
|
||||
resource: createTranslationResource({
|
||||
ref: translationRef,
|
||||
translations: {
|
||||
sv: () =>
|
||||
Promise.resolve({
|
||||
default: createTranslationMessages({
|
||||
ref: translationRef,
|
||||
messages: {
|
||||
a: 'Ä',
|
||||
b: 'Ö',
|
||||
},
|
||||
}),
|
||||
}),
|
||||
},
|
||||
}),
|
||||
}),
|
||||
).toEqual({
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: 'v1',
|
||||
kind: 'translation',
|
||||
namespace: 'test',
|
||||
override: expect.any(Function),
|
||||
name: 'sv',
|
||||
attachTo: { id: 'app', input: 'translations' },
|
||||
disabled: false,
|
||||
inputs: {},
|
||||
output: {
|
||||
resource: createTranslationExtension.translationDataRef,
|
||||
},
|
||||
factory: expect.any(Function),
|
||||
toString: expect.any(Function),
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -1,47 +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 { TranslationBlueprint } from '../blueprints/TranslationBlueprint';
|
||||
import { TranslationMessages, TranslationResource } from '../translation';
|
||||
import { createExtension } from '../wiring';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link TranslationBlueprint} instead.
|
||||
*/
|
||||
export function createTranslationExtension(options: {
|
||||
name?: string;
|
||||
resource: TranslationResource | TranslationMessages;
|
||||
}) {
|
||||
return createExtension({
|
||||
kind: 'translation',
|
||||
namespace: options.resource.id,
|
||||
name: options.name,
|
||||
attachTo: { id: 'app', input: 'translations' },
|
||||
output: {
|
||||
resource: TranslationBlueprint.dataRefs.translation,
|
||||
},
|
||||
factory: () => ({ resource: options.resource }),
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use {@link TranslationBlueprint} instead.
|
||||
*/
|
||||
export namespace createTranslationExtension {
|
||||
export const translationDataRef = TranslationBlueprint.dataRefs.translation;
|
||||
}
|
||||
@@ -14,14 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createApiExtension } from './createApiExtension';
|
||||
export { createAppRootElementExtension } from './createAppRootElementExtension';
|
||||
export { createAppRootWrapperExtension } from './createAppRootWrapperExtension';
|
||||
export { createRouterExtension } from './createRouterExtension';
|
||||
export { createPageExtension } from './createPageExtension';
|
||||
export { createNavItemExtension } from './createNavItemExtension';
|
||||
export { createNavLogoExtension } from './createNavLogoExtension';
|
||||
export { createSignInPageExtension } from './createSignInPageExtension';
|
||||
export { createThemeExtension } from './createThemeExtension';
|
||||
export { createComponentExtension } from './createComponentExtension';
|
||||
export { createTranslationExtension } from './createTranslationExtension';
|
||||
|
||||
@@ -20,8 +20,7 @@ import zodToJsonSchema from 'zod-to-json-schema';
|
||||
import { PortableSchema } from './types';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use the `config.schema` option of `createExtension` instead, or use `createExtensionBlueprint`.
|
||||
* @internal
|
||||
*/
|
||||
export function createSchemaFromZod<TOutput, TInput>(
|
||||
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
|
||||
|
||||
@@ -14,5 +14,4 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
export { createSchemaFromZod } from './createSchemaFromZod';
|
||||
export { type PortableSchema } from './types';
|
||||
|
||||
@@ -21,6 +21,9 @@ import { createExtensionInput } from './createExtensionInput';
|
||||
|
||||
const stringDataRef = createExtensionDataRef<string>().with({ id: 'string' });
|
||||
const numberDataRef = createExtensionDataRef<number>().with({ id: 'number' });
|
||||
const booleanDataRef = createExtensionDataRef<boolean>().with({
|
||||
id: 'boolean',
|
||||
});
|
||||
|
||||
function unused(..._any: any[]) {}
|
||||
|
||||
@@ -29,42 +32,35 @@ describe('createExtension', () => {
|
||||
const baseConfig = {
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringDataRef,
|
||||
},
|
||||
output: [stringDataRef],
|
||||
};
|
||||
const extension = createExtension({
|
||||
...baseConfig,
|
||||
factory() {
|
||||
return {
|
||||
foo: 'bar',
|
||||
};
|
||||
return [stringDataRef('bar')];
|
||||
},
|
||||
});
|
||||
expect(extension).toMatchObject({ version: 'v1', namespace: 'test' });
|
||||
expect(extension).toMatchObject({ version: 'v2', namespace: 'test' });
|
||||
|
||||
// When declared as an error function without a block the TypeScript errors
|
||||
// are a more specific and will often point at the property that is problematic.
|
||||
// Member arrow function declaration
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => [
|
||||
stringDataRef(
|
||||
// @ts-expect-error
|
||||
3,
|
||||
),
|
||||
],
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 3,
|
||||
}),
|
||||
factory: () => [numberDataRef(3)],
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({
|
||||
bar: 'bar',
|
||||
}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({}),
|
||||
factory: () => [],
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
@@ -79,39 +75,37 @@ describe('createExtension', () => {
|
||||
'bar',
|
||||
});
|
||||
|
||||
// When declared as a function with a block the TypeScript error will instead
|
||||
// be tied to the factory function declaration itself, but the error messages
|
||||
// is still helpful and points to part of the return type that is problematic.
|
||||
// Method declaration
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {
|
||||
foo: 3,
|
||||
};
|
||||
return [
|
||||
stringDataRef(
|
||||
// @ts-expect-error
|
||||
3,
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory() {
|
||||
return [numberDataRef(3)];
|
||||
},
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory() {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {
|
||||
bar: 'bar',
|
||||
};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory() {
|
||||
return {};
|
||||
return;
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
@@ -122,36 +116,37 @@ describe('createExtension', () => {
|
||||
},
|
||||
});
|
||||
|
||||
// Member function declaration
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {
|
||||
foo: 3,
|
||||
};
|
||||
return [
|
||||
stringDataRef(
|
||||
// @ts-expect-error
|
||||
3,
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => {
|
||||
return [numberDataRef(3)];
|
||||
},
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => {
|
||||
return [];
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {
|
||||
bar: 'bar',
|
||||
};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {};
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
// @ts-expect-error
|
||||
factory: () => {
|
||||
return {};
|
||||
return;
|
||||
},
|
||||
});
|
||||
createExtension({
|
||||
@@ -167,52 +162,27 @@ describe('createExtension', () => {
|
||||
const baseConfig = {
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
output: {
|
||||
foo: stringDataRef,
|
||||
bar: stringDataRef.optional(),
|
||||
},
|
||||
output: [stringDataRef, numberDataRef.optional()],
|
||||
};
|
||||
const extension = createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 'bar',
|
||||
}),
|
||||
factory: () => [stringDataRef('bar')],
|
||||
});
|
||||
expect(extension).toMatchObject({ version: 'v1', namespace: 'test' });
|
||||
expect(extension).toMatchObject({ version: 'v2', namespace: 'test' });
|
||||
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 'bar',
|
||||
bar: 'baz',
|
||||
}),
|
||||
factory: () => [stringDataRef('bar'), numberDataRef(3)],
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 3,
|
||||
}),
|
||||
factory: () => [numberDataRef(3)],
|
||||
});
|
||||
// @ts-expect-error
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () => ({
|
||||
foo: 'bar',
|
||||
bar: 3,
|
||||
}),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({ bar: 'bar' }),
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
factory: () =>
|
||||
// @ts-expect-error
|
||||
({}),
|
||||
factory: () => [],
|
||||
});
|
||||
createExtension({
|
||||
...baseConfig,
|
||||
@@ -238,57 +208,48 @@ describe('createExtension', () => {
|
||||
namespace: 'test',
|
||||
attachTo: { id: 'root', input: 'default' },
|
||||
inputs: {
|
||||
mixed: createExtensionInput({
|
||||
required: stringDataRef,
|
||||
optional: stringDataRef.optional(),
|
||||
}),
|
||||
onlyRequired: createExtensionInput({
|
||||
required: stringDataRef,
|
||||
}),
|
||||
onlyOptional: createExtensionInput({
|
||||
optional: stringDataRef.optional(),
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
foo: stringDataRef,
|
||||
mixed: createExtensionInput([stringDataRef, numberDataRef.optional()]),
|
||||
onlyRequired: createExtensionInput([stringDataRef]),
|
||||
onlyOptional: createExtensionInput([stringDataRef.optional()]),
|
||||
},
|
||||
output: [stringDataRef],
|
||||
factory({ inputs }) {
|
||||
const a1: string = inputs.mixed?.[0].output.required;
|
||||
const a1: string = inputs.mixed?.[0].get(stringDataRef);
|
||||
// @ts-expect-error
|
||||
const a2: number = inputs.mixed?.[0].output.required;
|
||||
const a2: number = inputs.mixed?.[0].get(stringDataRef);
|
||||
// @ts-expect-error
|
||||
const a3: any = inputs.mixed?.[0].output.nonExistent;
|
||||
const a3: any = inputs.mixed?.[0].get(booleanDataRef);
|
||||
unused(a1, a2, a3);
|
||||
|
||||
const b1: string | undefined = inputs.mixed?.[0].output.optional;
|
||||
const b1: number | undefined = inputs.mixed?.[0].get(numberDataRef);
|
||||
// @ts-expect-error
|
||||
const b2: string = inputs.mixed?.[0].output.optional;
|
||||
const b2: string = inputs.mixed?.[0].get(numberDataRef);
|
||||
// @ts-expect-error
|
||||
const b3: number = inputs.mixed?.[0].output.optional;
|
||||
const b3: number = inputs.mixed?.[0].get(numberDataRef);
|
||||
// @ts-expect-error
|
||||
const b4: number | undefined = inputs.mixed?.[0].output.optional;
|
||||
const b4: string | undefined = inputs.mixed?.[0].get(numberDataRef);
|
||||
unused(b1, b2, b3, b4);
|
||||
|
||||
const c1: string = inputs.onlyRequired?.[0].output.required;
|
||||
const c1: string = inputs.onlyRequired?.[0].get(stringDataRef);
|
||||
// @ts-expect-error
|
||||
const c2: number = inputs.onlyRequired?.[0].output.required;
|
||||
const c2: number = inputs.onlyRequired?.[0].get(stringDataRef);
|
||||
unused(c1, c2);
|
||||
|
||||
const d1: string | undefined = inputs.onlyOptional?.[0].output.optional;
|
||||
const d1: string | undefined =
|
||||
inputs.onlyOptional?.[0].get(stringDataRef);
|
||||
// @ts-expect-error
|
||||
const d2: string = inputs.onlyOptional?.[0].output.optional;
|
||||
const d2: string = inputs.onlyOptional?.[0].get(stringDataRef);
|
||||
// @ts-expect-error
|
||||
const d3: number = inputs.onlyOptional?.[0].output.optional;
|
||||
const d3: number = inputs.onlyOptional?.[0].get(stringDataRef);
|
||||
// @ts-expect-error
|
||||
const d4: number | undefined = inputs.onlyOptional?.[0].output.optional;
|
||||
const d4: number | undefined =
|
||||
inputs.onlyOptional?.[0].get(stringDataRef);
|
||||
unused(d1, d2, d3, d4);
|
||||
|
||||
return {
|
||||
foo: 'bar',
|
||||
};
|
||||
return [stringDataRef('bar')];
|
||||
},
|
||||
});
|
||||
expect(extension).toMatchObject({ version: 'v1', namespace: 'test' });
|
||||
expect(extension).toMatchObject({ version: 'v2', namespace: 'test' });
|
||||
expect(String(extension)).toBe(
|
||||
'ExtensionDefinition{namespace=test,attachTo=root@default}',
|
||||
);
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
*/
|
||||
|
||||
import { AppNode } from '../apis';
|
||||
import { PortableSchema, createSchemaFromZod } from '../schema';
|
||||
import { PortableSchema } from '../schema';
|
||||
import { Expand } from '../types';
|
||||
import {
|
||||
ResolveInputValueOverrides,
|
||||
@@ -29,46 +29,9 @@ import {
|
||||
AnyExtensionDataRef,
|
||||
ExtensionDataValue,
|
||||
} from './createExtensionDataRef';
|
||||
import { ExtensionInput, LegacyExtensionInput } from './createExtensionInput';
|
||||
import { ExtensionInput } from './createExtensionInput';
|
||||
import { z } from 'zod';
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Extension data maps will be removed.
|
||||
*/
|
||||
export type AnyExtensionDataMap = {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated This type will be removed.
|
||||
*/
|
||||
export type AnyExtensionInputMap = {
|
||||
[inputName in string]: LegacyExtensionInput<
|
||||
AnyExtensionDataMap,
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
};
|
||||
|
||||
/**
|
||||
* Converts an extension data map into the matching concrete data values type.
|
||||
* @public
|
||||
* @deprecated Extension data maps will be removed.
|
||||
*/
|
||||
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'];
|
||||
};
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
|
||||
/**
|
||||
* Convert a single extension input into a matching resolved input.
|
||||
@@ -80,11 +43,6 @@ export type ResolvedExtensionInput<
|
||||
? {
|
||||
node: AppNode;
|
||||
} & ExtensionDataContainer<TExtensionInput['extensionData'][number]>
|
||||
: TExtensionInput['extensionData'] extends AnyExtensionDataMap
|
||||
? {
|
||||
node: AppNode;
|
||||
output: ExtensionDataValues<TExtensionInput['extensionData']>;
|
||||
}
|
||||
: never;
|
||||
|
||||
/**
|
||||
@@ -93,7 +51,7 @@ export type ResolvedExtensionInput<
|
||||
*/
|
||||
export type ResolvedExtensionInputs<
|
||||
TInputs extends {
|
||||
[name in string]: ExtensionInput<any, any> | LegacyExtensionInput<any, any>;
|
||||
[name in string]: ExtensionInput<any, any>;
|
||||
},
|
||||
> = {
|
||||
[InputName in keyof TInputs]: false extends TInputs[InputName]['config']['singleton']
|
||||
@@ -103,31 +61,6 @@ export type ResolvedExtensionInputs<
|
||||
: Expand<ResolvedExtensionInput<TInputs[InputName]> | undefined>;
|
||||
};
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated This way of structuring the options is deprecated, this type will be removed in the future
|
||||
*/
|
||||
export interface LegacyCreateExtensionOptions<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
> {
|
||||
kind?: string;
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
output: TOutput;
|
||||
configSchema?: PortableSchema<TConfig, TConfigInput>;
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}): Expand<ExtensionDataValues<TOutput>>;
|
||||
}
|
||||
|
||||
type ToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
|
||||
k: infer I,
|
||||
) => void
|
||||
@@ -326,13 +259,27 @@ export type InternalExtensionDefinition<
|
||||
(
|
||||
| {
|
||||
readonly version: 'v1';
|
||||
readonly inputs: AnyExtensionInputMap;
|
||||
readonly output: AnyExtensionDataMap;
|
||||
readonly inputs: {
|
||||
[inputName in string]: {
|
||||
$$type: '@backstage/ExtensionInput';
|
||||
extensionData: {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
config: { optional: boolean; singleton: boolean };
|
||||
};
|
||||
};
|
||||
readonly output: {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: ResolvedExtensionInputs<AnyExtensionInputMap>;
|
||||
}): ExtensionDataValues<any>;
|
||||
inputs: {
|
||||
[inputName in string]: unknown;
|
||||
};
|
||||
}): {
|
||||
[inputName in string]: unknown;
|
||||
};
|
||||
}
|
||||
| {
|
||||
readonly version: 'v2';
|
||||
@@ -419,23 +366,6 @@ export function createExtension<
|
||||
name: string | undefined extends TName ? undefined : TName;
|
||||
}
|
||||
>;
|
||||
/**
|
||||
* @public
|
||||
* @deprecated - use the array format of `output` instead, see TODO-doc-link
|
||||
*/
|
||||
export function createExtension<
|
||||
TOutput extends AnyExtensionDataMap,
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
>(
|
||||
options: LegacyCreateExtensionOptions<
|
||||
TOutput,
|
||||
TInputs,
|
||||
TConfig,
|
||||
TConfigInput
|
||||
>,
|
||||
): ExtensionDefinition<TConfig, TConfigInput, never, never>;
|
||||
export function createExtension<
|
||||
const TKind extends string | undefined,
|
||||
const TNamespace extends string | undefined,
|
||||
@@ -447,43 +377,31 @@ export function createExtension<
|
||||
{ optional: boolean; singleton: boolean }
|
||||
>;
|
||||
},
|
||||
TLegacyInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
TConfigInput,
|
||||
TConfigSchema extends { [key: string]: (zImpl: typeof z) => z.ZodType },
|
||||
UFactoryOutput extends ExtensionDataValue<any, any>,
|
||||
>(
|
||||
options:
|
||||
| CreateExtensionOptions<
|
||||
TKind,
|
||||
TNamespace,
|
||||
TName,
|
||||
UOutput,
|
||||
TInputs,
|
||||
TConfigSchema,
|
||||
UFactoryOutput
|
||||
>
|
||||
| LegacyCreateExtensionOptions<
|
||||
AnyExtensionDataMap,
|
||||
TLegacyInputs,
|
||||
TConfig,
|
||||
TConfigInput
|
||||
>,
|
||||
options: CreateExtensionOptions<
|
||||
TKind,
|
||||
TNamespace,
|
||||
TName,
|
||||
UOutput,
|
||||
TInputs,
|
||||
TConfigSchema,
|
||||
UFactoryOutput
|
||||
>,
|
||||
): ExtensionDefinition<
|
||||
TConfig &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
}),
|
||||
TConfigInput &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>),
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
},
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>,
|
||||
UOutput,
|
||||
TInputs,
|
||||
{
|
||||
@@ -492,29 +410,20 @@ export function createExtension<
|
||||
name: TName;
|
||||
}
|
||||
> {
|
||||
if ('configSchema' in options && 'config' in options) {
|
||||
throw new Error(`Cannot provide both configSchema and config.schema`);
|
||||
}
|
||||
let configSchema: PortableSchema<any, any> | undefined;
|
||||
if ('configSchema' in options) {
|
||||
configSchema = options.configSchema;
|
||||
}
|
||||
if ('config' in options) {
|
||||
const newConfigSchema = options.config?.schema;
|
||||
configSchema =
|
||||
newConfigSchema &&
|
||||
createSchemaFromZod(innerZ =>
|
||||
innerZ.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(newConfigSchema).map(([k, v]) => [k, v(innerZ)]),
|
||||
),
|
||||
const schemaDeclaration = options.config?.schema;
|
||||
const configSchema =
|
||||
schemaDeclaration &&
|
||||
createSchemaFromZod(innerZ =>
|
||||
innerZ.object(
|
||||
Object.fromEntries(
|
||||
Object.entries(schemaDeclaration).map(([k, v]) => [k, v(innerZ)]),
|
||||
),
|
||||
);
|
||||
}
|
||||
),
|
||||
);
|
||||
|
||||
return {
|
||||
$$type: '@backstage/ExtensionDefinition',
|
||||
version: Symbol.iterator in options.output ? 'v2' : 'v1',
|
||||
version: 'v2',
|
||||
kind: options.kind,
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
@@ -687,22 +596,18 @@ export function createExtension<
|
||||
} as CreateExtensionOptions<TKind, TNamespace, TName, AnyExtensionDataRef extends UNewOutput ? UOutput : UNewOutput, TInputs & TExtraInputs, TConfigSchema & TExtensionConfigSchema, UOverrideFactoryOutput>);
|
||||
},
|
||||
} as InternalExtensionDefinition<
|
||||
TConfig &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<
|
||||
ReturnType<TConfigSchema[key]>
|
||||
>;
|
||||
}),
|
||||
TConfigInput &
|
||||
(string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>),
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: {
|
||||
[key in keyof TConfigSchema]: z.infer<ReturnType<TConfigSchema[key]>>;
|
||||
},
|
||||
string extends keyof TConfigSchema
|
||||
? {}
|
||||
: z.input<
|
||||
z.ZodObject<{
|
||||
[key in keyof TConfigSchema]: ReturnType<TConfigSchema[key]>;
|
||||
}>
|
||||
>,
|
||||
UOutput,
|
||||
TInputs,
|
||||
{
|
||||
|
||||
@@ -15,11 +15,7 @@
|
||||
*/
|
||||
|
||||
import { createExtensionDataRef } from './createExtensionDataRef';
|
||||
import {
|
||||
ExtensionInput,
|
||||
LegacyExtensionInput,
|
||||
createExtensionInput,
|
||||
} from './createExtensionInput';
|
||||
import { ExtensionInput, createExtensionInput } from './createExtensionInput';
|
||||
|
||||
const stringDataRef = createExtensionDataRef<string>().with({ id: 'str' });
|
||||
const numberDataRef = createExtensionDataRef<number>().with({ id: 'num' });
|
||||
@@ -130,40 +126,4 @@ describe('createExtensionInput', () => {
|
||||
createExtensionInput([stringDataRef, stringDataRef], { singleton: true }),
|
||||
).toThrow("ExtensionInput may not have duplicate data refs: 'str'");
|
||||
});
|
||||
|
||||
describe('old api', () => {
|
||||
it('should create a regular input', () => {
|
||||
const input = createExtensionInput({
|
||||
str: stringDataRef,
|
||||
num: numberDataRef,
|
||||
});
|
||||
expect(input).toEqual({
|
||||
$$type: '@backstage/ExtensionInput',
|
||||
extensionData: { str: stringDataRef, num: numberDataRef },
|
||||
config: { singleton: false, optional: false },
|
||||
});
|
||||
|
||||
const x1: LegacyExtensionInput<
|
||||
{ str: typeof stringDataRef; num: typeof numberDataRef },
|
||||
{ singleton: false; optional: false }
|
||||
> = input;
|
||||
// @ts-expect-error
|
||||
const x2: LegacyExtensionInput<
|
||||
{ str: typeof numberDataRef; num: typeof stringDataRef }, // switched
|
||||
{ singleton: false; optional: false }
|
||||
> = input;
|
||||
// @ts-expect-error
|
||||
const x3: LegacyExtensionInput<
|
||||
{ str: typeof stringDataRef; num: typeof numberDataRef },
|
||||
{ singleton: true; optional: false }
|
||||
> = input;
|
||||
// @ts-expect-error
|
||||
const x4: LegacyExtensionInput<
|
||||
{ str: typeof stringDataRef; num: typeof numberDataRef },
|
||||
{ singleton: false; optional: true }
|
||||
> = input;
|
||||
|
||||
unused(x1, x2, x3, x4);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { AnyExtensionDataMap } from './createExtension';
|
||||
import { ExtensionDataRef } from './createExtensionDataRef';
|
||||
|
||||
/** @public */
|
||||
@@ -27,36 +26,6 @@ export interface ExtensionInput<
|
||||
config: TConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated This type will be removed. Use `ExtensionInput` instead.
|
||||
*/
|
||||
export interface LegacyExtensionInput<
|
||||
TExtensionDataMap extends AnyExtensionDataMap,
|
||||
TConfig extends { singleton: boolean; optional: boolean },
|
||||
> {
|
||||
$$type: '@backstage/ExtensionInput';
|
||||
extensionData: TExtensionDataMap;
|
||||
config: TConfig;
|
||||
}
|
||||
|
||||
/**
|
||||
* @public
|
||||
* @deprecated Use the following form instead: `createExtensionInput([dataRef1, dataRef2])`
|
||||
*/
|
||||
export function createExtensionInput<
|
||||
TExtensionDataMap extends AnyExtensionDataMap,
|
||||
TConfig extends { singleton?: boolean; optional?: boolean },
|
||||
>(
|
||||
extensionData: TExtensionDataMap,
|
||||
config?: TConfig,
|
||||
): LegacyExtensionInput<
|
||||
TExtensionDataMap,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
>;
|
||||
/** @public */
|
||||
export function createExtensionInput<
|
||||
UExtensionData extends ExtensionDataRef<unknown, string, { optional?: true }>,
|
||||
@@ -73,26 +42,17 @@ export function createExtensionInput<
|
||||
>;
|
||||
export function createExtensionInput<
|
||||
TExtensionData extends ExtensionDataRef<unknown, string, { optional?: true }>,
|
||||
TExtensionDataMap extends AnyExtensionDataMap,
|
||||
TConfig extends { singleton?: boolean; optional?: boolean },
|
||||
>(
|
||||
extensionData: Array<TExtensionData> | TExtensionDataMap,
|
||||
extensionData: Array<TExtensionData>,
|
||||
config?: TConfig,
|
||||
):
|
||||
| LegacyExtensionInput<
|
||||
TExtensionDataMap,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
>
|
||||
| ExtensionInput<
|
||||
TExtensionData,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
> {
|
||||
): ExtensionInput<
|
||||
TExtensionData,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
> {
|
||||
if (process.env.NODE_ENV !== 'production') {
|
||||
if (Array.isArray(extensionData)) {
|
||||
const seen = new Set();
|
||||
@@ -124,19 +84,11 @@ export function createExtensionInput<
|
||||
? true
|
||||
: false,
|
||||
},
|
||||
} as
|
||||
| LegacyExtensionInput<
|
||||
TExtensionDataMap,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
>
|
||||
| ExtensionInput<
|
||||
TExtensionData,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
>;
|
||||
} as ExtensionInput<
|
||||
TExtensionData,
|
||||
{
|
||||
singleton: TConfig['singleton'] extends true ? true : false;
|
||||
optional: TConfig['optional'] extends true ? true : false;
|
||||
}
|
||||
>;
|
||||
}
|
||||
|
||||
@@ -40,22 +40,22 @@ describe('createExtensionOverrides', () => {
|
||||
createExtension({
|
||||
name: 'a',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
output: [],
|
||||
factory: () => [],
|
||||
}),
|
||||
createExtension({
|
||||
namespace: 'b',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
output: [],
|
||||
factory: () => [],
|
||||
}),
|
||||
createExtension({
|
||||
kind: 'k',
|
||||
namespace: 'c',
|
||||
name: 'n',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
output: [],
|
||||
factory: () => [],
|
||||
}),
|
||||
],
|
||||
}),
|
||||
@@ -74,9 +74,9 @@ describe('createExtensionOverrides', () => {
|
||||
"factory": [Function],
|
||||
"id": "a",
|
||||
"inputs": {},
|
||||
"output": {},
|
||||
"output": [],
|
||||
"toString": [Function],
|
||||
"version": "v1",
|
||||
"version": "v2",
|
||||
},
|
||||
{
|
||||
"$$type": "@backstage/Extension",
|
||||
@@ -89,9 +89,9 @@ describe('createExtensionOverrides', () => {
|
||||
"factory": [Function],
|
||||
"id": "b",
|
||||
"inputs": {},
|
||||
"output": {},
|
||||
"output": [],
|
||||
"toString": [Function],
|
||||
"version": "v1",
|
||||
"version": "v2",
|
||||
},
|
||||
{
|
||||
"$$type": "@backstage/Extension",
|
||||
@@ -104,9 +104,9 @@ describe('createExtensionOverrides', () => {
|
||||
"factory": [Function],
|
||||
"id": "k:c/n",
|
||||
"inputs": {},
|
||||
"output": {},
|
||||
"output": [],
|
||||
"toString": [Function],
|
||||
"version": "v1",
|
||||
"version": "v2",
|
||||
},
|
||||
],
|
||||
"featureFlags": [],
|
||||
@@ -122,8 +122,8 @@ describe('createExtensionOverrides', () => {
|
||||
createExtension({
|
||||
namespace: 'a',
|
||||
attachTo: { id: 'app', input: 'apis' },
|
||||
output: {},
|
||||
factory: () => ({}),
|
||||
output: [],
|
||||
factory: () => [],
|
||||
}),
|
||||
],
|
||||
});
|
||||
|
||||
@@ -17,7 +17,6 @@
|
||||
import React from 'react';
|
||||
import { createApp } from '@backstage/frontend-app-api';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { createSchemaFromZod } from '../schema/createSchemaFromZod';
|
||||
import { createFrontendPlugin } from './createFrontendPlugin';
|
||||
import { JsonObject } from '@backstage/types';
|
||||
import { createExtension } from './createExtension';
|
||||
@@ -43,14 +42,14 @@ const Extension1 = createExtension({
|
||||
const Extension2 = createExtension({
|
||||
name: '2',
|
||||
attachTo: { id: 'test/output', input: 'names' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
output: [nameExtensionDataRef],
|
||||
config: {
|
||||
schema: {
|
||||
name: z => z.string().default('extension-2'),
|
||||
},
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('extension-2') }),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
return [nameExtensionDataRef(config.name)];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -58,45 +57,45 @@ const Extension3 = createExtension({
|
||||
name: '3',
|
||||
attachTo: { id: 'test/output', input: 'names' },
|
||||
inputs: {
|
||||
addons: createExtensionInput({
|
||||
name: nameExtensionDataRef,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
addons: createExtensionInput([nameExtensionDataRef]),
|
||||
},
|
||||
output: [nameExtensionDataRef],
|
||||
factory({ inputs }) {
|
||||
return {
|
||||
name: `extension-3:${inputs.addons.map(n => n.output.name).join('-')}`,
|
||||
};
|
||||
return [
|
||||
nameExtensionDataRef(
|
||||
`extension-3:${inputs.addons
|
||||
.map(n => n.get(nameExtensionDataRef))
|
||||
.join('-')}`,
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
const Child = createExtension({
|
||||
name: 'child',
|
||||
attachTo: { id: 'test/3', input: 'addons' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
output: [nameExtensionDataRef],
|
||||
config: {
|
||||
schema: {
|
||||
name: z => z.string().default('child'),
|
||||
},
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('child') }),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
return [nameExtensionDataRef(config.name)];
|
||||
},
|
||||
});
|
||||
|
||||
const Child2 = createExtension({
|
||||
name: 'child2',
|
||||
attachTo: { id: 'test/3', input: 'addons' },
|
||||
output: {
|
||||
name: nameExtensionDataRef,
|
||||
output: [nameExtensionDataRef],
|
||||
config: {
|
||||
schema: {
|
||||
name: z => z.string().default('child2'),
|
||||
},
|
||||
},
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({ name: z.string().default('child2') }),
|
||||
),
|
||||
factory({ config }) {
|
||||
return { name: config.name };
|
||||
return [nameExtensionDataRef(config.name)];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -104,19 +103,19 @@ const outputExtension = createExtension({
|
||||
name: 'output',
|
||||
attachTo: { id: 'app', input: 'root' },
|
||||
inputs: {
|
||||
names: createExtensionInput({
|
||||
name: nameExtensionDataRef,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
names: createExtensionInput([nameExtensionDataRef]),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs }) {
|
||||
return {
|
||||
element: React.createElement('span', {}, [
|
||||
`Names: ${inputs.names.map(n => n.output.name).join(', ')}`,
|
||||
]),
|
||||
};
|
||||
return [
|
||||
coreExtensionData.reactElement(
|
||||
React.createElement('span', {}, [
|
||||
`Names: ${inputs.names
|
||||
.map(n => n.get(nameExtensionDataRef))
|
||||
.join(', ')}`,
|
||||
]),
|
||||
),
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
|
||||
@@ -19,17 +19,12 @@ export {
|
||||
createExtension,
|
||||
type ExtensionDefinition,
|
||||
type CreateExtensionOptions,
|
||||
type ExtensionDataValues,
|
||||
type ResolvedExtensionInput,
|
||||
type ResolvedExtensionInputs,
|
||||
type LegacyCreateExtensionOptions,
|
||||
type AnyExtensionInputMap,
|
||||
type AnyExtensionDataMap,
|
||||
} from './createExtension';
|
||||
export {
|
||||
createExtensionInput,
|
||||
type ExtensionInput,
|
||||
type LegacyExtensionInput,
|
||||
} from './createExtensionInput';
|
||||
export { type ExtensionDataContainer } from './createExtensionDataContainer';
|
||||
export {
|
||||
|
||||
@@ -16,9 +16,6 @@
|
||||
|
||||
import { AppNode } from '../apis';
|
||||
import {
|
||||
AnyExtensionDataMap,
|
||||
AnyExtensionInputMap,
|
||||
ExtensionDataValues,
|
||||
ExtensionDefinition,
|
||||
ResolvedExtensionInputs,
|
||||
toInternalExtensionDefinition,
|
||||
@@ -47,13 +44,27 @@ export type InternalExtension<TConfig, TConfigInput> = Extension<
|
||||
(
|
||||
| {
|
||||
readonly version: 'v1';
|
||||
readonly inputs: AnyExtensionInputMap;
|
||||
readonly output: AnyExtensionDataMap;
|
||||
factory(options: {
|
||||
readonly inputs: {
|
||||
[inputName in string]: {
|
||||
$$type: '@backstage/ExtensionInput';
|
||||
extensionData: {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
config: { optional: boolean; singleton: boolean };
|
||||
};
|
||||
};
|
||||
readonly output: {
|
||||
[name in string]: AnyExtensionDataRef;
|
||||
};
|
||||
factory(context: {
|
||||
node: AppNode;
|
||||
config: TConfig;
|
||||
inputs: ResolvedExtensionInputs<AnyExtensionInputMap>;
|
||||
}): ExtensionDataValues<any>;
|
||||
inputs: {
|
||||
[inputName in string]: unknown;
|
||||
};
|
||||
}): {
|
||||
[inputName in string]: unknown;
|
||||
};
|
||||
}
|
||||
| {
|
||||
readonly version: 'v2';
|
||||
|
||||
@@ -18,10 +18,10 @@ import React, { useCallback } from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import {
|
||||
ApiBlueprint,
|
||||
analyticsApiRef,
|
||||
configApiRef,
|
||||
coreExtensionData,
|
||||
createApiExtension,
|
||||
createApiFactory,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
@@ -64,8 +64,8 @@ describe('createExtensionTester', () => {
|
||||
it("should fail to render an extension that doesn't output a react element", async () => {
|
||||
const extension = createExtension({
|
||||
...defaultDefinition,
|
||||
output: { path: coreExtensionData.routePath },
|
||||
factory: () => ({ path: '/foo' }),
|
||||
output: [coreExtensionData.routePath],
|
||||
factory: () => [coreExtensionData.routePath('/foo')],
|
||||
});
|
||||
const tester = createExtensionTester(extension);
|
||||
expect(() => tester.render()).toThrowErrorMatchingInlineSnapshot(
|
||||
@@ -122,7 +122,7 @@ describe('createExtensionTester', () => {
|
||||
const appTitle = configApi.getOptionalString('app.title');
|
||||
return (
|
||||
<div>
|
||||
<h2>{appTitle ?? 'Backstafe app'}</h2>
|
||||
<h2>{appTitle ?? 'Backstage app'}</h2>
|
||||
<h3>{config.title ?? 'Index page'}</h3>
|
||||
<Link to="/details">See details</Link>
|
||||
</div>
|
||||
@@ -186,12 +186,14 @@ describe('createExtensionTester', () => {
|
||||
// Mocking the analytics api implementation
|
||||
const analyticsApiMock = new MockAnalyticsApi();
|
||||
|
||||
const analyticsApiOverride = createApiExtension({
|
||||
factory: createApiFactory({
|
||||
api: analyticsApiRef,
|
||||
deps: {},
|
||||
factory: () => analyticsApiMock,
|
||||
}),
|
||||
const analyticsApiOverride = ApiBlueprint.make({
|
||||
params: {
|
||||
factory: createApiFactory({
|
||||
api: analyticsApiRef,
|
||||
deps: {},
|
||||
factory: () => analyticsApiMock,
|
||||
}),
|
||||
},
|
||||
});
|
||||
|
||||
const indexPageExtension = createExtension({
|
||||
|
||||
@@ -26,13 +26,13 @@ import {
|
||||
ExtensionDataRef,
|
||||
ExtensionDefinition,
|
||||
IconComponent,
|
||||
NavItemBlueprint,
|
||||
RouteRef,
|
||||
RouterBlueprint,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionInput,
|
||||
createExtensionOverrides,
|
||||
createNavItemExtension,
|
||||
createRouterExtension,
|
||||
useRouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { Config, ConfigReader } from '@backstage/config';
|
||||
@@ -74,30 +74,29 @@ const TestAppNavExtension = createExtension({
|
||||
name: 'nav',
|
||||
attachTo: { id: 'app/layout', input: 'nav' },
|
||||
inputs: {
|
||||
items: createExtensionInput({
|
||||
target: createNavItemExtension.targetDataRef,
|
||||
}),
|
||||
},
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
items: createExtensionInput([NavItemBlueprint.dataRefs.target]),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs }) {
|
||||
return {
|
||||
element: (
|
||||
return [
|
||||
coreExtensionData.reactElement(
|
||||
<nav>
|
||||
<ul>
|
||||
{inputs.items.map((item, index) => (
|
||||
<NavItem
|
||||
key={index}
|
||||
icon={item.output.target.icon}
|
||||
title={item.output.target.title}
|
||||
routeRef={item.output.target.routeRef}
|
||||
/>
|
||||
))}
|
||||
{inputs.items.map((item, index) => {
|
||||
const target = item.get(NavItemBlueprint.dataRefs.target);
|
||||
return (
|
||||
<NavItem
|
||||
key={index}
|
||||
icon={target.icon}
|
||||
title={target.title}
|
||||
routeRef={target.routeRef}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</nav>
|
||||
</nav>,
|
||||
),
|
||||
};
|
||||
];
|
||||
},
|
||||
});
|
||||
|
||||
@@ -253,18 +252,7 @@ export class ExtensionTester<UOutput extends AnyExtensionDataRef> {
|
||||
let subjectOverride;
|
||||
// attaching to app/routes to render as index route
|
||||
if (subjectInternal.version === 'v1') {
|
||||
subjectOverride = createExtension({
|
||||
...subjectInternal,
|
||||
attachTo: { id: 'app/routes', input: 'routes' },
|
||||
output: {
|
||||
...subjectInternal.output,
|
||||
path: coreExtensionData.routePath,
|
||||
},
|
||||
factory: params => ({
|
||||
...subjectInternal.factory(params as any),
|
||||
path: '/',
|
||||
}),
|
||||
});
|
||||
throw new Error('The extension tester does not support v1 extensions');
|
||||
} else if (subjectInternal.version === 'v2') {
|
||||
subjectOverride = createExtension({
|
||||
...subjectInternal,
|
||||
@@ -282,6 +270,7 @@ export class ExtensionTester<UOutput extends AnyExtensionDataRef> {
|
||||
return [...parentOutput, coreExtensionData.routePath('/')];
|
||||
},
|
||||
});
|
||||
(subjectOverride as any).configSchema = subjectInternal.configSchema;
|
||||
} else {
|
||||
throw new Error('Unsupported extension version');
|
||||
}
|
||||
@@ -293,11 +282,13 @@ export class ExtensionTester<UOutput extends AnyExtensionDataRef> {
|
||||
subjectOverride,
|
||||
...this.#extensions.slice(1).map(extension => extension.definition),
|
||||
TestAppNavExtension,
|
||||
createRouterExtension({
|
||||
RouterBlueprint.make({
|
||||
namespace: 'test',
|
||||
Component: ({ children }) => (
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
),
|
||||
params: {
|
||||
Component: ({ children }) => (
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
),
|
||||
},
|
||||
}),
|
||||
],
|
||||
}),
|
||||
|
||||
@@ -29,8 +29,8 @@ import {
|
||||
useRouteRef,
|
||||
createExtensionInput,
|
||||
IconComponent,
|
||||
createNavItemExtension,
|
||||
RouterBlueprint,
|
||||
NavItemBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
/**
|
||||
@@ -86,7 +86,7 @@ export const TestAppNavExtension = createExtension({
|
||||
name: 'nav',
|
||||
attachTo: { id: 'app/layout', input: 'nav' },
|
||||
inputs: {
|
||||
items: createExtensionInput([createNavItemExtension.targetDataRef]),
|
||||
items: createExtensionInput([NavItemBlueprint.dataRefs.target]),
|
||||
},
|
||||
output: [coreExtensionData.reactElement],
|
||||
factory({ inputs }) {
|
||||
@@ -96,7 +96,7 @@ export const TestAppNavExtension = createExtension({
|
||||
<ul>
|
||||
{inputs.items.map((item, index) => {
|
||||
const { icon, title, routeRef } = item.get(
|
||||
createNavItemExtension.targetDataRef,
|
||||
NavItemBlueprint.dataRefs.target,
|
||||
);
|
||||
|
||||
return (
|
||||
|
||||
@@ -5,7 +5,6 @@
|
||||
```ts
|
||||
/// <reference types="react" />
|
||||
|
||||
import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api';
|
||||
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -13,31 +12,10 @@ import { Entity } from '@backstage/catalog-model';
|
||||
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { PortableSchema } from '@backstage/frontend-plugin-api';
|
||||
import { ResolvedExtensionInputs } from '@backstage/frontend-plugin-api';
|
||||
import { ResourcePermission } from '@backstage/plugin-permission-common';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { TranslationRef } from '@backstage/core-plugin-api/alpha';
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
export const catalogExtensionData: {
|
||||
entityContentTitle: ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'catalog.entity-content-title',
|
||||
{}
|
||||
>;
|
||||
entityFilterFunction: ConfigurableExtensionDataRef<
|
||||
(entity: Entity) => boolean,
|
||||
'catalog.entity-filter-function',
|
||||
{}
|
||||
>;
|
||||
entityFilterExpression: ConfigurableExtensionDataRef<
|
||||
string,
|
||||
'catalog.entity-filter-expression',
|
||||
{}
|
||||
>;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export const catalogReactTranslationRef: TranslationRef<
|
||||
'catalog-react',
|
||||
@@ -120,82 +98,6 @@ export function convertLegacyEntityContentExtension(
|
||||
},
|
||||
): ExtensionDefinition<any>;
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
export function createEntityCardExtension<
|
||||
TConfig extends {
|
||||
filter?: string;
|
||||
},
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
filter?:
|
||||
| typeof catalogExtensionData.entityFilterFunction.T
|
||||
| typeof catalogExtensionData.entityFilterExpression.T;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}): ExtensionDefinition<
|
||||
TConfig,
|
||||
TConfig,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
export function createEntityContentExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
defaultPath: string;
|
||||
defaultTitle: string;
|
||||
filter?:
|
||||
| typeof catalogExtensionData.entityFilterFunction.T
|
||||
| typeof catalogExtensionData.entityFilterExpression.T;
|
||||
loader: (options: {
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}): ExtensionDefinition<
|
||||
{
|
||||
title: string;
|
||||
path: string;
|
||||
filter?: string | undefined;
|
||||
},
|
||||
{
|
||||
filter?: string | undefined;
|
||||
title?: string | undefined;
|
||||
path?: string | undefined;
|
||||
},
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @alpha
|
||||
export const EntityCardBlueprint: ExtensionBlueprint<
|
||||
{
|
||||
|
||||
@@ -19,7 +19,10 @@ import {
|
||||
coreExtensionData,
|
||||
createExtensionBlueprint,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { catalogExtensionData } from '../extensions';
|
||||
import {
|
||||
entityFilterFunctionDataRef,
|
||||
entityFilterExpressionDataRef,
|
||||
} from './extensionData';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
@@ -30,12 +33,12 @@ export const EntityCardBlueprint = createExtensionBlueprint({
|
||||
attachTo: { id: 'entity-content:catalog/overview', input: 'cards' },
|
||||
output: [
|
||||
coreExtensionData.reactElement,
|
||||
catalogExtensionData.entityFilterFunction.optional(),
|
||||
catalogExtensionData.entityFilterExpression.optional(),
|
||||
entityFilterFunctionDataRef.optional(),
|
||||
entityFilterExpressionDataRef.optional(),
|
||||
],
|
||||
dataRefs: {
|
||||
filterFunction: catalogExtensionData.entityFilterFunction,
|
||||
filterExpression: catalogExtensionData.entityFilterExpression,
|
||||
filterFunction: entityFilterFunctionDataRef,
|
||||
filterExpression: entityFilterExpressionDataRef,
|
||||
},
|
||||
config: {
|
||||
schema: {
|
||||
@@ -49,19 +52,19 @@ export const EntityCardBlueprint = createExtensionBlueprint({
|
||||
}: {
|
||||
loader: () => Promise<JSX.Element>;
|
||||
filter?:
|
||||
| typeof catalogExtensionData.entityFilterFunction.T
|
||||
| typeof catalogExtensionData.entityFilterExpression.T;
|
||||
| typeof entityFilterFunctionDataRef.T
|
||||
| typeof entityFilterExpressionDataRef.T;
|
||||
},
|
||||
{ node, config },
|
||||
) {
|
||||
yield coreExtensionData.reactElement(ExtensionBoundary.lazy(node, loader));
|
||||
|
||||
if (config.filter) {
|
||||
yield catalogExtensionData.entityFilterExpression(config.filter);
|
||||
yield entityFilterExpressionDataRef(config.filter);
|
||||
} else if (typeof filter === 'string') {
|
||||
yield catalogExtensionData.entityFilterExpression(filter);
|
||||
yield entityFilterExpressionDataRef(filter);
|
||||
} else if (typeof filter === 'function') {
|
||||
yield catalogExtensionData.entityFilterFunction(filter);
|
||||
yield entityFilterFunctionDataRef(filter);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -20,7 +20,11 @@ import {
|
||||
ExtensionBoundary,
|
||||
RouteRef,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { catalogExtensionData } from '../extensions';
|
||||
import {
|
||||
entityContentTitleDataRef,
|
||||
entityFilterFunctionDataRef,
|
||||
entityFilterExpressionDataRef,
|
||||
} from './extensionData';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
@@ -32,15 +36,15 @@ export const EntityContentBlueprint = createExtensionBlueprint({
|
||||
output: [
|
||||
coreExtensionData.reactElement,
|
||||
coreExtensionData.routePath,
|
||||
catalogExtensionData.entityContentTitle,
|
||||
entityContentTitleDataRef,
|
||||
coreExtensionData.routeRef.optional(),
|
||||
catalogExtensionData.entityFilterFunction.optional(),
|
||||
catalogExtensionData.entityFilterExpression.optional(),
|
||||
entityFilterFunctionDataRef.optional(),
|
||||
entityFilterExpressionDataRef.optional(),
|
||||
],
|
||||
dataRefs: {
|
||||
title: catalogExtensionData.entityContentTitle,
|
||||
filterFunction: catalogExtensionData.entityFilterFunction,
|
||||
filterExpression: catalogExtensionData.entityFilterExpression,
|
||||
title: entityContentTitleDataRef,
|
||||
filterFunction: entityFilterFunctionDataRef,
|
||||
filterExpression: entityFilterExpressionDataRef,
|
||||
},
|
||||
config: {
|
||||
schema: {
|
||||
@@ -62,8 +66,8 @@ export const EntityContentBlueprint = createExtensionBlueprint({
|
||||
defaultTitle: string;
|
||||
routeRef?: RouteRef;
|
||||
filter?:
|
||||
| typeof catalogExtensionData.entityFilterFunction.T
|
||||
| typeof catalogExtensionData.entityFilterExpression.T;
|
||||
| typeof entityFilterFunctionDataRef.T
|
||||
| typeof entityFilterExpressionDataRef.T;
|
||||
},
|
||||
{ node, config },
|
||||
) {
|
||||
@@ -74,18 +78,18 @@ export const EntityContentBlueprint = createExtensionBlueprint({
|
||||
|
||||
yield coreExtensionData.routePath(path);
|
||||
|
||||
yield catalogExtensionData.entityContentTitle(title);
|
||||
yield entityContentTitleDataRef(title);
|
||||
|
||||
if (routeRef) {
|
||||
yield coreExtensionData.routeRef(routeRef);
|
||||
}
|
||||
|
||||
if (config.filter) {
|
||||
yield catalogExtensionData.entityFilterExpression(config.filter);
|
||||
yield entityFilterExpressionDataRef(config.filter);
|
||||
} else if (typeof filter === 'string') {
|
||||
yield catalogExtensionData.entityFilterExpression(filter);
|
||||
yield entityFilterExpressionDataRef(filter);
|
||||
} else if (typeof filter === 'function') {
|
||||
yield catalogExtensionData.entityFilterFunction(filter);
|
||||
yield entityFilterFunctionDataRef(filter);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* 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 { Entity } from '@backstage/catalog-model';
|
||||
import { createExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
|
||||
/** @internal */
|
||||
export const entityContentTitleDataRef = createExtensionDataRef<string>().with({
|
||||
id: 'catalog.entity-content-title',
|
||||
});
|
||||
|
||||
/** @internal */
|
||||
export const entityFilterFunctionDataRef = createExtensionDataRef<
|
||||
(entity: Entity) => boolean
|
||||
>().with({ id: 'catalog.entity-filter-function' });
|
||||
|
||||
/** @internal */
|
||||
export const entityFilterExpressionDataRef =
|
||||
createExtensionDataRef<string>().with({
|
||||
id: 'catalog.entity-filter-expression',
|
||||
});
|
||||
@@ -1,213 +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 {
|
||||
AnyExtensionInputMap,
|
||||
ExtensionBoundary,
|
||||
PortableSchema,
|
||||
ResolvedExtensionInputs,
|
||||
RouteRef,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
createExtensionDataRef,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import React, { lazy } from 'react';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
// eslint-disable-next-line @backstage/no-relative-monorepo-imports
|
||||
import { Expand } from '../../../../packages/frontend-plugin-api/src/types';
|
||||
|
||||
export { useEntityPermission } from '../hooks/useEntityPermission';
|
||||
export { isOwnerOf } from '../utils';
|
||||
export * from '../translation';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @deprecated use `dataRefs` property on either {@link EntityCardBlueprint} or {@link EntityContentBlueprint} instead
|
||||
*/
|
||||
export const catalogExtensionData = {
|
||||
entityContentTitle: createExtensionDataRef<string>().with({
|
||||
id: 'catalog.entity-content-title',
|
||||
}),
|
||||
entityFilterFunction: createExtensionDataRef<
|
||||
(entity: Entity) => boolean
|
||||
>().with({ id: 'catalog.entity-filter-function' }),
|
||||
entityFilterExpression: createExtensionDataRef<string>().with({
|
||||
id: 'catalog.entity-filter-expression',
|
||||
}),
|
||||
};
|
||||
|
||||
// TODO: Figure out how to merge with provided config schema
|
||||
/**
|
||||
* @alpha
|
||||
* @deprecated use {@link EntityCardBlueprint} instead
|
||||
*/
|
||||
export function createEntityCardExtension<
|
||||
TConfig extends { filter?: string },
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
filter?:
|
||||
| typeof catalogExtensionData.entityFilterFunction.T
|
||||
| typeof catalogExtensionData.entityFilterExpression.T;
|
||||
loader: (options: {
|
||||
config: TConfig;
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}) {
|
||||
const configSchema =
|
||||
'configSchema' in options
|
||||
? options.configSchema
|
||||
: (createSchemaFromZod(z =>
|
||||
z.object({
|
||||
filter: z.string().optional(),
|
||||
}),
|
||||
) as PortableSchema<TConfig>);
|
||||
return createExtension({
|
||||
kind: 'entity-card',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'entity-content:catalog/overview',
|
||||
input: 'cards',
|
||||
},
|
||||
disabled: options.disabled,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
filterFunction: catalogExtensionData.entityFilterFunction.optional(),
|
||||
filterExpression: catalogExtensionData.entityFilterExpression.optional(),
|
||||
},
|
||||
inputs: options.inputs,
|
||||
configSchema,
|
||||
factory({ config, inputs, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ inputs, config })
|
||||
.then(element => ({ default: () => element })),
|
||||
);
|
||||
|
||||
return {
|
||||
element: (
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
...mergeFilters({ config, options }),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @deprecated use {@link EntityContentBlueprint} instead
|
||||
*/
|
||||
export function createEntityContentExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: { id: string; input: string };
|
||||
disabled?: boolean;
|
||||
inputs?: TInputs;
|
||||
routeRef?: RouteRef;
|
||||
defaultPath: string;
|
||||
defaultTitle: string;
|
||||
filter?:
|
||||
| typeof catalogExtensionData.entityFilterFunction.T
|
||||
| typeof catalogExtensionData.entityFilterExpression.T;
|
||||
loader: (options: {
|
||||
inputs: Expand<ResolvedExtensionInputs<TInputs>>;
|
||||
}) => Promise<JSX.Element>;
|
||||
}) {
|
||||
return createExtension({
|
||||
kind: 'entity-content',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'page:catalog/entity',
|
||||
input: 'contents',
|
||||
},
|
||||
disabled: options.disabled,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
path: coreExtensionData.routePath,
|
||||
routeRef: coreExtensionData.routeRef.optional(),
|
||||
title: catalogExtensionData.entityContentTitle,
|
||||
filterFunction: catalogExtensionData.entityFilterFunction.optional(),
|
||||
filterExpression: catalogExtensionData.entityFilterExpression.optional(),
|
||||
},
|
||||
inputs: options.inputs,
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z.object({
|
||||
path: z.string().default(options.defaultPath),
|
||||
title: z.string().default(options.defaultTitle),
|
||||
filter: z.string().optional(),
|
||||
}),
|
||||
),
|
||||
factory({ config, inputs, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ inputs })
|
||||
.then(element => ({ default: () => element })),
|
||||
);
|
||||
|
||||
return {
|
||||
path: config.path,
|
||||
title: config.title,
|
||||
routeRef: options.routeRef,
|
||||
element: (
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
...mergeFilters({ config, options }),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Decides what filter outputs to produce, given some options and config
|
||||
*/
|
||||
function mergeFilters(inputs: {
|
||||
options: {
|
||||
filter?:
|
||||
| typeof catalogExtensionData.entityFilterFunction.T
|
||||
| typeof catalogExtensionData.entityFilterExpression.T;
|
||||
};
|
||||
config: {
|
||||
filter?: string;
|
||||
};
|
||||
}): {
|
||||
filterFunction?: typeof catalogExtensionData.entityFilterFunction.T;
|
||||
filterExpression?: typeof catalogExtensionData.entityFilterExpression.T;
|
||||
} {
|
||||
const { options, config } = inputs;
|
||||
if (config.filter) {
|
||||
return { filterExpression: config.filter };
|
||||
} else if (typeof options.filter === 'string') {
|
||||
return { filterExpression: options.filter };
|
||||
} else if (typeof options.filter === 'function') {
|
||||
return { filterFunction: options.filter };
|
||||
}
|
||||
return {};
|
||||
}
|
||||
@@ -15,5 +15,7 @@
|
||||
*/
|
||||
|
||||
export * from './blueprints';
|
||||
export * from './extensions';
|
||||
export * from './converters';
|
||||
export { catalogReactTranslationRef } from '../translation';
|
||||
export { isOwnerOf } from '../utils/isOwnerOf';
|
||||
export { useEntityPermission } from '../hooks/useEntityPermission';
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
|
||||
import { AnyApiFactory } from '@backstage/frontend-plugin-api';
|
||||
import { AnyExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { AnyExtensionInputMap } from '@backstage/frontend-plugin-api';
|
||||
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
|
||||
import { BackstagePlugin } from '@backstage/frontend-plugin-api';
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
@@ -18,7 +17,6 @@ import { ExtensionInput } from '@backstage/frontend-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { IconComponent } from '@backstage/core-plugin-api';
|
||||
import { JSX as JSX_2 } from 'react';
|
||||
import { PortableSchema } from '@backstage/frontend-plugin-api';
|
||||
import { RouteRef } from '@backstage/frontend-plugin-api';
|
||||
import { SearchResultItemExtensionComponent } from '@backstage/plugin-search-react/alpha';
|
||||
import { SearchResultItemExtensionPredicate } from '@backstage/plugin-search-react/alpha';
|
||||
@@ -126,28 +124,6 @@ export const catalogTranslationRef: TranslationRef<
|
||||
}
|
||||
>;
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
export function createCatalogFilterExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
loader: (options: { config: TConfig }) => Promise<JSX.Element>;
|
||||
}): ExtensionDefinition<
|
||||
TConfig,
|
||||
TConfig,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @alpha (undocumented)
|
||||
const _default: BackstagePlugin<
|
||||
{
|
||||
|
||||
@@ -1,66 +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 React, { lazy } from 'react';
|
||||
import {
|
||||
AnyExtensionInputMap,
|
||||
ExtensionBoundary,
|
||||
PortableSchema,
|
||||
coreExtensionData,
|
||||
createExtension,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @deprecated Use {@link CatalogFilterBlueprint} instead
|
||||
*/
|
||||
export function createCatalogFilterExtension<
|
||||
TInputs extends AnyExtensionInputMap,
|
||||
TConfig,
|
||||
>(options: {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
inputs?: TInputs;
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
loader: (options: { config: TConfig }) => Promise<JSX.Element>;
|
||||
}) {
|
||||
return createExtension({
|
||||
kind: 'catalog-filter',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: { id: 'page:catalog', input: 'filters' },
|
||||
inputs: options.inputs ?? {},
|
||||
configSchema: options.configSchema,
|
||||
output: {
|
||||
element: coreExtensionData.reactElement,
|
||||
},
|
||||
factory({ config, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.loader({ config })
|
||||
.then(element => ({ default: () => element })),
|
||||
);
|
||||
|
||||
return {
|
||||
element: (
|
||||
<ExtensionBoundary node={node}>
|
||||
<ExtensionComponent />
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
export { default } from './plugin';
|
||||
export { createCatalogFilterExtension } from './createCatalogFilterExtension';
|
||||
|
||||
export * from './blueprints';
|
||||
export * from './translation';
|
||||
|
||||
@@ -7,9 +7,7 @@
|
||||
|
||||
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
|
||||
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
|
||||
import { ListItemProps } from '@material-ui/core/ListItem';
|
||||
import { PortableSchema } from '@backstage/frontend-plugin-api';
|
||||
import { SearchDocument } from '@backstage/plugin-search-common';
|
||||
import { SearchResult } from '@backstage/plugin-search-common';
|
||||
|
||||
@@ -19,38 +17,6 @@ export type BaseSearchResultListItemProps<T = {}> = T & {
|
||||
result?: SearchDocument;
|
||||
} & Omit<ListItemProps, 'button'>;
|
||||
|
||||
// @alpha @deprecated
|
||||
export function createSearchResultListItemExtension<
|
||||
TConfig extends {
|
||||
noTrack?: boolean;
|
||||
},
|
||||
>(
|
||||
options: SearchResultItemExtensionOptions<TConfig>,
|
||||
): ExtensionDefinition<
|
||||
TConfig,
|
||||
TConfig,
|
||||
never,
|
||||
never,
|
||||
{
|
||||
kind?: string | undefined;
|
||||
namespace?: string | undefined;
|
||||
name?: string | undefined;
|
||||
}
|
||||
>;
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
export namespace createSearchResultListItemExtension {
|
||||
const // @deprecated (undocumented)
|
||||
itemDataRef: ConfigurableExtensionDataRef<
|
||||
{
|
||||
predicate?: SearchResultItemExtensionPredicate | undefined;
|
||||
component: SearchResultItemExtensionComponent;
|
||||
},
|
||||
'search.search-result-list-item.item',
|
||||
{}
|
||||
>;
|
||||
}
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type SearchResultItemExtensionComponent = <
|
||||
P extends BaseSearchResultListItemProps,
|
||||
@@ -58,25 +24,6 @@ export type SearchResultItemExtensionComponent = <
|
||||
props: P,
|
||||
) => JSX.Element | null;
|
||||
|
||||
// @alpha @deprecated (undocumented)
|
||||
export type SearchResultItemExtensionOptions<
|
||||
TConfig extends {
|
||||
noTrack?: boolean;
|
||||
},
|
||||
> = {
|
||||
namespace?: string;
|
||||
name?: string;
|
||||
attachTo?: {
|
||||
id: string;
|
||||
input: string;
|
||||
};
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
component: (options: {
|
||||
config: TConfig;
|
||||
}) => Promise<SearchResultItemExtensionComponent>;
|
||||
predicate?: SearchResultItemExtensionPredicate;
|
||||
};
|
||||
|
||||
// @alpha (undocumented)
|
||||
export type SearchResultItemExtensionPredicate = (
|
||||
result: SearchResult,
|
||||
|
||||
@@ -1,171 +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 {
|
||||
createExtensionInput,
|
||||
createPageExtension,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import { createExtensionTester } from '@backstage/frontend-test-utils';
|
||||
import { SearchResult } from '@backstage/plugin-search-common';
|
||||
import { screen } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
import { createSearchResultListItemExtension } from './extensions';
|
||||
import { BaseSearchResultListItemProps } from './blueprints';
|
||||
|
||||
describe('createSearchResultListItemExtension', () => {
|
||||
it('Should use the correct result component', async () => {
|
||||
type TechDocsSearchResultListItemProps = BaseSearchResultListItemProps<{
|
||||
lineClamp: number;
|
||||
}>;
|
||||
const TechDocsSearchResultItemComponent = (
|
||||
props: TechDocsSearchResultListItemProps,
|
||||
) => (
|
||||
<div>
|
||||
TechDocs - Rank: {props.rank} - Line clamp: {props.lineClamp}
|
||||
</div>
|
||||
);
|
||||
|
||||
const TechDocsSearchResultItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
namespace: 'techdocs',
|
||||
configSchema: createSchemaFromZod(z =>
|
||||
z
|
||||
.object({
|
||||
noTrack: z.boolean().default(true),
|
||||
lineClamp: z.number().default(5),
|
||||
})
|
||||
.default({}),
|
||||
),
|
||||
predicate: result => result.type === 'techdocs',
|
||||
component:
|
||||
async ({ config }) =>
|
||||
props =>
|
||||
<TechDocsSearchResultItemComponent {...props} {...config} />,
|
||||
});
|
||||
|
||||
const ExploreSearchResultItemComponent = (
|
||||
props: BaseSearchResultListItemProps,
|
||||
) => <div>Explore - Rank: {props.rank}</div>;
|
||||
|
||||
const ExploreSearchResultItemExtension =
|
||||
createSearchResultListItemExtension({
|
||||
namespace: 'explore',
|
||||
predicate: result => result.type === 'explore',
|
||||
component: async () => ExploreSearchResultItemComponent,
|
||||
});
|
||||
|
||||
const SearchPageExtension = createPageExtension({
|
||||
namespace: 'search',
|
||||
defaultPath: '/',
|
||||
inputs: {
|
||||
items: createExtensionInput({
|
||||
item: createSearchResultListItemExtension.itemDataRef,
|
||||
}),
|
||||
},
|
||||
loader: async ({ inputs }) => {
|
||||
const results = [
|
||||
{
|
||||
type: 'techdocs',
|
||||
rank: 1,
|
||||
document: {
|
||||
title: 'Title1',
|
||||
text: 'Text1',
|
||||
location: '/location1',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'explore',
|
||||
rank: 2,
|
||||
document: {
|
||||
title: 'Title2',
|
||||
text: 'Text2',
|
||||
location: '/location2',
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'other',
|
||||
rank: 3,
|
||||
document: {
|
||||
title: 'Title3',
|
||||
text: 'Text3',
|
||||
location: '/location3',
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
const DefaultResultItem = (props: BaseSearchResultListItemProps) => (
|
||||
<div>Default - Rank: {props.rank}</div>
|
||||
);
|
||||
|
||||
const getResultItemComponent = (result: SearchResult) => {
|
||||
const value = inputs.items.find(item =>
|
||||
item?.output.item.predicate?.(result),
|
||||
);
|
||||
return value?.output.item.component ?? DefaultResultItem;
|
||||
};
|
||||
|
||||
const Component = () => {
|
||||
return (
|
||||
<div>
|
||||
<h1>Search Page</h1>
|
||||
<ul>
|
||||
{results.map((result, index) => {
|
||||
const SearchResultListItem = getResultItemComponent(result);
|
||||
return (
|
||||
<SearchResultListItem
|
||||
key={index}
|
||||
rank={result.rank}
|
||||
result={result.document}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
return <Component />;
|
||||
},
|
||||
});
|
||||
|
||||
createExtensionTester(SearchPageExtension)
|
||||
.add(TechDocsSearchResultItemExtension, {
|
||||
// TODO(Rugvip): We need to make the config input type available for use here
|
||||
config: {
|
||||
lineClamp: 3,
|
||||
} as any,
|
||||
})
|
||||
.add(ExploreSearchResultItemExtension)
|
||||
.render();
|
||||
|
||||
expect(await screen.findByText(/Search Page/)).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByText(/TechDocs - Rank: 1 - Line clamp: 3/, {
|
||||
exact: false,
|
||||
}),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Explore - Rank: 2/, { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
|
||||
expect(
|
||||
await screen.findByText(/Default - Rank: 3/, { exact: false }),
|
||||
).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
@@ -1,133 +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 React, { lazy } from 'react';
|
||||
import {
|
||||
ExtensionBoundary,
|
||||
PortableSchema,
|
||||
createExtension,
|
||||
createSchemaFromZod,
|
||||
} from '@backstage/frontend-plugin-api';
|
||||
import {
|
||||
SearchResultItemExtensionComponent,
|
||||
SearchResultItemExtensionPredicate,
|
||||
} from './blueprints';
|
||||
import { SearchResultListItemExtension } from '../extensions';
|
||||
import { searchResultListItemDataRef } from './blueprints/types';
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @deprecated Use {@link SearchResultListItemBlueprint} instead
|
||||
*/
|
||||
export type SearchResultItemExtensionOptions<
|
||||
TConfig extends { noTrack?: boolean },
|
||||
> = {
|
||||
/**
|
||||
* The extension namespace.
|
||||
*/
|
||||
namespace?: string;
|
||||
/**
|
||||
* The extension name.
|
||||
*/
|
||||
name?: string;
|
||||
/**
|
||||
* The extension attachment point (e.g., search modal or page).
|
||||
*/
|
||||
attachTo?: { id: string; input: string };
|
||||
/**
|
||||
* Optional extension config schema.
|
||||
*/
|
||||
configSchema?: PortableSchema<TConfig>;
|
||||
/**
|
||||
* The extension component.
|
||||
*/
|
||||
component: (options: {
|
||||
config: TConfig;
|
||||
}) => Promise<SearchResultItemExtensionComponent>;
|
||||
/**
|
||||
* When an extension defines a predicate, it returns true if the result should be rendered by that extension.
|
||||
* Defaults to a predicate that returns true, which means it renders all sorts of results.
|
||||
*/
|
||||
predicate?: SearchResultItemExtensionPredicate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Creates items for the search result list.
|
||||
*
|
||||
* @alpha
|
||||
* @deprecated Use {@link SearchResultListItemBlueprint} instead
|
||||
*/
|
||||
export function createSearchResultListItemExtension<
|
||||
TConfig extends { noTrack?: boolean },
|
||||
>(options: SearchResultItemExtensionOptions<TConfig>) {
|
||||
const configSchema =
|
||||
'configSchema' in options
|
||||
? options.configSchema
|
||||
: (createSchemaFromZod(z =>
|
||||
z.object({
|
||||
noTrack: z.boolean().default(false),
|
||||
}),
|
||||
) as PortableSchema<TConfig>);
|
||||
|
||||
return createExtension({
|
||||
kind: 'search-result-list-item',
|
||||
namespace: options.namespace,
|
||||
name: options.name,
|
||||
attachTo: options.attachTo ?? {
|
||||
id: 'page:search',
|
||||
input: 'items',
|
||||
},
|
||||
configSchema,
|
||||
output: {
|
||||
item: createSearchResultListItemExtension.itemDataRef,
|
||||
},
|
||||
factory({ config, node }) {
|
||||
const ExtensionComponent = lazy(() =>
|
||||
options
|
||||
.component({ config })
|
||||
.then(component => ({ default: component })),
|
||||
) as unknown as SearchResultItemExtensionComponent;
|
||||
|
||||
return {
|
||||
item: {
|
||||
predicate: options.predicate,
|
||||
component: props => (
|
||||
<ExtensionBoundary node={node}>
|
||||
<SearchResultListItemExtension
|
||||
rank={props.rank}
|
||||
result={props.result}
|
||||
noTrack={config.noTrack}
|
||||
>
|
||||
<ExtensionComponent {...props} />
|
||||
</SearchResultListItemExtension>
|
||||
</ExtensionBoundary>
|
||||
),
|
||||
},
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @alpha
|
||||
* @deprecated Use {@link SearchResultListItemBlueprint} instead
|
||||
*/
|
||||
export namespace createSearchResultListItemExtension {
|
||||
/**
|
||||
* @deprecated Use {@link SearchResultListItemBlueprint#dataRefs.item} instead
|
||||
*/
|
||||
export const itemDataRef = searchResultListItemDataRef;
|
||||
}
|
||||
@@ -13,5 +13,4 @@
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
export * from './extensions';
|
||||
export * from './blueprints';
|
||||
|
||||
Reference in New Issue
Block a user