Merge pull request #13246 from backstage/blam/next-scaffolder/wizard-2
scaffolder(next): implement the review step
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user