Merge pull request #13246 from backstage/blam/next-scaffolder/wizard-2

scaffolder(next): implement the review step
This commit is contained in:
Ben Lambert
2022-08-22 11:14:57 +02:00
committed by GitHub
6 changed files with 268 additions and 40 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder': minor
---
Implementing review step for the scaffolder under `create/next`
@@ -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(
<ReviewState formState={formState} schemas={[]} />,
);
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(
<ReviewState formState={formState} schemas={schemas} />,
);
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(
<ReviewState formState={formState} schemas={schemas} />,
);
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(
<ReviewState formState={formState} schemas={schemas} />,
);
expect(getByRole('row', { name: 'Name lols' })).toBeInTheDocument();
});
});
@@ -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 <StructuredMetadataTable metadata={reviewData} />;
};
@@ -47,17 +47,17 @@ describe('Stepper', () => {
title: 'React JSON Schema Form Test',
};
const { getByText } = await renderInTestApp(
const { getByRole } = await renderInTestApp(
<Stepper manifest={manifest} extensions={[]} />,
);
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(
<Stepper manifest={manifest} extensions={[]} />,
);
@@ -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(
@@ -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 (
<>
<MuiStepper activeStep={activeStep} alternativeLabel variant="elevation">
@@ -112,30 +117,51 @@ export const Stepper = (props: StepperProps) => {
<MuiStepLabel>{step.title}</MuiStepLabel>
</MuiStep>
))}
<MuiStep>
<MuiStepLabel>Review</MuiStepLabel>
</MuiStep>
</MuiStepper>
<div className={styles.formWrapper}>
<Form
extraErrors={errors}
formData={formState}
schema={steps[activeStep].schema}
uiSchema={steps[activeStep].uiSchema}
onSubmit={handleNext}
fields={extensions}
showErrorList={false}
>
<div className={styles.footer}>
<Button
onClick={handleBack}
className={styles.backButton}
disabled={activeStep < 1}
>
Back
</Button>
<Button variant="contained" color="primary" type="submit">
{activeStep === steps.length - 1 ? 'Review' : 'Next'}
</Button>
</div>
</Form>
{activeStep < steps.length ? (
<Form
extraErrors={errors}
formData={formState}
schema={steps[activeStep].schema}
uiSchema={steps[activeStep].uiSchema}
onSubmit={handleNext}
fields={extensions}
showErrorList={false}
>
<div className={styles.footer}>
<Button
onClick={handleBack}
className={styles.backButton}
disabled={activeStep < 1}
>
Back
</Button>
<Button variant="contained" color="primary" type="submit">
{activeStep === steps.length - 1 ? 'Review' : 'Next'}
</Button>
</div>
</Form>
) : (
<>
<ReviewState formState={formState} schemas={steps} />
<div className={styles.footer}>
<Button
onClick={handleBack}
className={styles.backButton}
disabled={activeStep < 1}
>
Back
</Button>
<Button variant="contained" onClick={handleCreate}>
Create
</Button>
</div>
</>
)}
</div>
</>
);
@@ -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,