Merge remote-tracking branch 'origin/master' into scaffolder-result-content
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
---
|
||||
'@backstage/backend-common': patch
|
||||
'@backstage/core-components': patch
|
||||
---
|
||||
|
||||
Internal refactor to avoid usage of deprecated symbols.
|
||||
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': minor
|
||||
---
|
||||
|
||||
Adds the ability to supply a `transformErrors` function to the `Stepper` for `/next`
|
||||
@@ -115,7 +115,7 @@ export function parseMysqlConnectionString(
|
||||
password,
|
||||
host: hostname,
|
||||
port: Number(port || 3306),
|
||||
database: decodeURIComponent(pathname.substr(1)),
|
||||
database: decodeURIComponent(pathname.substring(1)),
|
||||
};
|
||||
|
||||
const ssl = searchParams.get('ssl');
|
||||
|
||||
@@ -31,7 +31,6 @@ import AWSMock from 'aws-sdk-mock';
|
||||
import aws from 'aws-sdk';
|
||||
import path from 'path';
|
||||
import { NotModifiedError } from '@backstage/errors';
|
||||
import getRawBody from 'raw-body';
|
||||
|
||||
const treeResponseFactory = DefaultReadTreeResponseFactory.create({
|
||||
config: new ConfigReader({}),
|
||||
@@ -247,15 +246,16 @@ describe('AwsS3UrlReader', () => {
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket', async () => {
|
||||
const response = await reader.read(
|
||||
const { buffer } = await reader.readUrl(
|
||||
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
|
||||
);
|
||||
const response = await buffer();
|
||||
expect(response.toString().trim()).toBe('site_name: Test');
|
||||
});
|
||||
|
||||
it('rejects unknown targets', async () => {
|
||||
await expect(
|
||||
reader.read(
|
||||
reader.readUrl(
|
||||
'https://test-bucket.s3.us-east-2.NOTamazonaws.com/file.yaml',
|
||||
),
|
||||
).rejects.toThrow(
|
||||
@@ -294,21 +294,21 @@ describe('AwsS3UrlReader', () => {
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket via buffer', async () => {
|
||||
const response = await reader.readUrl!(
|
||||
const { buffer, etag } = await reader.readUrl(
|
||||
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
|
||||
);
|
||||
expect(response.etag).toBe('123abc');
|
||||
const buffer = await response.buffer();
|
||||
expect(buffer.toString().trim()).toBe('site_name: Test');
|
||||
expect(etag).toBe('123abc');
|
||||
const response = await buffer();
|
||||
expect(response.toString().trim()).toBe('site_name: Test');
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket via stream', async () => {
|
||||
const response = await reader.readUrl!(
|
||||
const { buffer, etag } = await reader.readUrl(
|
||||
'https://test-bucket.s3.us-east-2.amazonaws.com/awsS3-mock-object.yaml',
|
||||
);
|
||||
expect(response.etag).toBe('123abc');
|
||||
const fromStream = await getRawBody(response.stream!());
|
||||
expect(fromStream.toString().trim()).toBe('site_name: Test');
|
||||
expect(etag).toBe('123abc');
|
||||
const response = await buffer();
|
||||
expect(response.toString().trim()).toBe('site_name: Test');
|
||||
});
|
||||
|
||||
it('rejects unknown targets', async () => {
|
||||
@@ -354,12 +354,12 @@ describe('AwsS3UrlReader', () => {
|
||||
});
|
||||
|
||||
it('returns contents of an object in a bucket via buffer', async () => {
|
||||
const response = await reader.readUrl!(
|
||||
const { buffer, etag } = await reader.readUrl(
|
||||
'http://localhost:4566/test-bucket/awsS3-mock-object.yaml',
|
||||
);
|
||||
expect(response.etag).toBe('123abc');
|
||||
const buffer = await response.buffer();
|
||||
expect(buffer.toString().trim()).toBe('site_name: Test');
|
||||
expect(etag).toBe('123abc');
|
||||
const response = await buffer();
|
||||
expect(response.toString().trim()).toBe('site_name: Test');
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -116,8 +116,9 @@ describe('AzureUrlReader', () => {
|
||||
treeResponseFactory,
|
||||
});
|
||||
|
||||
const data = await reader.read(url);
|
||||
const res = await JSON.parse(data.toString('utf-8'));
|
||||
const { buffer } = await reader.readUrl(url);
|
||||
const fromStream = await buffer();
|
||||
const res = await JSON.parse(fromStream.toString());
|
||||
expect(res).toEqual(response);
|
||||
});
|
||||
|
||||
@@ -145,7 +146,7 @@ describe('AzureUrlReader', () => {
|
||||
logger,
|
||||
treeResponseFactory,
|
||||
});
|
||||
await reader.read(url);
|
||||
await reader.readUrl(url);
|
||||
}).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -161,7 +161,7 @@ describe('BitbucketCloudUrlReader', () => {
|
||||
describe('read', () => {
|
||||
it('rejects unknown targets', async () => {
|
||||
await expect(
|
||||
reader.read('https://not.bitbucket.com/apa'),
|
||||
reader.readUrl('https://not.bitbucket.com/apa'),
|
||||
).rejects.toThrow(
|
||||
'Incorrect URL: https://not.bitbucket.com/apa, Error: Invalid Bitbucket Cloud URL or file path',
|
||||
);
|
||||
|
||||
@@ -171,21 +171,22 @@ describe('FetchUrlReader', () => {
|
||||
|
||||
describe('read', () => {
|
||||
it('should return etag from the response', async () => {
|
||||
const buffer = await fetchUrlReader.read(
|
||||
const { buffer } = await fetchUrlReader.readUrl(
|
||||
'https://backstage.io/some-resource',
|
||||
);
|
||||
expect(buffer.toString()).toBe('content foo');
|
||||
const response = await buffer();
|
||||
expect(response.toString()).toBe('content foo');
|
||||
});
|
||||
|
||||
it('should throw NotFound if server responds with 404', async () => {
|
||||
await expect(
|
||||
fetchUrlReader.read('https://backstage.io/not-exists'),
|
||||
fetchUrlReader.readUrl('https://backstage.io/not-exists'),
|
||||
).rejects.toThrow(NotFoundError);
|
||||
});
|
||||
|
||||
it('should throw Error if server responds with 500', async () => {
|
||||
await expect(
|
||||
fetchUrlReader.read('https://backstage.io/error'),
|
||||
fetchUrlReader.readUrl('https://backstage.io/error'),
|
||||
).rejects.toThrow(Error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -92,7 +92,7 @@ describe('GithubUrlReader', () => {
|
||||
describe('implementation', () => {
|
||||
it('rejects unknown targets', async () => {
|
||||
await expect(
|
||||
githubProcessor.read('https://not.github.com/apa'),
|
||||
githubProcessor.readUrl('https://not.github.com/apa'),
|
||||
).rejects.toThrow(
|
||||
'Incorrect URL: https://not.github.com/apa, Error: Invalid GitHub URL or file path',
|
||||
);
|
||||
@@ -135,7 +135,7 @@ describe('GithubUrlReader', () => {
|
||||
),
|
||||
);
|
||||
|
||||
await gheProcessor.read(
|
||||
await gheProcessor.readUrl(
|
||||
'https://github.com/backstage/mock/tree/blob/main',
|
||||
);
|
||||
});
|
||||
|
||||
@@ -150,8 +150,9 @@ describe('GitlabUrlReader', () => {
|
||||
treeResponseFactory,
|
||||
});
|
||||
|
||||
const data = await reader.read(url);
|
||||
const res = await JSON.parse(data.toString('utf-8'));
|
||||
const { buffer } = await reader.readUrl(url);
|
||||
const fromStream = await buffer();
|
||||
const res = await JSON.parse(fromStream.toString());
|
||||
expect(res).toEqual(response);
|
||||
});
|
||||
|
||||
@@ -169,7 +170,7 @@ describe('GitlabUrlReader', () => {
|
||||
logger,
|
||||
treeResponseFactory,
|
||||
});
|
||||
await reader.read(url);
|
||||
await reader.readUrl(url);
|
||||
}).rejects.toThrow(error);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,8 +41,8 @@ describe('UrlReaderPredicateMux', () => {
|
||||
reader: barReader,
|
||||
});
|
||||
|
||||
await mux.read('http://foo/1');
|
||||
expect(fooReader.read).toHaveBeenCalledWith('http://foo/1');
|
||||
await mux.readUrl('http://foo/1');
|
||||
expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/1', undefined);
|
||||
await mux.readUrl('http://foo/2');
|
||||
expect(fooReader.readUrl).toHaveBeenCalledWith('http://foo/2', undefined);
|
||||
await mux.readTree('http://foo/3');
|
||||
@@ -50,8 +50,8 @@ describe('UrlReaderPredicateMux', () => {
|
||||
await mux.search('http://foo/4');
|
||||
expect(fooReader.search).toHaveBeenCalledWith('http://foo/4', undefined);
|
||||
|
||||
await mux.read('http://bar/1');
|
||||
expect(barReader.read).toHaveBeenCalledWith('http://bar/1');
|
||||
await mux.readUrl('http://bar/1');
|
||||
expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/1', undefined);
|
||||
await mux.readUrl('http://bar/2');
|
||||
expect(barReader.readUrl).toHaveBeenCalledWith('http://bar/2', undefined);
|
||||
await mux.readTree('http://bar/3');
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
/*
|
||||
* Copyright 2020 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 { ConfigReader } from '@backstage/config';
|
||||
import { isError } from '@backstage/errors';
|
||||
import { getVoidLogger } from '../logging';
|
||||
import { UrlReaders } from './UrlReaders';
|
||||
|
||||
const reader = UrlReaders.default({
|
||||
logger: getVoidLogger(),
|
||||
config: new ConfigReader({
|
||||
// The tokens in this config provide read only access to the backstage-verification repos
|
||||
integrations: {
|
||||
github: [
|
||||
{
|
||||
host: 'github.com',
|
||||
token:
|
||||
process.env.INTEGRATION_TEST_GITHUB_TOKEN ||
|
||||
`${86}af${617}d9c3c8bf958b37a${630691452765}bb0b0a`,
|
||||
},
|
||||
],
|
||||
gitlab: [
|
||||
{
|
||||
host: 'gitlab.com',
|
||||
token:
|
||||
process.env.INTEGRATION_TEST_GITLAB_TOKEN || 'tveGtSHDBJM9ZRHZNRfm',
|
||||
},
|
||||
],
|
||||
bitbucket: [
|
||||
{
|
||||
host: 'bitbucket.org',
|
||||
username: 'backstage-verification',
|
||||
appPassword:
|
||||
process.env.INTEGRATION_TEST_BITBUCKET_TOKEN ||
|
||||
'H79MAAhtbZwCafkVTrrQ',
|
||||
},
|
||||
],
|
||||
azure: [
|
||||
{
|
||||
host: 'dev.azure.com',
|
||||
// lasts until 2022-01-28
|
||||
token:
|
||||
process.env.INTEGRATION_TEST_AZURE_TOKEN ||
|
||||
`myvyavvfojh6wvw4ose4bfywqttqx${5}z${5}zs${5}bdxauqaek3yinkazq`,
|
||||
},
|
||||
],
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
function withRetries(count: number, fn: () => Promise<void>) {
|
||||
return async () => {
|
||||
let error;
|
||||
for (let i = 0; i < count; i++) {
|
||||
try {
|
||||
await fn();
|
||||
return;
|
||||
} catch (err) {
|
||||
error = err;
|
||||
}
|
||||
}
|
||||
if (
|
||||
isError(error) &&
|
||||
!error.message.match(/rate limit|Too Many Requests/)
|
||||
) {
|
||||
throw error;
|
||||
} else {
|
||||
console.warn('Request was rate limited', error);
|
||||
}
|
||||
};
|
||||
}
|
||||
/* eslint-disable jest/no-disabled-tests */
|
||||
describe.skip('UrlReaders', () => {
|
||||
jest.setTimeout(30_000);
|
||||
|
||||
it(
|
||||
'should read data from azure',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2Ftemplate.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-azure');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://dev.azure.com/backstage-verification/test-templates/_git/test-templates?path=%2F{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should read data from gitlab',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://gitlab.com/backstage-verification/test-templates/-/blob/master/template.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-gitlab');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://gitlab.com/backstage-verification/test-templates/-/tree/master/{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should read data from bitbucket',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/template.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-bitbucket');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://bitbucket.org/backstage-verification/test-template/src/master/{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
|
||||
it(
|
||||
'should read data from github',
|
||||
withRetries(3, async () => {
|
||||
const data = await reader.read(
|
||||
'https://github.com/backstage-verification/test-templates/blob/master/template.yaml',
|
||||
);
|
||||
expect(data.toString()).toContain('test-template-github');
|
||||
|
||||
const res = await reader.readTree(
|
||||
'https://github.com/backstage-verification/test-templates/tree/master/{{cookiecutter.name}}',
|
||||
);
|
||||
const files = await res.files();
|
||||
expect(files).toEqual([
|
||||
{
|
||||
path: 'catalog-info.yaml',
|
||||
content: expect.any(Function),
|
||||
},
|
||||
]);
|
||||
}),
|
||||
);
|
||||
});
|
||||
@@ -224,7 +224,7 @@ describe('CommonValidatorFunctions', () => {
|
||||
['abc xyz', true],
|
||||
['abc xyz abc.', true],
|
||||
])(`isValidString %p ? %p`, (value, result) => {
|
||||
expect(CommonValidatorFunctions.isValidString(value)).toBe(result);
|
||||
expect(CommonValidatorFunctions.isNonEmptyString(value)).toBe(result);
|
||||
});
|
||||
|
||||
it.each([
|
||||
|
||||
@@ -68,7 +68,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => {
|
||||
</ListItemIcon>
|
||||
<ListItemText
|
||||
primary={item.title}
|
||||
secondary={item.links?.reduce<React.ReactNodeArray>(
|
||||
secondary={item.links?.reduce<React.ReactNode[]>(
|
||||
(prev, link, idx) => [
|
||||
...prev,
|
||||
idx > 0 && <br key={idx} />,
|
||||
|
||||
@@ -11,6 +11,7 @@ import { BackstagePlugin } from '@backstage/core-plugin-api';
|
||||
import { ComponentType } from 'react';
|
||||
import { DiscoveryApi } from '@backstage/core-plugin-api';
|
||||
import { Entity } from '@backstage/catalog-model';
|
||||
import type { ErrorTransformer } from '@rjsf/utils';
|
||||
import { Extension } from '@backstage/core-plugin-api';
|
||||
import { ExternalRouteRef } from '@backstage/core-plugin-api';
|
||||
import { FetchApi } from '@backstage/core-plugin-api';
|
||||
@@ -263,6 +264,7 @@ export type NextRouterProps = {
|
||||
TaskPageComponent?: React_2.ComponentType<{}>;
|
||||
};
|
||||
groups?: TemplateGroupFilter[];
|
||||
transformErrors?: ErrorTransformer;
|
||||
};
|
||||
|
||||
// @alpha
|
||||
|
||||
@@ -30,6 +30,7 @@ import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
|
||||
import { TemplateGroupFilter } from '../TemplateListPage/TemplateGroups';
|
||||
import { nextSelectedTemplateRouteRef } from '../../routes';
|
||||
import { SecretsContextProvider } from '../../components/secrets/SecretsContext';
|
||||
import type { ErrorTransformer } from '@rjsf/utils';
|
||||
|
||||
/**
|
||||
* The Props for the Scaffolder Router
|
||||
@@ -44,6 +45,7 @@ export type NextRouterProps = {
|
||||
TaskPageComponent?: React.ComponentType<{}>;
|
||||
};
|
||||
groups?: TemplateGroupFilter[];
|
||||
transformErrors?: ErrorTransformer;
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -18,6 +18,7 @@ import { TemplateParameterSchema } from '../../../types';
|
||||
import { Stepper } from './Stepper';
|
||||
import { renderInTestApp } from '@backstage/test-utils';
|
||||
import { act, fireEvent } from '@testing-library/react';
|
||||
import type { RJSFValidationError } from '@rjsf/utils';
|
||||
|
||||
describe('Stepper', () => {
|
||||
it('should render the step titles for each step of the manifest', async () => {
|
||||
@@ -140,4 +141,50 @@ describe('Stepper', () => {
|
||||
|
||||
expect(getByText('im a custom field extension')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('should transform default error message', async () => {
|
||||
const manifest: TemplateParameterSchema = {
|
||||
steps: [
|
||||
{
|
||||
title: 'Step 1',
|
||||
schema: {
|
||||
properties: {
|
||||
postcode: {
|
||||
type: 'string',
|
||||
pattern: '[A-Z][0-9][A-Z] [0-9][A-Z][0-9]',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
title: 'transformErrors Form Test',
|
||||
};
|
||||
|
||||
const transformErrors = (errors: RJSFValidationError[]) => {
|
||||
return errors.map(err =>
|
||||
err.property === '.postcode'
|
||||
? { ...err, message: 'invalid postcode' }
|
||||
: err,
|
||||
);
|
||||
};
|
||||
|
||||
const { getByText, getByRole } = await renderInTestApp(
|
||||
<Stepper
|
||||
manifest={manifest}
|
||||
extensions={[]}
|
||||
onComplete={jest.fn()}
|
||||
transformErrors={transformErrors}
|
||||
/>,
|
||||
);
|
||||
|
||||
await fireEvent.change(getByRole('textbox', { name: 'postcode' }), {
|
||||
target: { value: 'invalid' },
|
||||
});
|
||||
|
||||
await act(async () => {
|
||||
await fireEvent.click(getByRole('button', { name: 'Review' }));
|
||||
});
|
||||
|
||||
expect(getByText('invalid postcode')).toBeInTheDocument();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -36,6 +36,7 @@ import { useTemplateSchema } from './useTemplateSchema';
|
||||
import { ReviewState } from './ReviewState';
|
||||
import validator from '@rjsf/validator-ajv8';
|
||||
import { selectedTemplateRouteRef } from '../../../routes';
|
||||
import type { ErrorTransformer } from '@rjsf/utils';
|
||||
|
||||
const useStyles = makeStyles(theme => ({
|
||||
backButton: {
|
||||
@@ -56,6 +57,7 @@ export interface StepperProps {
|
||||
manifest: TemplateParameterSchema;
|
||||
extensions: NextFieldExtensionOptions<any, any>[];
|
||||
onComplete: (values: Record<string, JsonValue>) => Promise<void>;
|
||||
transformErrors?: ErrorTransformer;
|
||||
}
|
||||
|
||||
// TODO(blam): We require here, as the types in this package depend on @rjsf/core explicitly
|
||||
@@ -149,6 +151,7 @@ export const Stepper = (props: StepperProps) => {
|
||||
onSubmit={handleNext}
|
||||
fields={extensions}
|
||||
showErrorList={false}
|
||||
transformErrors={props.transformErrors}
|
||||
>
|
||||
<div className={styles.footer}>
|
||||
<Button
|
||||
|
||||
@@ -44,9 +44,11 @@ import {
|
||||
} from '../../routes';
|
||||
import { SecretsContext } from '../../components/secrets/SecretsContext';
|
||||
import { JsonValue } from '@backstage/types';
|
||||
import type { ErrorTransformer } from '@rjsf/utils';
|
||||
|
||||
export interface TemplateWizardPageProps {
|
||||
customFieldExtensions: NextFieldExtensionOptions<any, any>[];
|
||||
transformErrors?: ErrorTransformer;
|
||||
}
|
||||
|
||||
const useStyles = makeStyles<BackstageTheme>(() => ({
|
||||
@@ -137,6 +139,7 @@ export const TemplateWizardPage = (props: TemplateWizardPageProps) => {
|
||||
manifest={manifest}
|
||||
extensions={props.customFieldExtensions}
|
||||
onComplete={onComplete}
|
||||
transformErrors={props.transformErrors}
|
||||
/>
|
||||
</InfoCard>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user