feat: added a new PageBlueprint

Signed-off-by: blam <ben@blam.sh>

Signed-off-by: blam <ben@blam.sh>
This commit is contained in:
blam
2024-08-01 14:52:04 +02:00
parent f8036017f6
commit d1191ee2d7
2 changed files with 225 additions and 0 deletions
@@ -0,0 +1,154 @@
/*
* Copyright 2024 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 { createRouteRef } from '../routing';
import { PageBlueprint } from './PageBlueprint';
import { createExtensionTester } from '@backstage/frontend-test-utils';
import {
coreExtensionData,
createExtensionBlueprint,
createExtensionInput,
} from '../wiring';
import { waitFor } from '@testing-library/react';
describe('PageBlueprint', () => {
const mockRouteRef = createRouteRef();
it('should return an extension when calling make with sensible defaults', () => {
const myPage = PageBlueprint.make({
name: 'test-page',
params: {
loader: () => Promise.resolve(<div>Test</div>),
defaultPath: '/test',
routeRef: mockRouteRef,
},
});
expect(myPage).toMatchInlineSnapshot(`
{
"$$type": "@backstage/ExtensionDefinition",
"attachTo": {
"id": "app/routes",
"input": "routes",
},
"configSchema": {
"parse": [Function],
"schema": {
"$schema": "http://json-schema.org/draft-07/schema#",
"additionalProperties": false,
"properties": {
"path": {
"type": "string",
},
},
"type": "object",
},
},
"disabled": false,
"factory": [Function],
"inputs": {},
"kind": "page",
"name": "test-page",
"namespace": undefined,
"output": [
[Function],
[Function],
{
"$$type": "@backstage/ExtensionDataRef",
"config": {
"optional": true,
},
"id": "core.routing.ref",
"optional": [Function],
"toString": [Function],
},
],
"toString": [Function],
"version": "v2",
}
`);
});
it('should output a routeRef with the input routeRef', async () => {
const myPage = PageBlueprint.make({
name: 'test-page',
params: {
loader: () => Promise.resolve(<div data-testid="test">Test</div>),
defaultPath: '/test',
routeRef: mockRouteRef,
},
});
const tester = createExtensionTester(myPage);
// TODO(blam): test for the routePath output doesn't work.
// expect(tester.data(coreExtensionData.routePath)).toBe('/test');
expect(tester.data(coreExtensionData.routeRef)).toBe(mockRouteRef);
const { getByTestId } = tester.render();
await waitFor(() => expect(getByTestId('test')).toBeInTheDocument());
});
it('should allow defining additional inputs to the extension', async () => {
const myPage = PageBlueprint.make({
name: 'test-page',
params: {
loader: async ({ inputs }) => {
return (
<div data-testid="test">
{/* todo(blam): need to fix the typescript here, as inputs is not the right type */}
{inputs.cards.map(c => c.get(coreExtensionData.reactElement))}
</div>
);
},
defaultPath: '/test',
routeRef: mockRouteRef,
},
inputs: {
cards: createExtensionInput([coreExtensionData.reactElement], {
optional: false,
singleton: false,
}),
},
});
const CardBlueprint = createExtensionBlueprint({
kind: 'card',
attachTo: { id: 'page:test-page', input: 'cards' },
output: [coreExtensionData.reactElement],
factory() {
return [
coreExtensionData.reactElement(
<div data-testid="card">I'm a lovely card</div>,
),
];
},
});
const tester = createExtensionTester(myPage).add(
CardBlueprint.make({ name: 'card', params: {} }),
);
const { getByTestId, getByText } = tester.render();
await waitFor(() => expect(getByTestId('card')).toBeInTheDocument());
await waitFor(() =>
expect(getByText("I'm a lovely card")).toBeInTheDocument(),
);
});
});
@@ -0,0 +1,71 @@
/*
* Copyright 2024 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 { RouteRef } from '../routing';
import { coreExtensionData, createExtensionBlueprint } from '../wiring';
import { ExtensionBoundary } from '../components';
export const PageBlueprint = createExtensionBlueprint({
kind: 'page',
attachTo: { id: 'app/routes', input: 'routes' },
output: [
coreExtensionData.routePath,
coreExtensionData.reactElement,
coreExtensionData.routeRef.optional(),
],
config: {
schema: {
path: z => z.string().optional(),
},
},
factory(
{
defaultPath,
loader,
routeRef,
}: {
defaultPath?: string;
loader: (opts: {
config: typeof config;
inputs: typeof inputs;
}) => Promise<JSX.Element>;
routeRef?: RouteRef;
},
{ config, inputs, node },
) {
const ExtensionComponent = lazy(() =>
loader({ config, inputs }).then(element => ({ default: () => element })),
);
// TODO(blam): this is a little awkward for optional returns.
// I wonder if we should be using generators or yield instead
// for a better API here.
const outputs = [
coreExtensionData.routePath(config.path ?? defaultPath!),
coreExtensionData.reactElement(
<ExtensionBoundary node={node}>
<ExtensionComponent />
</ExtensionBoundary>,
),
];
if (routeRef) {
return [...outputs, coreExtensionData.routeRef(routeRef)];
}
return outputs;
},
});