Merge branch 'master' of https://github.com/backstage/backstage into update/review-explode

This commit is contained in:
Stephen Glass
2024-07-14 15:25:41 -04:00
814 changed files with 25776 additions and 5985 deletions
+52
View File
@@ -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
+11
View File
@@ -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>;
+5 -5
View File
@@ -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}
/>
);
},
@@ -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();
});
});
});
@@ -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={[
[
@@ -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();
});
});
@@ -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(),