diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx
new file mode 100644
index 0000000000..b77d0d0c59
--- /dev/null
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx
@@ -0,0 +1,61 @@
+/*
+ * Copyright 2022 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 { ReviewState } from './ReviewState';
+import { render } from '@testing-library/react';
+
+describe('ReviewState', () => {
+ it('should render the text as normal with no options', () => {
+ const formState = {
+ name: 'John Doe',
+ test: 'bob',
+ };
+
+ const { getByRole } = render(
+ ,
+ );
+
+ expect(getByRole('row', { name: 'Name John Doe' })).toBeInTheDocument();
+ expect(getByRole('row', { name: 'Test bob' })).toBeInTheDocument();
+ });
+
+ it('should mask password ui:fields', () => {
+ const formState = {
+ name: 'John Doe',
+ test: 'bob',
+ };
+
+ const schemas = [
+ {
+ mergedSchema: {
+ type: 'object',
+ properties: {
+ name: {
+ type: 'string',
+ 'ui:widget': 'password',
+ },
+ },
+ },
+ },
+ ];
+
+ const { getByRole } = render(
+ ,
+ );
+
+ expect(getByRole('row', { name: 'Name ******' })).toBeInTheDocument();
+ });
+});
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.tsx
new file mode 100644
index 0000000000..4889b24483
--- /dev/null
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.tsx
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2022 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 { StructuredMetadataTable } from '@backstage/core-components';
+import { JsonObject } from '@backstage/types';
+import { ParsedTemplateSchema } from './useTemplateSchema';
+import { Draft07 as JSONSchema } from 'json-schema-library';
+
+interface ReviewStateProps {
+ schemas: ParsedTemplateSchema[];
+ formState: JsonObject;
+}
+
+export const ReviewState = (props: ReviewStateProps) => {
+ const reviewData = Object.fromEntries(
+ Object.entries(props.formState).map(([key, value]) => {
+ for (const step of props.schemas) {
+ const parsedSchema = new JSONSchema(step.mergedSchema);
+ const definitionInSchema = parsedSchema.getSchema(
+ `#/${key}`,
+ props.formState,
+ );
+ if (definitionInSchema) {
+ if (definitionInSchema['ui:widget'] === 'password') {
+ return [key, '******'];
+ }
+
+ const backstageReviewOptions =
+ definitionInSchema['ui:backstage']?.review;
+
+ if (backstageReviewOptions) {
+ if (backstageReviewOptions.mask) {
+ return [key, backstageReviewOptions.mask];
+ }
+ if (!backstageReviewOptions.show) {
+ return [];
+ }
+ }
+ }
+ }
+ return [key, value];
+ }),
+ );
+ return ;
+};
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx
index b85a1e1f5d..80926e5909 100644
--- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx
@@ -29,6 +29,7 @@ import { FieldExtensionOptions } from '../../../extensions';
import { TemplateParameterSchema } from '../../../types';
import { createAsyncValidators } from './createAsyncValidators';
import { useTemplateSchema } from './useTemplateSchema';
+import { ReviewState } from './ReviewState';
const useStyles = makeStyles(theme => ({
backButton: {
@@ -74,7 +75,7 @@ export const Stepper = (props: StepperProps) => {
}, [props.extensions]);
const validation = useMemo(() => {
- const { mergedSchema } = steps[activeStep];
+ const { mergedSchema } = steps[activeStep] ?? {};
return createAsyncValidators(mergedSchema, validators, {
apiHolder,
});
@@ -112,30 +113,37 @@ export const Stepper = (props: StepperProps) => {
{step.title}
))}
+
+ Review
+
-
+ {activeStep < steps.length ? (
+
+ ) : (
+
+ )}
>
);
diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts
index 0482d89503..dc23a8a49c 100644
--- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts
+++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/useTemplateSchema.ts
@@ -19,17 +19,16 @@ import { UiSchema } from '@rjsf/core';
import { TemplateParameterSchema } from '../../../types';
import { extractSchemaFromStep } from './schema';
+export interface ParsedTemplateSchema {
+ uiSchema: UiSchema;
+ mergedSchema: JsonObject;
+ schema: JsonObject;
+ title: string;
+ description?: string;
+}
export const useTemplateSchema = (
manifest: TemplateParameterSchema,
-): {
- steps: {
- uiSchema: UiSchema;
- mergedSchema: JsonObject;
- schema: JsonObject;
- title: string;
- description?: string;
- }[];
-} => {
+): { steps: ParsedTemplateSchema } => {
const featureFlags = useApi(featureFlagsApiRef);
const steps = manifest.steps.map(({ title, description, schema }) => ({
title,