diff --git a/packages/app/src/components/scaffolder/decorators.ts b/packages/app/src/components/scaffolder/decorators.ts index 7516a861b2..ffb78562f8 100644 --- a/packages/app/src/components/scaffolder/decorators.ts +++ b/packages/app/src/components/scaffolder/decorators.ts @@ -17,7 +17,7 @@ import { githubAuthApiRef } from '@backstage/core-plugin-api'; import { createScaffolderFormDecorator } from '@backstage/plugin-scaffolder-react/alpha'; export const mockDecorator = createScaffolderFormDecorator({ - id: 'githubOauth', + id: 'mock-decorator', schema: { input: { test: z => z.string(), @@ -26,8 +26,11 @@ export const mockDecorator = createScaffolderFormDecorator({ deps: { githubApi: githubAuthApiRef, }, - decorator: async ({ setSecrets }, { githubApi }) => { - const token = await githubApi.getAccessToken(); - setSecrets(state => ({ ...state, GITHUB_TOKEN: token })); + decorator: async ( + { setSecrets, setFormState, input: { test } }, + { githubApi: _githubApi }, + ) => { + setFormState(state => ({ ...state, test, mock: 'MOCK' })); + setSecrets(state => ({ ...state, GITHUB_TOKEN: 'MOCK_TOKEN' })); }, }); diff --git a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx index 0b4cbbc94a..35bae80ec5 100644 --- a/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx +++ b/plugins/scaffolder-react/src/next/components/Stepper/Stepper.tsx @@ -120,11 +120,11 @@ export const Stepper = (stepperProps: StepperProps) => { reviewButtonText = 'Review', } = components; const analytics = useAnalytics(); - const { presentation, steps } = useTemplateSchema(props.manifest); const apiHolder = useApiHolder(); const [activeStep, setActiveStep] = useState(0); const [isValidating, setIsValidating] = useState(false); const [initialState] = useFormDataFromQuery(props.initialState); + const { presentation, steps } = useTemplateSchema(props.manifest); const [stepsState, setStepsState] = useState>(initialState); diff --git a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx index f984f5ff7a..204df49f99 100644 --- a/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx +++ b/plugins/scaffolder-react/src/next/components/Workflow/Workflow.tsx @@ -26,10 +26,7 @@ import { makeStyles } from '@material-ui/core/styles'; import { errorApiRef, useAnalytics, useApi } from '@backstage/core-plugin-api'; import { useTemplateParameterSchema } from '../../hooks/useTemplateParameterSchema'; import { Stepper, type StepperProps } from '../Stepper/Stepper'; -import { - SecretsContextProvider, - useInternalTemplateSecrets, -} from '../../../secrets/SecretsContext'; +import { SecretsContextProvider } from '../../../secrets/SecretsContext'; import { useFilteredSchemaProperties } from '../../hooks/useFilteredSchemaProperties'; import { ReviewStepProps } from '@backstage/plugin-scaffolder-react'; import { useTemplateTimeSavedMinutes } from '../../hooks/useTemplateTimeSaved'; @@ -88,40 +85,9 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { const { loading, manifest, error } = useTemplateParameterSchema(templateRef); const sortedManifest = useFilteredSchemaProperties(manifest); const minutesSaved = useTemplateTimeSavedMinutes(templateRef); - const { setSecrets } = useInternalTemplateSecrets(); - const formDecorators = useFormDecorators(); const workflowOnCreate = useCallback( - async (originalFormState: Record) => { - let formState: Record = { ...originalFormState }; - - if (manifest?.EXPERIMENTAL_formDecorators) { - // for each of the form decorators, go and call the decorator with the context - await Promise.all( - manifest.EXPERIMENTAL_formDecorators.map(async decorator => { - const formDecorator = formDecorators?.get(decorator.id); - if (!formDecorator) { - // eslint-disable-next-line no-console - console.error('Failed to find form decorator', decorator.id); - return; - } - - await formDecorator.decorator({ - setSecrets, - setFormState: ( - handler: ( - oldState: Record, - ) => Record, - ) => { - formState = { ...handler(formState) }; - }, - formState, - input: decorator.input ?? {}, - }); - }), - ); - } - + async (formState: Record) => { onCreate(formState); const name = @@ -130,15 +96,7 @@ export const Workflow = (workflowProps: WorkflowProps): JSX.Element | null => { value: minutesSaved, }); }, - [ - manifest?.EXPERIMENTAL_formDecorators, - formDecorators, - onCreate, - analytics, - templateName, - minutesSaved, - setSecrets, - ], + [onCreate, analytics, templateName, minutesSaved], ); useEffect(() => { diff --git a/plugins/scaffolder-react/src/secrets/SecretsContext.tsx b/plugins/scaffolder-react/src/secrets/SecretsContext.tsx index a12722a1fd..64b25d0e8f 100644 --- a/plugins/scaffolder-react/src/secrets/SecretsContext.tsx +++ b/plugins/scaffolder-react/src/secrets/SecretsContext.tsx @@ -94,15 +94,3 @@ export const useTemplateSecrets = (): ScaffolderUseTemplateSecrets => { return { setSecrets, secrets }; }; - -export const useInternalTemplateSecrets = () => { - const value = useContext(SecretsContext)?.atVersion(1); - - if (!value) { - throw new Error( - 'useTemplateSecrets must be used within a SecretsContextProvider', - ); - } - - return value; -}; diff --git a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx index b5e24a7d52..2ed6de4479 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateWizardPage/TemplateWizardPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { useState } from 'react'; import { Navigate, useNavigate } from 'react-router-dom'; import useAsync from 'react-use/esm/useAsync'; import { @@ -36,9 +36,12 @@ import { } from '@backstage/plugin-scaffolder-react'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { Workflow } from '@backstage/plugin-scaffolder-react/alpha'; +import { + Workflow, + useTemplateParameterSchema, +} from '@backstage/plugin-scaffolder-react/alpha'; import { JsonValue } from '@backstage/types'; -import { Header, Page } from '@backstage/core-components'; +import { Header, Page, Progress } from '@backstage/core-components'; import { rootRouteRef, @@ -49,6 +52,7 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { scaffolderTranslationRef } from '../../../translation'; import { TemplateWizardPageContextMenu } from './TemplateWizardPageContextMenu'; +import { useFormDecorators } from '../../hooks'; /** * @alpha @@ -70,9 +74,10 @@ export type TemplateWizardPageProps = { export const TemplateWizardPage = (props: TemplateWizardPageProps) => { const rootRef = useRouteRef(rootRouteRef); const taskRoute = useRouteRef(scaffolderTaskRouteRef); - const { secrets } = useTemplateSecrets(); + const { secrets: contextSecrets } = useTemplateSecrets(); const scaffolderApi = useApi(scaffolderApiRef); const catalogApi = useApi(catalogApiRef); + const [isCreating, setIsCreating] = useState(false); const navigate = useNavigate(); const { templateName, namespace } = useRouteRefParams( selectedTemplateRouteRef, @@ -85,12 +90,26 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { name: templateName, }); + const { manifest } = useTemplateParameterSchema(templateRef); + const decorators = useFormDecorators({ manifest }); + const { value: editUrl } = useAsync(async () => { const data = await catalogApi.getEntityByRef(templateRef); return data?.metadata.annotations?.[ANNOTATION_EDIT_URL]; }, [templateRef, catalogApi]); - const onCreate = async (values: Record) => { + const onCreate = async (initialValues: Record) => { + if (isCreating) { + return; + } + + setIsCreating(true); + + const { formState: values, secrets } = await decorators.run({ + formState: initialValues, + secrets: contextSecrets, + }); + const { taskId } = await scaffolderApi.scaffold({ templateRef, values, @@ -113,6 +132,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => { > + {isCreating && } Promise; }; -export const useFormDecorators = () => { +/** @alpha */ +export const useFormDecorators = ({ + manifest, +}: { + manifest?: TemplateParameterSchema; +}) => { const formDecoratorsApi = useApi(formDecoratorsApiRef); const errorApi = useApi(errorApiRef); const { value: decorators } = useAsync( @@ -34,7 +41,7 @@ export const useFormDecorators = () => { ); const apiHolder = useApiHolder(); - return useMemo(() => { + const boundDecorators = useMemo(() => { const decoratorsMap = new Map(); for (const decorator of decorators ?? []) { @@ -62,4 +69,50 @@ export const useFormDecorators = () => { } return decoratorsMap; }, [apiHolder, decorators, errorApi]); + + return { + run: async (opts: { + formState: Record; + secrets: Record; + }) => { + let formState: Record = { ...opts.formState }; + let secrets: Record = {}; + + if (manifest?.EXPERIMENTAL_formDecorators) { + // for each of the form decorators, go and call the decorator with the context + await Promise.all( + manifest.EXPERIMENTAL_formDecorators.map(async decorator => { + const formDecorator = boundDecorators?.get(decorator.id); + if (!formDecorator) { + errorApi.post( + new Error(`Failed to find form decorator ${decorator.id}`), + ); + return; + } + + await formDecorator.decorator({ + setSecrets: ( + handler: ( + oldState: Record, + ) => Record, + ) => { + secrets = { ...handler(secrets) }; + }, + setFormState: ( + handler: ( + oldState: Record, + ) => Record, + ) => { + formState = { ...handler(formState) }; + }, + formState, + input: decorator.input ?? {}, + }); + }), + ); + } + + return { formState, secrets }; + }, + }; }; diff --git a/plugins/scaffolder/src/alpha/index.ts b/plugins/scaffolder/src/alpha/index.ts index 53e2b8f0bf..17c9c82fef 100644 --- a/plugins/scaffolder/src/alpha/index.ts +++ b/plugins/scaffolder/src/alpha/index.ts @@ -22,6 +22,8 @@ export { type ScaffolderTemplateFormPreviewerClassKey, } from './components'; +export * from './hooks'; + export { scaffolderTranslationRef } from '../translation'; export * from './api';