diff --git a/plugins/scaffolder/src/api.ts b/plugins/scaffolder/src/api.ts index c7b1594071..4a0ecc0161 100644 --- a/plugins/scaffolder/src/api.ts +++ b/plugins/scaffolder/src/api.ts @@ -15,7 +15,7 @@ */ import { EntityName } from '@backstage/catalog-model'; -import { JsonObject } from '@backstage/config'; +import { JsonObject, JsonValue } from '@backstage/config'; import { createApiRef, DiscoveryApi, @@ -24,6 +24,7 @@ import { } from '@backstage/core'; import { ResponseError } from '@backstage/errors'; import { ScmIntegrationRegistry } from '@backstage/integration'; +import { Field, FieldValidation } from '@rjsf/core'; import ObservableImpl from 'zen-observable'; import { ListActionsResponse, ScaffolderTask, Status } from './types'; @@ -52,6 +53,12 @@ export type LogEvent = { taskId: string; }; +export type CustomField = { + name: string; + component: Field; + validation: (data: JsonValue, field: FieldValidation) => void; +}; + export interface ScaffolderApi { getTemplateParameterSchema( templateName: EntityName, @@ -75,6 +82,13 @@ export interface ScaffolderApi { // Returns a list of all installed actions. listActions(): Promise; + // Register Custom Fields + registerCustomField(field: CustomField): void; + getCustomFields(): { + components: Map; + validators: Map void>; + }; + streamLogs({ taskId, after, @@ -88,6 +102,7 @@ export class ScaffolderClient implements ScaffolderApi { private readonly discoveryApi: DiscoveryApi; private readonly identityApi: IdentityApi; private readonly scmIntegrationsApi: ScmIntegrationRegistry; + private readonly customFields: Map = new Map(); constructor(options: { discoveryApi: DiscoveryApi; @@ -136,6 +151,24 @@ export class ScaffolderClient implements ScaffolderApi { return schema; } + registerCustomField(field: CustomField) { + this.customFields.set(field.name, field); + } + + getCustomFields() { + return [...this.customFields.entries()].reduce( + (previous, [name, field]) => { + previous.components.set(name, field.component); + previous.validators.set(name, field.validation); + return previous; + }, + { + components: new Map(), + validators: new Map(), + }, + ); + } + /** * Executes the scaffolding of a component, given a template and its * parameter values. diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx index deff014120..aeb90b37d4 100644 --- a/plugins/scaffolder/src/components/Router.tsx +++ b/plugins/scaffolder/src/components/Router.tsx @@ -20,25 +20,17 @@ import { ScaffolderPage } from './ScaffolderPage'; import { TemplatePage } from './TemplatePage'; import { TaskPage } from './TaskPage'; import { ActionsPage } from './ActionsPage'; -import { - ExtensionContext, - ExtensionCollector, - extensionsReducer, -} from '../extensions'; export const Router = ({ children }: React.PropsWithChildren<{}>) => { - const [state, dispatch] = React.useReducer(extensionsReducer, { - fields: [], - }); return ( - - {children} + <> } /> } /> } /> } /> - + {children} + ); }; diff --git a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx index 18e6962800..0aad67a1c0 100644 --- a/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx +++ b/plugins/scaffolder/src/components/TemplatePage/TemplatePage.tsx @@ -24,23 +24,16 @@ import { useRouteRef, } from '@backstage/core'; import { LinearProgress } from '@material-ui/core'; -import { - Field, - FieldValidation, - FormValidation, - IChangeEvent, -} from '@rjsf/core'; +import { FieldValidation, FormValidation, IChangeEvent } from '@rjsf/core'; import parseGitUrl from 'git-url-parse'; -import React, { useCallback, useContext, useState } from 'react'; +import React, { useCallback, useState } from 'react'; import { generatePath, useNavigate, Navigate } from 'react-router'; import { useParams } from 'react-router-dom'; import { useAsync } from 'react-use'; import { scaffolderApiRef } from '../../api'; import { rootRouteRef } from '../../routes'; import { MultistepJsonForm } from '../MultistepJsonForm'; -import { RepoUrlPicker, OwnerPicker } from '../fields'; import { JsonObject, JsonValue } from '@backstage/config'; -import { ExtensionContext } from '../../extensions'; const useTemplateParameterSchema = (templateName: string) => { const scaffolderApi = useApi(scaffolderApiRef); @@ -62,7 +55,10 @@ function isObject(obj: unknown): obj is JsonObject { export const createValidator = ( rootSchema: JsonObject, - validators: Record void>, + validators: Map< + string, + (value: JsonValue, validation: FieldValidation) => void + >, ) => { function validate( schema: JsonObject, @@ -90,9 +86,8 @@ export const createValidator = ( const propSchema = schemaProps[key]; const fieldName = isObject(propSchema) && (propSchema['ui:field'] as string); - const validator = fieldName && validators[fieldName]; - if (validator) { - validator(propData, propValidation); + if (fieldName && validators.has(fieldName)) { + validators.get(fieldName)!(propData as JsonValue, propValidation); } } } @@ -146,7 +141,6 @@ export const TemplatePage = () => { const { schema, loading, error } = useTemplateParameterSchema(templateName); const [formState, setFormState] = useState({}); const handleFormReset = () => setFormState({}); - const { state } = useContext(ExtensionContext)!; const handleChange = useCallback( (e: IChangeEvent) => setFormState(e.formData), @@ -172,18 +166,7 @@ export const TemplatePage = () => { return ; } - const { components, validation } = state!.fields.reduce( - (acc, next) => { - acc.components[next.name] = next.component; - acc.validation[next.name] = next.validation; - return acc; - }, - { components: {}, validation: {} } as { - components: Record; - validation: Record void>; - }, - ); - + const { components, validators } = scaffolderApi.getCustomFields(); return (
{ > { return { ...step, - validate: createValidator(step.schema, validation), + validate: createValidator(step.schema, validators), }; })} /> diff --git a/plugins/scaffolder/src/extensions/index.tsx b/plugins/scaffolder/src/extensions/index.tsx index ac83985411..d448a89960 100644 --- a/plugins/scaffolder/src/extensions/index.tsx +++ b/plugins/scaffolder/src/extensions/index.tsx @@ -14,23 +14,9 @@ * limitations under the License. */ import { JsonValue } from '@backstage/config'; -import { - Extension, - ComponentLoader, - createReactExtension, - getComponentData, - attachComponentData, -} from '@backstage/core'; +import { Extension, useApi } from '@backstage/core'; import { FieldValidation, Field } from '@rjsf/core'; -import React from 'react'; -import { useMount } from 'react-use'; - -import { - childDiscoverer, - routeElementDiscoverer, - traverseElementTree, - createCollector, -} from '@backstage/core-api/src/extensions/traversal'; +import { scaffolderApiRef } from '../api'; export type FieldExtensionOptions = { name: string; @@ -41,81 +27,15 @@ export type FieldExtensionOptions = { export function createScaffolderFieldExtension( options: FieldExtensionOptions, ): Extension { - const extensionData = { - 'scaffolder.extensions.field.v1': options, + const WrappingRegister = () => { + const scaffolderApi = useApi(scaffolderApiRef); + scaffolderApi.registerCustomField(options); + return null; }; - const Result = (props: any) => ; - - for (const [key, value] of Object.entries(extensionData)) { - attachComponentData(Result, key, value); - } return { expose() { - return Result; + return WrappingRegister; }, }; } - -export type ExtensionState = { - fields: FieldExtensionOptions[]; -}; -export type RegisterFieldExtensionAction = { - type: 'fields'; - data: FieldExtensionOptions[]; -}; - -export type ExtensionAction = RegisterFieldExtensionAction; -export type ExtensionDispatch = (action: ExtensionAction) => void; - -export const ExtensionContext = React.createContext< - { state: ExtensionState; dispatch: ExtensionDispatch } | undefined ->(undefined); - -export const extensionsReducer = ( - state: ExtensionState, - action: ExtensionAction, -): ExtensionState => { - if (action.type === 'fields') { - return { - ...state, - fields: [...state.fields, ...action.data], - }; - } - return state; -}; - -export const ExtensionCollector = ({ - children, -}: React.PropsWithChildren<{}>) => { - const context = React.useContext(ExtensionContext); - - if (!context) { - throw new Error('ExtensionsCollector must be used in a ExtensionsContext'); - } - - useMount(() => { - const { fields } = traverseElementTree({ - root: children, - discoverers: [childDiscoverer, routeElementDiscoverer], - collectors: { - fields: createCollector( - () => [] as FieldExtensionOptions[], - (acc, node) => { - const data = getComponentData( - node, - 'scaffolder.extensions.field.v1', - ); - - if (data) { - acc.push(data); - } - }, - ), - }, - }); - context.dispatch({ data: fields, type: 'fields' }); - }); - - return null; -};