diff --git a/.changeset/serious-windows-occur.md b/.changeset/serious-windows-occur.md new file mode 100644 index 0000000000..37f778e846 --- /dev/null +++ b/.changeset/serious-windows-occur.md @@ -0,0 +1,6 @@ +--- +'@backstage/backend-common': patch +'@backstage/core-components': patch +--- + +Internal refactor to avoid usage of deprecated symbols. diff --git a/.changeset/sour-flowers-care.md b/.changeset/sour-flowers-care.md new file mode 100644 index 0000000000..8524c4981c --- /dev/null +++ b/.changeset/sour-flowers-care.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': minor +--- + +Adds the ability to supply a `transformErrors` function to the `Stepper` for `/next` diff --git a/packages/backend-common/src/database/connectors/mysql.ts b/packages/backend-common/src/database/connectors/mysql.ts index b75dc3d99c..4283ff42bd 100644 --- a/packages/backend-common/src/database/connectors/mysql.ts +++ b/packages/backend-common/src/database/connectors/mysql.ts @@ -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'); diff --git a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts index 454b8a2c5a..4a2be1fc74 100644 --- a/packages/backend-common/src/reading/AwsS3UrlReader.test.ts +++ b/packages/backend-common/src/reading/AwsS3UrlReader.test.ts @@ -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'); }); }); diff --git a/packages/backend-common/src/reading/AzureUrlReader.test.ts b/packages/backend-common/src/reading/AzureUrlReader.test.ts index bc088d8bc9..f969b3b7eb 100644 --- a/packages/backend-common/src/reading/AzureUrlReader.test.ts +++ b/packages/backend-common/src/reading/AzureUrlReader.test.ts @@ -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); }); }); diff --git a/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts index 12ace2755b..ca6e15a005 100644 --- a/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts +++ b/packages/backend-common/src/reading/BitbucketCloudUrlReader.test.ts @@ -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', ); diff --git a/packages/backend-common/src/reading/FetchUrlReader.test.ts b/packages/backend-common/src/reading/FetchUrlReader.test.ts index 75172e83b0..e45117a106 100644 --- a/packages/backend-common/src/reading/FetchUrlReader.test.ts +++ b/packages/backend-common/src/reading/FetchUrlReader.test.ts @@ -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); }); }); diff --git a/packages/backend-common/src/reading/GithubUrlReader.test.ts b/packages/backend-common/src/reading/GithubUrlReader.test.ts index a5f9d26de5..2315d1f9e8 100644 --- a/packages/backend-common/src/reading/GithubUrlReader.test.ts +++ b/packages/backend-common/src/reading/GithubUrlReader.test.ts @@ -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', ); }); diff --git a/packages/backend-common/src/reading/GitlabUrlReader.test.ts b/packages/backend-common/src/reading/GitlabUrlReader.test.ts index 29ef3f4ce8..012f8978ae 100644 --- a/packages/backend-common/src/reading/GitlabUrlReader.test.ts +++ b/packages/backend-common/src/reading/GitlabUrlReader.test.ts @@ -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); }); }); diff --git a/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts index 5a11e068df..38473557e0 100644 --- a/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts +++ b/packages/backend-common/src/reading/UrlReaderPredicateMux.test.ts @@ -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'); diff --git a/packages/backend-common/src/reading/integration.test.ts b/packages/backend-common/src/reading/integration.test.ts deleted file mode 100644 index 951df29605..0000000000 --- a/packages/backend-common/src/reading/integration.test.ts +++ /dev/null @@ -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) { - 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), - }, - ]); - }), - ); -}); diff --git a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts index 9a9ec8bcda..a887a9aa2e 100644 --- a/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts +++ b/packages/catalog-model/src/validation/CommonValidatorFunctions.test.ts @@ -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([ diff --git a/packages/core-components/src/components/SupportButton/SupportButton.tsx b/packages/core-components/src/components/SupportButton/SupportButton.tsx index 01de79bdee..4e0bb866f5 100644 --- a/packages/core-components/src/components/SupportButton/SupportButton.tsx +++ b/packages/core-components/src/components/SupportButton/SupportButton.tsx @@ -68,7 +68,7 @@ const SupportListItem = ({ item }: { item: SupportItem }) => { ( + secondary={item.links?.reduce( (prev, link, idx) => [ ...prev, idx > 0 &&
, diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index bc6643cffe..33a5f8c2fe 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -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'; @@ -262,6 +263,7 @@ export type NextRouterProps = { TaskPageComponent?: React_2.ComponentType<{}>; }; groups?: TemplateGroupFilter[]; + transformErrors?: ErrorTransformer; }; // @alpha diff --git a/plugins/scaffolder/src/next/Router/Router.tsx b/plugins/scaffolder/src/next/Router/Router.tsx index ebe00fdfb8..0fc1c9eceb 100644 --- a/plugins/scaffolder/src/next/Router/Router.tsx +++ b/plugins/scaffolder/src/next/Router/Router.tsx @@ -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; }; /** diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx index 12ad05ab75..bf82fecf49 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.test.tsx @@ -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( + , + ); + + 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(); + }); }); diff --git a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx index 75f92916df..de06655aa1 100644 --- a/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx +++ b/plugins/scaffolder/src/next/TemplateWizardPage/Stepper/Stepper.tsx @@ -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[]; onComplete: (values: Record) => Promise; + 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} >