diff --git a/.changeset/two-planets-provide.md b/.changeset/two-planets-provide.md new file mode 100644 index 0000000000..920eebc01d --- /dev/null +++ b/.changeset/two-planets-provide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Implementing review step for the scaffolder under `create/next` 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..2b2e706102 --- /dev/null +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/ReviewState.test.tsx @@ -0,0 +1,140 @@ +/* + * 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'; +import { ParsedTemplateSchema } from './useTemplateSchema'; + +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: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'string', + 'ui:widget': 'password', + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + description: 'asd', + }, + ]; + + const { getByRole } = render( + , + ); + + expect(getByRole('row', { name: 'Name ******' })).toBeInTheDocument(); + }); + + it('should hide from review if show is not set', async () => { + const formState = { + name: 'John Doe', + test: 'bob', + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'string', + 'ui:widget': 'password', + 'ui:backstage': { + review: { + show: false, + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + description: 'asd', + }, + ]; + + const { queryByRole } = render( + , + ); + + expect( + await queryByRole('row', { name: 'Name ******' }), + ).not.toBeInTheDocument(); + }); + + it('should allow for masking an option with a set text', () => { + const formState = { + name: 'John Doe', + test: 'bob', + }; + + const schemas: ParsedTemplateSchema[] = [ + { + mergedSchema: { + type: 'object', + properties: { + name: { + type: 'string', + 'ui:widget': 'password', + 'ui:backstage': { + review: { + mask: 'lols', + }, + }, + }, + }, + }, + schema: {}, + title: 'test', + uiSchema: {}, + description: 'asd', + }, + ]; + + const { getByRole } = render( + , + ); + + expect(getByRole('row', { name: 'Name lols' })).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..f39a26622a --- /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) { + const backstageReviewOptions = + definitionInSchema['ui:backstage']?.review; + + if (backstageReviewOptions) { + if (backstageReviewOptions.mask) { + return [key, backstageReviewOptions.mask]; + } + if (backstageReviewOptions.show === false) { + return []; + } + } + + if (definitionInSchema['ui:widget'] === 'password') { + return [key, '******']; + } + } + } + return [key, value]; + }), + ); + return ; +}; diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx index 90901f22c5..f605e9c114 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx @@ -47,17 +47,17 @@ describe('Stepper', () => { title: 'React JSON Schema Form Test', }; - const { getByText } = await renderInTestApp( + const { getByRole } = await renderInTestApp( , ); - expect(getByText('Next')).toBeInTheDocument(); + expect(getByRole('button', { name: 'Next' })).toBeInTheDocument(); await act(async () => { - await fireEvent.click(getByText('Next')); + await fireEvent.click(getByRole('button', { name: 'Next' })); }); - expect(getByText('Review')).toBeInTheDocument(); + expect(getByRole('button', { name: 'Review' })).toBeInTheDocument(); }); it('should remember the state of the form when cycling through the pages', async () => { @@ -87,7 +87,7 @@ describe('Stepper', () => { title: 'React JSON Schema Form Test', }; - const { getByRole, getByText } = await renderInTestApp( + const { getByRole } = await renderInTestApp( , ); @@ -96,11 +96,11 @@ describe('Stepper', () => { }); await act(async () => { - await fireEvent.click(getByText('Next')); + await fireEvent.click(getByRole('button', { name: 'Next' })); }); await act(async () => { - await fireEvent.click(getByText('Back')); + await fireEvent.click(getByRole('button', { name: 'Back' })); }); expect(getByRole('textbox', { name: 'name' })).toHaveValue( diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index b85a1e1f5d..f5656092c1 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -29,11 +29,13 @@ 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: { marginRight: theme.spacing(1), }, + footer: { display: 'flex', flexDirection: 'row', @@ -74,8 +76,7 @@ export const Stepper = (props: StepperProps) => { }, [props.extensions]); const validation = useMemo(() => { - const { mergedSchema } = steps[activeStep]; - return createAsyncValidators(mergedSchema, validators, { + return createAsyncValidators(steps[activeStep]?.mergedSchema, validators, { apiHolder, }); }, [steps, activeStep, validators, apiHolder]); @@ -104,6 +105,10 @@ export const Stepper = (props: StepperProps) => { setFormState(current => ({ ...current, ...formData })); }; + const handleCreate = () => { + // TODO(blam): Create the template in a modal with the ability to view the logs etc. + }; + return ( <> @@ -112,30 +117,51 @@ 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..d0d8590a7b 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,