add router to the app root

Signed-off-by: Fredrik Adelöw <freben@gmail.com>
This commit is contained in:
Fredrik Adelöw
2024-01-12 13:58:56 +01:00
parent 2dd5da2c82
commit e8ce05959e
10 changed files with 352 additions and 159 deletions
@@ -687,6 +687,39 @@ export function createRouteRef<
}
>;
// @public
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 (undocumented)
export namespace createRouterExtension {
const // (undocumented)
componentDataRef: ConfigurableExtensionDataRef<
React_2.ComponentType<{
children?: React_2.ReactNode;
}>,
{}
>;
}
// @public (undocumented)
export function createSchemaFromZod<TOutput, TInput>(
schemaCreator: (zImpl: typeof z) => ZodSchema<TOutput, ZodTypeDef, TInput>,
+16 -16
View File
@@ -22,6 +22,21 @@
"prepack": "backstage-cli package prepack",
"postpack": "backstage-cli package postpack"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@material-ui/core": "^4.12.4",
"@types/react": "^16.13.1 || ^17.0.0",
"lodash": "^4.17.21",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.21.4"
},
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"devDependencies": {
"@backstage/cli": "workspace:^",
"@backstage/frontend-app-api": "workspace:^",
@@ -33,20 +48,5 @@
},
"files": [
"dist"
],
"peerDependencies": {
"react": "^16.13.1 || ^17.0.0 || ^18.0.0",
"react-router-dom": "6.0.0-beta.0 || ^6.3.0"
},
"dependencies": {
"@backstage/core-components": "workspace:^",
"@backstage/core-plugin-api": "workspace:^",
"@backstage/types": "workspace:^",
"@backstage/version-bridge": "workspace:^",
"@material-ui/core": "^4.12.4",
"@types/react": "^16.13.1 || ^17.0.0",
"lodash": "^4.17.21",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.21.4"
}
]
}
@@ -0,0 +1,167 @@
/*
* 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),
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),
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();
});
});
@@ -0,0 +1,84 @@
/*
* 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 { createExtensionDataRef } from '../wiring/createExtensionDataRef';
import { Expand } from '../types';
/**
* 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
*/
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: createRouterExtension.componentDataRef,
},
factory({ inputs, config }) {
const Component = (props: PropsWithChildren<{}>) => {
return (
<options.Component inputs={inputs} config={config}>
{props.children}
</options.Component>
);
};
return {
component: Component,
};
},
});
}
/** @public */
export namespace createRouterExtension {
export const componentDataRef =
createExtensionDataRef<ComponentType<PropsWithChildren<{}>>>(
'app.router.wrapper',
);
}
@@ -17,6 +17,7 @@
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';