diff --git a/packages/app-experiments/package.json b/packages/app-experiments/package.json index aa2cb39f1d..af83b2e0e1 100644 --- a/packages/app-experiments/package.json +++ b/packages/app-experiments/package.json @@ -85,13 +85,15 @@ "@roadiehq/backstage-plugin-github-pull-requests": "^2.2.7", "@roadiehq/backstage-plugin-travis-ci": "^2.0.5", "history": "^5.0.0", + "lodash": "^4.17.21", "prop-types": "^15.7.2", "react": "^17.0.2", "react-dom": "^17.0.2", "react-router": "^6.3.0", "react-router-dom": "^6.3.0", "react-use": "^17.2.4", - "zen-observable": "^0.10.0" + "zen-observable": "^0.10.0", + "zod": "^3.21.4" }, "devDependencies": { "@backstage/test-utils": "workspace:^", diff --git a/packages/app-experiments/src/Experiment1.tsx b/packages/app-experiments/src/Experiment1.tsx index b086af215c..6453ad1179 100644 --- a/packages/app-experiments/src/Experiment1.tsx +++ b/packages/app-experiments/src/Experiment1.tsx @@ -14,7 +14,10 @@ * limitations under the License. */ -import React, { ComponentType, createContext } from 'react'; +import React, { CSSProperties, ComponentType, createContext } from 'react'; +import { z } from 'zod'; +import mapValues from 'lodash/mapValues'; +import { Typography } from '@material-ui/core'; /* @@ -29,23 +32,56 @@ App structure: // import { redExtensionPointRef } from 'wherever'; -type Extension = { - factory(instanceOptions: { id: string; config: TInstanceConfig }): TOutput; +type Extension = { + factory(instanceOptions: { id: string; config: TInstanceConfig }): TSelf; }; -type ExtensionInstanceConfig = { +type ExtensionOptions< + TInstanceConfig, + TSelf, + TPoints extends { + [name in string]: { + typeRef: ExtensionPointTypeRef; + configSchema: z.ZodObject; + }; + }, +> = { + mountTypeRef: ExtensionPointTypeRef; + points: TPoints; + // points: { + // [name in keyof TPoints]: { + // typeRef: TPoints[name]['typeRef']; + // configSchema: z.ZodObject; + // }; + // }; + factory(instanceOptions: { + id: string; + config: TInstanceConfig; + points: { + [name in keyof TPoints]: ExtensionPointRef; + }; + // points: {[name in keyof TPoints]: TPoints[name] extends ExtensionPointTypeRef ? ExtensionPointRef : never}; + }): TSelf; +}; + +// points: { +// top: { ref: componentExtensionPointTypeRef, configSchema: z.object({}) }, +// bottom: { ref: componentExtensionPointTypeRef, configSchema: z.object({}) }, +// }, + +type ExtensionInstanceConfig = { id: string; config: TInstanceConfig; // what should point be for root - point?: string; - extension: Extension; + mount?: string; + extension: Extension; }; -type ExtensionInstance = { +type ExtensionInstance = { id: string; // what should point be for root - point?: string; - output: TOutput; + mount?: string; + output: TSelf; }; const BackstageAppContext = createContext<{ @@ -53,21 +89,213 @@ const BackstageAppContext = createContext<{ }>({ extensionInstances: [] }); const container = { - createExtension(extensionOptions: { - render: (options: { id: string; config: TInstanceConfig }) => TOutput; - }): Extension { + createExtension< + TInstanceConfig, + TSelf, + TPoints extends { + [name in string]: { + typeRef: ExtensionPointTypeRef; + configSchema: z.ZodObject; + }; + }, + >( + extensionOptions: ExtensionOptions, + ): Extension { return { factory(instanceOptions) { - return extensionOptions.render({ + return extensionOptions.factory({ id: instanceOptions.id, config: instanceOptions.config, + points: mapValues(extensionOptions.points, (point, name) => + createExtensionPointRef(name, point.typeRef), + ), }); }, }; }, }; -const ExtensionInstanceDerp = (props: { id: string }) => { +function useExtensionInstanceChildren( + id: string, + point: ExtensionPointRef, +): T[] { + const { extensionInstances } = React.useContext(BackstageAppContext); + + return extensionInstances + .filter(i => i.mount === `${id}/${point.name}`) + .map(extensionInstance => extensionInstance.output as T); +} + +const ExtensionInstanceChildren = (props: { + id: string; + point: ExtensionPointRef; +}) => { + const outputs = useExtensionInstanceChildren(props.id, props.point); + + return ( + <> + {outputs.map(Component => ( + + ))} + + ); +}; + +export const coreExtensionPointTypes = { + component: createExtensionPointTypeRef(), + styles: createExtensionPointTypeRef(), +}; + +const Container = container.createExtension({ + points: { + default: { + typeRef: coreExtensionPointTypes.component, + configSchema: z.object({}), + }, + }, + mountTypeRef: coreExtensionPointTypes.component, + factory: + ({ id, points }) => + () => + , +}); + +interface ExtensionPointTypeRef { + T: T; + $$type: 'extension-point-type'; +} + +function createExtensionPointTypeRef(): ExtensionPointTypeRef { + return { T: null as T, $$type: 'extension-point-type' }; +} + +interface ExtensionPointRef { + name: string; + typeRef: ExtensionPointTypeRef; + $$type: 'extension-point'; +} + +function createExtensionPointRef( + name: string, + typeRef: ExtensionPointTypeRef, +): ExtensionPointRef { + return { name, typeRef, $$type: 'extension-point' }; +} + +// const splitLayoutTopExtensionPointRef = createExtensionPointRef({ +// type: componentExtensionPointTypeRef, +// point: 'top' +// }) + +// const splitLayoutBottomExtensionPointRef = createExtensionPointRef({ +// type: componentExtensionPointTypeRef, +// point: 'bottom' +// }) + +const SplitLayout = container.createExtension({ + points: { + top: { + typeRef: coreExtensionPointTypes.component, + configSchema: z.object({}), + }, + bottom: { + typeRef: coreExtensionPointTypes.component, + configSchema: z.object({}), + }, + }, + mountTypeRef: coreExtensionPointTypes.component, + factory: + ({ id, points }) => + () => { + return ( +
+
+ +
+
+ +
+
+ ); + }, +}); + +// const SnazzySplitLayout = container.replaceExtension(SplitLayout, { +// factory: ({ id, points }) => { +// ... +// } +// }) + +const Box = container.createExtension({ + points: {}, + mountTypeRef: coreExtensionPointTypes.component, + factory: ({ config }: { config: { color: string } }) => { + return () => ( + /** + * If I need stuff, I have to get it from context. + */ +
+ {config.color} +
+ ); + }, +}); + +const StyledBox = container.createExtension({ + points: { + style: { + typeRef: coreExtensionPointTypes.styles, + configSchema: z.object({}), + }, + }, + mountTypeRef: coreExtensionPointTypes.component, + factory: ({ id, points }) => { + return () => { + const [style] = useExtensionInstanceChildren(id, points.style); + + return
Styled box
; + }; + }, +}); + +const BoxStyle = container.createExtension({ + points: {}, + mountTypeRef: coreExtensionPointTypes.styles, + factory({ config }: { config: { color: string } }) { + return { + borderColor: config.color, + borderStyle: 'solid', + borderWidth: '5px', + width: 120, + height: 120, + }; + }, +}); + +function createExtensionInstances( + instanceConfigs: ExtensionInstanceConfig[], +): ExtensionInstance[] { + return instanceConfigs.map(instanceConfig => ({ + id: instanceConfig.id, + mount: instanceConfig.mount, + output: instanceConfig.extension.factory({ + id: instanceConfig.id, + config: instanceConfig.config, + }), + })); +} + +const ExtensionInstanceRenderer = (props: { id: string }) => { const { id } = props; const { extensionInstances } = React.useContext(BackstageAppContext); @@ -83,74 +311,45 @@ const ExtensionInstanceDerp = (props: { id: string }) => { return ; }; -const ExtensionPointInstance = (props: { id: string }) => { - const { extensionInstances } = React.useContext(BackstageAppContext); - - return ( - <> - {extensionInstances - .filter(i => i.point === props.id) - .map(extensionInstance => ( - - ))} - - ); -}; - -const Container = container.createExtension({ - render: ({ id }) => , -}); - -const Box = container.createExtension({ - render: ({ config }: { config: { color: string } }) => { - return ( -
- {config.color} -
- ); - }, -}); - -function createExtensionInstances( - instanceConfigs: ExtensionInstanceConfig[], -): ExtensionInstance[] { - return instanceConfigs.map(instanceConfig => ({ - id: instanceConfig.id, - point: instanceConfig.point, - output: instanceConfig.extension.factory({ - id: instanceConfig.id, - config: instanceConfig.config, - }), - })); -} - export function Experiment1() { - // const element = useExtension("red"); - const extensionInstances = createExtensionInstances([ { id: 'root', config: {}, extension: Container }, - { id: 'nestedRoot', point: 'root', config: {}, extension: Container }, + { + id: 'layout', + mount: 'root/default', // Maybe can omit /default here? + config: {}, + extension: SplitLayout, + }, { id: 'red', - point: 'nestedRoot', + mount: 'layout/top', config: { color: 'red' }, extension: Box, }, { id: 'green', - point: 'nestedRoot', + mount: 'layout/top', config: { color: 'green' }, extension: Box, }, { id: 'blue', - point: 'nestedRoot', + mount: 'layout/bottom', config: { color: 'blue' }, extension: Box, }, + { + id: 'styled-box', + mount: 'layout/bottom', + config: {}, + extension: StyledBox, + }, + { + id: 'styled-box.style', + mount: 'styled-box/style', + config: { color: 'purple' }, + extension: BoxStyle, + }, ]); return ( @@ -161,8 +360,10 @@ export function Experiment1() { }} >

Experiment 1

-

This is an experiment to see how the app-experiments package works.

- + + This is an experiment to see how the app-experiments package works. + + ); }