Merge branch 'master' of https://github.com/backstage/backstage into update/review-explode
This commit is contained in:
@@ -1,5 +1,57 @@
|
||||
# @backstage/plugin-scaffolder-react
|
||||
|
||||
## 1.10.0-next.2
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.14.9-next.1
|
||||
- @backstage/plugin-catalog-react@1.12.2-next.2
|
||||
|
||||
## 1.10.0-next.1
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- 354e68c: Improve validation error display text in scaffolder
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- cc81579: Updated dependency `@rjsf/utils` to `5.18.5`.
|
||||
Updated dependency `@rjsf/core` to `5.18.5`.
|
||||
Updated dependency `@rjsf/material-ui` to `5.18.5`.
|
||||
Updated dependency `@rjsf/validator-ajv8` to `5.18.5`.
|
||||
- Updated dependencies
|
||||
- @backstage/plugin-catalog-react@1.12.2-next.1
|
||||
- @backstage/core-components@0.14.9-next.0
|
||||
- @backstage/core-plugin-api@1.9.3
|
||||
- @backstage/catalog-client@1.6.5
|
||||
- @backstage/catalog-model@1.5.0
|
||||
- @backstage/theme@0.5.6
|
||||
- @backstage/types@1.1.1
|
||||
- @backstage/version-bridge@1.0.8
|
||||
- @backstage/plugin-permission-react@0.4.23
|
||||
- @backstage/plugin-scaffolder-common@1.5.3
|
||||
|
||||
## 1.10.0-next.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
- b5deed0: Add support for `bitbucketCloud` autocomplete in `RepoUrlPicker`
|
||||
|
||||
### Patch Changes
|
||||
|
||||
- Updated dependencies
|
||||
- @backstage/core-components@0.14.9-next.0
|
||||
- @backstage/plugin-catalog-react@1.12.2-next.0
|
||||
- @backstage/core-plugin-api@1.9.3
|
||||
- @backstage/catalog-client@1.6.5
|
||||
- @backstage/catalog-model@1.5.0
|
||||
- @backstage/theme@0.5.6
|
||||
- @backstage/types@1.1.1
|
||||
- @backstage/version-bridge@1.0.8
|
||||
- @backstage/plugin-permission-react@0.4.23
|
||||
- @backstage/plugin-scaffolder-common@1.5.3
|
||||
|
||||
## 1.9.0
|
||||
|
||||
### Minor Changes
|
||||
|
||||
@@ -179,6 +179,17 @@ export type ReviewStepProps = {
|
||||
|
||||
// @public
|
||||
export interface ScaffolderApi {
|
||||
// (undocumented)
|
||||
autocomplete?(options: {
|
||||
token: string;
|
||||
provider: string;
|
||||
resource: string;
|
||||
context?: Record<string, string>;
|
||||
}): Promise<{
|
||||
results: {
|
||||
title: string;
|
||||
}[];
|
||||
}>;
|
||||
cancelTask(taskId: string): Promise<void>;
|
||||
// (undocumented)
|
||||
dryRun?(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@backstage/plugin-scaffolder-react",
|
||||
"version": "1.9.0",
|
||||
"version": "1.10.0-next.2",
|
||||
"description": "A frontend library that helps other Backstage plugins interact with the Scaffolder",
|
||||
"backstage": {
|
||||
"role": "web-library",
|
||||
@@ -72,10 +72,10 @@
|
||||
"@material-ui/icons": "^4.9.1",
|
||||
"@material-ui/lab": "4.0.0-alpha.61",
|
||||
"@react-hookz/web": "^24.0.0",
|
||||
"@rjsf/core": "5.18.4",
|
||||
"@rjsf/material-ui": "5.18.4",
|
||||
"@rjsf/utils": "5.18.4",
|
||||
"@rjsf/validator-ajv8": "5.18.4",
|
||||
"@rjsf/core": "5.18.5",
|
||||
"@rjsf/material-ui": "5.18.5",
|
||||
"@rjsf/utils": "5.18.5",
|
||||
"@rjsf/validator-ajv8": "5.18.5",
|
||||
"@types/json-schema": "^7.0.9",
|
||||
"@types/react": "^16.13.1 || ^17.0.0 || ^18.0.0",
|
||||
"classnames": "^2.2.6",
|
||||
|
||||
@@ -229,4 +229,11 @@ export interface ScaffolderApi {
|
||||
streamLogs(options: ScaffolderStreamLogsOptions): Observable<LogEvent>;
|
||||
|
||||
dryRun?(options: ScaffolderDryRunOptions): Promise<ScaffolderDryRunResponse>;
|
||||
|
||||
autocomplete?(options: {
|
||||
token: string;
|
||||
provider: string;
|
||||
resource: string;
|
||||
context?: Record<string, string>;
|
||||
}): Promise<{ results: { title: string }[] }>;
|
||||
}
|
||||
|
||||
@@ -44,6 +44,8 @@ export const Form = (props: PropsWithChildren<ScaffolderRJSFFormProps>) => {
|
||||
uiSchema={wrapperProps.uiSchema ?? {}}
|
||||
formData={wrapperProps.formData}
|
||||
rawErrors={wrapperProps.rawErrors ?? []}
|
||||
disabled={wrapperProps.disabled ?? false}
|
||||
readonly={wrapperProps.readonly ?? false}
|
||||
/>
|
||||
);
|
||||
},
|
||||
|
||||
+81
-18
@@ -19,25 +19,88 @@ import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { ErrorListProps } from '@rjsf/utils';
|
||||
|
||||
describe('Error List Template', () => {
|
||||
const errorList = {
|
||||
errors: [
|
||||
{
|
||||
stack: 'Test error 1',
|
||||
},
|
||||
{
|
||||
stack: 'Test error 2',
|
||||
},
|
||||
],
|
||||
errorSchema: {},
|
||||
} as Partial<ErrorListProps> as ErrorListProps;
|
||||
describe('should render the error messages', () => {
|
||||
it('no properties', async () => {
|
||||
const errorList = {
|
||||
errors: [
|
||||
{
|
||||
stack: 'Test error 1',
|
||||
},
|
||||
{
|
||||
stack: 'Test error 2',
|
||||
},
|
||||
],
|
||||
errorSchema: {},
|
||||
} as Partial<ErrorListProps> as ErrorListProps;
|
||||
|
||||
it('should render the error messages', async () => {
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ErrorListTemplate {...errorList} />,
|
||||
);
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ErrorListTemplate {...errorList} />,
|
||||
);
|
||||
|
||||
for (const error of errorList.errors) {
|
||||
expect(getByText(error.stack)).toBeInTheDocument();
|
||||
}
|
||||
for (const error of errorList.errors) {
|
||||
expect(getByText(error.stack)).toBeInTheDocument();
|
||||
}
|
||||
});
|
||||
|
||||
it('properties no title', async () => {
|
||||
const errorList = {
|
||||
errors: [
|
||||
{
|
||||
property: '.foo',
|
||||
stack: 'Test error 1',
|
||||
message: 'Test error 1',
|
||||
},
|
||||
{
|
||||
property: '.anExampleTitle',
|
||||
stack: 'Test error 2',
|
||||
message: 'Test error 2',
|
||||
},
|
||||
],
|
||||
errorSchema: {},
|
||||
schema: {},
|
||||
} as Partial<ErrorListProps> as ErrorListProps;
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ErrorListTemplate {...errorList} />,
|
||||
);
|
||||
|
||||
expect(getByText("'Foo' Test error 1")).toBeInTheDocument();
|
||||
expect(getByText("'An Example Title' Test error 2")).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('properties with title', async () => {
|
||||
const errorList = {
|
||||
errors: [
|
||||
{
|
||||
property: '.foo',
|
||||
stack: 'Test error 1',
|
||||
message: 'Test error 1',
|
||||
},
|
||||
{
|
||||
property: '.bar',
|
||||
stack: 'Test error 2',
|
||||
message: 'Test error 2',
|
||||
},
|
||||
],
|
||||
errorSchema: {},
|
||||
schema: {
|
||||
properties: {
|
||||
foo: {
|
||||
title: 'Hello',
|
||||
},
|
||||
bar: {
|
||||
title: 'Example Title',
|
||||
},
|
||||
},
|
||||
},
|
||||
} as Partial<ErrorListProps> as ErrorListProps;
|
||||
|
||||
const { getByText } = await renderInTestApp(
|
||||
<ErrorListTemplate {...errorList} />,
|
||||
);
|
||||
|
||||
expect(getByText("'Hello' Test error 1")).toBeInTheDocument();
|
||||
expect(getByText("'Example Title' Test error 2")).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
+23
-3
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { ErrorListProps } from '@rjsf/utils';
|
||||
import { ErrorListProps, RJSFValidationError } from '@rjsf/utils';
|
||||
import List from '@material-ui/core/List';
|
||||
import ListItem from '@material-ui/core/ListItem';
|
||||
import ListItemIcon from '@material-ui/core/ListItemIcon';
|
||||
@@ -22,6 +22,7 @@ import ListItemText from '@material-ui/core/ListItemText';
|
||||
import Paper from '@material-ui/core/Paper';
|
||||
import { Theme, createStyles, makeStyles } from '@material-ui/core/styles';
|
||||
import ErrorIcon from '@material-ui/icons/Error';
|
||||
import startCase from 'lodash/startCase';
|
||||
|
||||
const useStyles = makeStyles((_theme: Theme) =>
|
||||
createStyles({
|
||||
@@ -39,9 +40,28 @@ const useStyles = makeStyles((_theme: Theme) =>
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
export const ErrorListTemplate = ({ errors }: ErrorListProps) => {
|
||||
export const ErrorListTemplate = ({ errors, schema }: ErrorListProps) => {
|
||||
const classes = useStyles();
|
||||
|
||||
function formatErrorMessage(error: RJSFValidationError) {
|
||||
if (error.property && error.message) {
|
||||
const propertyName = error.property.startsWith('.')
|
||||
? error.property.substring(1)
|
||||
: error.property;
|
||||
if (schema.properties && propertyName in schema.properties) {
|
||||
const property = schema.properties[propertyName];
|
||||
|
||||
if (typeof property === 'object' && 'title' in property) {
|
||||
return `'${property.title}' ${error.message}`;
|
||||
}
|
||||
}
|
||||
// fall back to property name
|
||||
return `'${startCase(propertyName)}' ${error.message}`;
|
||||
}
|
||||
// fall back if property does not exist
|
||||
return error.stack;
|
||||
}
|
||||
|
||||
return (
|
||||
<Paper>
|
||||
<List dense className={classes.list}>
|
||||
@@ -52,7 +72,7 @@ export const ErrorListTemplate = ({ errors }: ErrorListProps) => {
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
classes={{ primary: classes.text }}
|
||||
primary={error.stack}
|
||||
primary={formatErrorMessage(error)}
|
||||
/>
|
||||
</ListItem>
|
||||
))}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import React from 'react';
|
||||
import { fireEvent, render } from '@testing-library/react';
|
||||
import { fireEvent } from '@testing-library/react';
|
||||
import { CardHeader } from './CardHeader';
|
||||
import { ThemeProvider } from '@material-ui/core/styles';
|
||||
import { lightTheme } from '@backstage/theme';
|
||||
@@ -30,7 +30,7 @@ import { stringifyEntityRef } from '@backstage/catalog-model';
|
||||
import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
|
||||
describe('CardHeader', () => {
|
||||
it('should select the correct theme from the theme provider from the header', () => {
|
||||
it('should select the correct theme from the theme provider from the header', async () => {
|
||||
// Can't really test what we want here.
|
||||
// But we can check that we call the getPage theme with the right type of template at least.
|
||||
const mockTheme = {
|
||||
@@ -38,7 +38,7 @@ describe('CardHeader', () => {
|
||||
getPageTheme: jest.fn(lightTheme.getPageTheme),
|
||||
};
|
||||
|
||||
render(
|
||||
await renderInTestApp(
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[
|
||||
|
||||
+19
-2
@@ -31,7 +31,7 @@ describe('<DefaultTemplateOutputs />', () => {
|
||||
],
|
||||
};
|
||||
|
||||
const { getByRole } = await renderInTestApp(
|
||||
const { getByRole, queryByTestId } = await renderInTestApp(
|
||||
<DefaultTemplateOutputs output={output} />,
|
||||
{
|
||||
mountedRoutes: {
|
||||
@@ -39,7 +39,8 @@ describe('<DefaultTemplateOutputs />', () => {
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
expect(queryByTestId('output-box')).not.toBeNull();
|
||||
expect(queryByTestId('text-output-box')).not.toBeNull();
|
||||
// first text output default visible
|
||||
expect(getByRole('heading', { level: 2 }).innerHTML).toBe(
|
||||
output.text[0].title,
|
||||
@@ -61,4 +62,20 @@ describe('<DefaultTemplateOutputs />', () => {
|
||||
expect(getByRole('heading', { level: 2 }).innerHTML).toBe(text.title);
|
||||
}
|
||||
});
|
||||
it('should not render anything when output is empty', async () => {
|
||||
// This is the default case when no output field is present in the template
|
||||
const output = {};
|
||||
const { queryByTestId } = await renderInTestApp(
|
||||
<DefaultTemplateOutputs output={output} />,
|
||||
{
|
||||
mountedRoutes: {
|
||||
'/catalog/:namespace/:kind/:name': entityRouteRef,
|
||||
},
|
||||
},
|
||||
);
|
||||
|
||||
// Ensure that nothing renders from this component
|
||||
expect(queryByTestId('output-box')).toBeNull();
|
||||
expect(queryByTestId('text-output-box')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
+22
-13
@@ -56,22 +56,31 @@ export const DefaultTemplateOutputs = (props: {
|
||||
return null;
|
||||
}
|
||||
|
||||
const emptyOutput = Object.keys(output).length === 0;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Box paddingBottom={2}>
|
||||
<Paper>
|
||||
<Box padding={2} justifyContent="center" display="flex" gridGap={16}>
|
||||
<TextOutputs
|
||||
output={output}
|
||||
index={textOutputIndex}
|
||||
setIndex={setTextOutputIndex}
|
||||
/>
|
||||
<LinkOutputs output={output} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
{!emptyOutput ? (
|
||||
<Box paddingBottom={2} data-testid="output-box">
|
||||
<Paper>
|
||||
<Box
|
||||
padding={2}
|
||||
justifyContent="center"
|
||||
display="flex"
|
||||
gridGap={16}
|
||||
>
|
||||
<TextOutputs
|
||||
output={output}
|
||||
index={textOutputIndex}
|
||||
setIndex={setTextOutputIndex}
|
||||
/>
|
||||
<LinkOutputs output={output} />
|
||||
</Box>
|
||||
</Paper>
|
||||
</Box>
|
||||
) : null}
|
||||
{textOutput ? (
|
||||
<Box paddingBottom={2}>
|
||||
<Box paddingBottom={2} data-testid="text-output-box">
|
||||
<InfoCard
|
||||
title={textOutput.title ?? 'Text Output'}
|
||||
noPadding
|
||||
|
||||
@@ -36,6 +36,7 @@ const scaffolderApiMock: jest.Mocked<ScaffolderApi> = {
|
||||
streamLogs: jest.fn(),
|
||||
listActions: jest.fn(),
|
||||
listTasks: jest.fn(),
|
||||
autocomplete: jest.fn(),
|
||||
};
|
||||
const catalogApiMock: jest.Mocked<CatalogApi> = {
|
||||
getEntityByRef: jest.fn(),
|
||||
|
||||
Reference in New Issue
Block a user