diff --git a/.changeset/angry-trees-relax.md b/.changeset/angry-trees-relax.md
new file mode 100644
index 0000000000..64dd50366c
--- /dev/null
+++ b/.changeset/angry-trees-relax.md
@@ -0,0 +1,11 @@
+---
+'@backstage/plugin-scaffolder': patch
+---
+
+Enabling the customization of the last step in the scaffolder template.
+
+To override the content you have to do the next:
+
+```typescript jsx
+
+```
diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md
index 33a5f8c2fe..39e58a7a42 100644
--- a/plugins/scaffolder/api-report.md
+++ b/plugins/scaffolder/api-report.md
@@ -388,12 +388,27 @@ export const RepoUrlPickerFieldSchema: FieldSchema<
export type RepoUrlPickerUiOptions =
typeof RepoUrlPickerFieldSchema.uiOptionsType;
+// @public
+export type ReviewStepProps = {
+ disableButtons: boolean;
+ formData: JsonObject;
+ handleBack: () => void;
+ handleReset: () => void;
+ handleCreate: () => void;
+ steps: {
+ uiSchema: UiSchema;
+ mergedSchema: JsonObject;
+ schema: JsonObject;
+ }[];
+};
+
// @public (undocumented)
export const rootRouteRef: RouteRef;
// @public
export type RouterProps = {
components?: {
+ ReviewStepComponent?: ComponentType;
TemplateCardComponent?:
| ComponentType<{
template: TemplateEntityV1beta3;
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx
index 21540a4895..3f8e153b65 100644
--- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.test.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-import { getReviewData } from './MultistepJsonForm';
+import { getReviewData, getUiSchemasFromSteps } from './ReviewStep';
describe('MultistepJsonForm', () => {
const formDataMock = {
@@ -76,7 +76,8 @@ describe('MultistepJsonForm', () => {
];
test('Fields are defined to be hidden or masked', () => {
- const reviewData = getReviewData(formDataMock, stepsMock);
+ const schemas = getUiSchemasFromSteps(stepsMock);
+ const reviewData = getReviewData(formDataMock, schemas);
expect(reviewData.password).toBe('******');
expect(reviewData.masked).toBe('******');
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
index 7afbeb5a97..1fc63b2b61 100644
--- a/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/MultistepJsonForm.tsx
@@ -13,41 +13,46 @@
* See the License for the specific language governing permissions and
* limitations under the License.
*/
-import { JsonObject } from '@backstage/types';
import {
- Box,
Button,
- Paper,
Step as StepUI,
StepContent,
StepLabel,
Stepper,
Typography,
} from '@material-ui/core';
+import { JsonObject } from '@backstage/types';
import {
errorApiRef,
- useApi,
featureFlagsApiRef,
useAnalytics,
useRouteRefParams,
+ useApi,
} from '@backstage/core-plugin-api';
-import { FormProps, IChangeEvent, UiSchema, withTheme } from '@rjsf/core';
+import { FormProps, IChangeEvent, withTheme } from '@rjsf/core';
+import { UiSchema } from '@rjsf/utils';
import { Theme as MuiTheme } from '@rjsf/material-ui';
-import React, { useState } from 'react';
+import React, { ComponentType, useState } from 'react';
import { transformSchemaToProps } from './schema';
-import { Content, StructuredMetadataTable } from '@backstage/core-components';
import cloneDeep from 'lodash/cloneDeep';
import * as fieldOverrides from './FieldOverrides';
import { LayoutOptions } from '../../layouts';
+import { ReviewStepProps } from '../types';
+import { ReviewStep } from './ReviewStep';
import { selectedTemplateRouteRef } from '../../routes';
+import { extractSchemaFromStep } from '../../next/TemplateWizardPage/Stepper/schema';
const Form = withTheme(MuiTheme);
+
type Step = {
schema: JsonObject;
title: string;
} & Partial, 'schema'>>;
-type Props = {
+/**
+ * The props for a dynamic form of a scaffolder template.
+ */
+export type MultistepJsonFormProps = {
/**
* Steps for the form, each contains title and form schema
*/
@@ -60,62 +65,24 @@ type Props = {
fields?: FormProps['fields'];
finishButtonLabel?: string;
layouts: LayoutOptions[];
+ ReviewStepComponent?: ComponentType;
};
-export function getUiSchemasFromSteps(steps: Step[]): UiSchema[] {
- const uiSchemas: Array = [];
- steps.forEach(step => {
- const schemaProps = step.schema.properties as JsonObject;
- for (const key in schemaProps) {
- if (schemaProps.hasOwnProperty(key)) {
- const uiSchema = schemaProps[key] as UiSchema;
- uiSchema.name = key;
- uiSchemas.push(uiSchema);
- }
- }
- });
- return uiSchemas;
+export function getSchemasFromSteps(steps: Step[]): {
+ uiSchema: UiSchema;
+ mergedSchema: JsonObject;
+ schema: JsonObject;
+}[] {
+ return steps.map(({ schema }) => ({
+ mergedSchema: schema,
+ ...extractSchemaFromStep(schema),
+ }));
}
-export function getReviewData(formData: Record, steps: Step[]) {
- const uiSchemas = getUiSchemasFromSteps(steps);
- const reviewData: Record = {};
- for (const key in formData) {
- if (formData.hasOwnProperty(key)) {
- const uiSchema = uiSchemas.find(us => us.name === key);
-
- if (!uiSchema) {
- reviewData[key] = formData[key];
- continue;
- }
-
- if (uiSchema['ui:widget'] === 'password') {
- reviewData[key] = '******';
- continue;
- }
-
- if (!uiSchema['ui:backstage'] || !uiSchema['ui:backstage'].review) {
- reviewData[key] = formData[key];
- continue;
- }
-
- const review = uiSchema['ui:backstage'].review as JsonObject;
- if (review.mask) {
- reviewData[key] = review.mask;
- continue;
- }
-
- if (!review.show) {
- continue;
- }
- reviewData[key] = formData[key];
- }
- }
-
- return reviewData;
-}
-
-export const MultistepJsonForm = (props: Props) => {
+/**
+ * Creates the dynamic form for a scaffolder template.
+ */
+export const MultistepJsonForm = (props: MultistepJsonFormProps) => {
const {
formData,
onChange,
@@ -123,8 +90,8 @@ export const MultistepJsonForm = (props: Props) => {
onFinish,
fields,
widgets,
- finishButtonLabel,
layouts,
+ ReviewStepComponent,
} = props;
const { templateName } = useRouteRefParams(selectedTemplateRouteRef);
const analytics = useAnalytics();
@@ -197,6 +164,8 @@ export const MultistepJsonForm = (props: Props) => {
}
};
+ const ReviewStepElement = ReviewStepComponent ?? ReviewStep;
+
return (
<>
@@ -240,30 +209,14 @@ export const MultistepJsonForm = (props: Props) => {
})}
{activeStep === steps.length && (
-
-
- Review and create
-
-
-
-
-
-
-
+
)}
>
);
diff --git a/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx b/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx
new file mode 100644
index 0000000000..d78d726869
--- /dev/null
+++ b/plugins/scaffolder/src/components/MultistepJsonForm/ReviewStep.tsx
@@ -0,0 +1,121 @@
+/*
+ * Copyright 2020 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 { Box, Button, Paper, Typography } from '@material-ui/core';
+import React from 'react';
+import { Content, StructuredMetadataTable } from '@backstage/core-components';
+import { UiSchema } from '@rjsf/core';
+import { JsonObject } from '@backstage/types';
+import { ReviewStepProps } from '../types';
+
+export function getReviewData(
+ formData: Record,
+ uiSchemas: UiSchema[],
+) {
+ const reviewData: Record = {};
+ for (const key in formData) {
+ if (formData.hasOwnProperty(key)) {
+ const uiSchema = uiSchemas.find(us => us.name === key);
+
+ if (!uiSchema) {
+ reviewData[key] = formData[key];
+ continue;
+ }
+
+ if (uiSchema['ui:widget'] === 'password') {
+ reviewData[key] = '******';
+ continue;
+ }
+
+ if (!uiSchema['ui:backstage'] || !uiSchema['ui:backstage'].review) {
+ reviewData[key] = formData[key];
+ continue;
+ }
+
+ const review = uiSchema['ui:backstage'].review as JsonObject;
+ if (review.mask) {
+ reviewData[key] = review.mask;
+ continue;
+ }
+
+ if (!review.show) {
+ continue;
+ }
+ reviewData[key] = formData[key];
+ }
+ }
+
+ return reviewData;
+}
+
+export function getUiSchemasFromSteps(
+ steps: {
+ schema: JsonObject;
+ }[],
+): UiSchema[] {
+ const uiSchemas: Array = [];
+ steps.forEach(step => {
+ const schemaProps = step.schema.properties as JsonObject;
+ for (const key in schemaProps) {
+ if (schemaProps.hasOwnProperty(key)) {
+ const uiSchema = schemaProps[key] as UiSchema;
+ uiSchema.name = key;
+ uiSchemas.push(uiSchema);
+ }
+ }
+ });
+ return uiSchemas;
+}
+
+/**
+ * The component displaying the Last Step in scaffolder template form.
+ * Which represents the summary of the input provided by the end user.
+ */
+export const ReviewStep = (props: ReviewStepProps) => {
+ const {
+ disableButtons,
+ formData,
+ handleBack,
+ handleCreate,
+ handleReset,
+ steps,
+ } = props;
+ return (
+
+
+ Review and create
+
+
+
+
+
+
+
+ );
+};
diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx
index 9f46b427a0..d35bcbc67a 100644
--- a/plugins/scaffolder/src/components/Router.tsx
+++ b/plugins/scaffolder/src/components/Router.tsx
@@ -15,7 +15,7 @@
*/
import React, { ComponentType, useEffect } from 'react';
-import { Routes, Route, useOutlet, Navigate } from 'react-router';
+import { Navigate, Route, Routes, useOutlet } from 'react-router';
import { Entity } from '@backstage/catalog-model';
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderPage } from './ScaffolderPage';
@@ -26,10 +26,10 @@ import { SecretsContextProvider } from './secrets/SecretsContext';
import { TemplateEditorPage } from './TemplateEditorPage';
import {
- FieldExtensionOptions,
- FIELD_EXTENSION_WRAPPER_KEY,
- FIELD_EXTENSION_KEY,
DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS,
+ FIELD_EXTENSION_KEY,
+ FIELD_EXTENSION_WRAPPER_KEY,
+ FieldExtensionOptions,
} from '../extensions';
import {
useElementFilter,
@@ -46,6 +46,7 @@ import {
} from '../routes';
import { ListTasksPage } from './ListTasksPage';
import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts';
+import { ReviewStepProps } from './types';
/**
* The props for the entrypoint `ScaffolderPage` component the plugin.
@@ -53,6 +54,7 @@ import { LayoutOptions, LAYOUTS_KEY, LAYOUTS_WRAPPER_KEY } from '../layouts';
*/
export type RouterProps = {
components?: {
+ ReviewStepComponent?: ComponentType;
TemplateCardComponent?:
| ComponentType<{ template: TemplateEntityV1beta3 }>
| undefined;
@@ -87,7 +89,8 @@ export type RouterProps = {
export const Router = (props: RouterProps) => {
const { groups, components = {}, defaultPreviewTemplate } = props;
- const { TemplateCardComponent, TaskPageComponent } = components;
+ const { ReviewStepComponent, TemplateCardComponent, TaskPageComponent } =
+ components;
const outlet = useOutlet();
const TaskPageElement = TaskPageComponent ?? TaskPage;
@@ -161,6 +164,7 @@ export const Router = (props: RouterProps) => {
element={
{
const scaffolderApi = useApi(scaffolderApiRef);
@@ -52,6 +53,7 @@ const useTemplateParameterSchema = (templateRef: string) => {
};
type Props = {
+ ReviewStepComponent?: ComponentType;
customFieldExtensions?: FieldExtensionOptions[];
layouts?: LayoutOptions[];
headerOptions?: {
@@ -62,6 +64,7 @@ type Props = {
};
export const TemplatePage = ({
+ ReviewStepComponent,
customFieldExtensions = [],
layouts = [],
headerOptions,
@@ -155,6 +158,7 @@ export const TemplatePage = ({
titleTypographyProps={{ component: 'h2' }}
>
void;
+ handleReset: () => void;
+ handleCreate: () => void;
+ steps: {
+ uiSchema: UiSchema;
+ mergedSchema: JsonObject;
+ schema: JsonObject;
+ }[];
+};
diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.tsx
similarity index 97%
rename from plugins/scaffolder/src/plugin.ts
rename to plugins/scaffolder/src/plugin.tsx
index a34105745f..cfa7af8128 100644
--- a/plugins/scaffolder/src/plugin.ts
+++ b/plugins/scaffolder/src/plugin.tsx
@@ -120,7 +120,7 @@ export const EntityNamePickerFieldExtension = scaffolderPlugin.provide(
/**
* The field extension which provides the ability to select a RepositoryUrl.
- * Currently this is an encoded URL that looks something like the following `github.com?repo=myRepoName&owner=backstage`.
+ * Currently, this is an encoded URL that looks something like the following `github.com?repo=myRepoName&owner=backstage`.
*
* @public
*/