diff --git a/.changeset/happy-lies-wink.md b/.changeset/happy-lies-wink.md new file mode 100644 index 0000000000..709cd3862e --- /dev/null +++ b/.changeset/happy-lies-wink.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +- Adds a new field `EntityNamePicker` that can be used in scaffolder templates to accept and validate an entity name. This field is registered by default, and can be used in templates by setting the `ui:field` property to `EntityNamePicker`. If you've customized your scaffolder field extensions, you can include this one by adding it when registering the scaffolder route: + +```diff +import { + ScaffolderFieldExtensions, ++ EntityNamePickerFieldExtension, +} from '@backstage/plugin-scaffolder'; + + }> + + {/* ...custom field extensions... */} + ++ + + ; +``` + +- Adds a new generic field `TextValuePicker` to be used when writing custom field extensions that use a standard UI with custom validation. An example of doing this can be found in `packages/app/src/components/scaffolder/customScaffolderExtensions.tsx`. diff --git a/packages/app/package.json b/packages/app/package.json index 1260deed1a..df69e0835a 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -61,6 +61,7 @@ }, "devDependencies": { "@backstage/test-utils": "^0.1.16", + "@rjsf/core": "^3.0.0", "@testing-library/cypress": "^7.0.1", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", diff --git a/packages/app/src/App.tsx b/packages/app/src/App.tsx index 13bdcbd2e6..10fb743304 100644 --- a/packages/app/src/App.tsx +++ b/packages/app/src/App.tsx @@ -41,7 +41,15 @@ import { GcpProjectsPage } from '@backstage/plugin-gcp-projects'; import { GraphiQLPage } from '@backstage/plugin-graphiql'; import { LighthousePage } from '@backstage/plugin-lighthouse'; import { NewRelicPage } from '@backstage/plugin-newrelic'; -import { ScaffolderPage, scaffolderPlugin } from '@backstage/plugin-scaffolder'; +import { + ScaffolderPage, + scaffolderPlugin, + ScaffolderFieldExtensions, + RepoUrlPickerFieldExtension, + OwnerPickerFieldExtension, + EntityPickerFieldExtension, + EntityNamePickerFieldExtension, +} from '@backstage/plugin-scaffolder'; import { SearchPage } from '@backstage/plugin-search'; import { TechRadarPage } from '@backstage/plugin-tech-radar'; import { TechdocsPage } from '@backstage/plugin-techdocs'; @@ -54,6 +62,7 @@ import { apis } from './apis'; import { Root } from './components/Root'; import { entityPage } from './components/catalog/EntityPage'; import { searchPage } from './components/search/SearchPage'; +import { LowerCaseValuePickerFieldExtension } from './components/scaffolder/customScaffolderExtensions'; import { providers } from './identityProviders'; import * as plugins from './plugins'; @@ -108,7 +117,15 @@ const routes = ( } /> } /> - } /> + }> + + + + + + + + } /> } /> } /> - } /> } /> } /> diff --git a/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx new file mode 100644 index 0000000000..5ef4f617a7 --- /dev/null +++ b/packages/app/src/components/scaffolder/customScaffolderExtensions.tsx @@ -0,0 +1,34 @@ +/* + * Copyright 2021 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 type { FieldValidation } from '@rjsf/core'; +import { + createScaffolderFieldExtension, + TextValuePicker, + scaffolderPlugin, +} from '@backstage/plugin-scaffolder'; + +export const LowerCaseValuePickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + name: 'LowerCaseValuePicker', + component: TextValuePicker, + validation: (value: string, validation: FieldValidation) => { + if (value.toLowerCase() !== value) { + validation.addError('Only lowercase values are allowed.'); + } + }, + }), +); diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index 690cba285b..4df2a0808c 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -43,6 +43,31 @@ export type CustomFieldValidator = }, ) => void); +// Warning: (ae-missing-release-tag) "EntityNamePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityNamePicker: ({ + schema: { title, description }, + ...props +}: FieldProps) => JSX.Element; + +// Warning: (ae-missing-release-tag) "EntityNamePickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityNamePickerFieldExtension: () => null; + +// Warning: (ae-missing-release-tag) "EntityPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EntityPicker: ({ + onChange, + schema: { title, description }, + required, + uiSchema, + rawErrors, + formData, +}: FieldProps) => JSX.Element; + // Warning: (ae-missing-release-tag) "EntityPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -57,11 +82,30 @@ export type FieldExtensionOptions = { validation?: CustomFieldValidator; }; +// Warning: (ae-missing-release-tag) "OwnerPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const OwnerPicker: ({ + schema: { title, description }, + uiSchema, + ...props +}: FieldProps) => JSX.Element; + // Warning: (ae-missing-release-tag) "OwnerPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) export const OwnerPickerFieldExtension: () => null; +// Warning: (ae-missing-release-tag) "RepoUrlPicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const RepoUrlPicker: ({ + onChange, + uiSchema, + rawErrors, + formData, +}: FieldProps) => JSX.Element; + // Warning: (ae-missing-release-tag) "RepoUrlPickerFieldExtension" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -175,5 +219,17 @@ const scaffolderPlugin: BackstagePlugin< export { scaffolderPlugin as plugin }; export { scaffolderPlugin }; +// Warning: (ae-missing-release-tag) "TextValuePicker" is exported by the package, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const TextValuePicker: ({ + onChange, + required, + schema: { title, description }, + rawErrors, + formData, + uiSchema: { 'ui:autofocus': autoFocus }, +}: FieldProps) => JSX.Element; + // (No @packageDocumentation comment for this package) ``` diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx new file mode 100644 index 0000000000..8e7c0b0bea --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/EntityNamePicker.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2021 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 { FieldProps } from '@rjsf/core'; +import { TextValuePicker } from '../TextValuePicker'; + +export const EntityNamePicker = ({ + schema: { title = 'Name', description = 'Unique name of the component' }, + ...props +}: FieldProps) => ( + +); diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts new file mode 100644 index 0000000000..0ba88d1277 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 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. + */ +export { EntityNamePicker } from './EntityNamePicker'; +export { entityNamePickerValidation } from './validation'; diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.test.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.test.ts new file mode 100644 index 0000000000..2006a87520 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.test.ts @@ -0,0 +1,65 @@ +/* + * Copyright 2021 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 { FieldValidation } from '@rjsf/core'; +import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; +import { entityNamePickerValidation } from './validation'; + +jest.mock('@backstage/catalog-model', () => ({ + KubernetesValidatorFunctions: { + isValidObjectName: jest.fn(), + }, +})); + +const mockIsValidObjectName = KubernetesValidatorFunctions.isValidObjectName as jest.MockedFunction< + typeof KubernetesValidatorFunctions.isValidObjectName +>; + +describe('EntityNamePicker Validation', () => { + let mockFieldValidation: FieldValidation; + + beforeEach(() => { + mockFieldValidation = ({ + addError: jest.fn(), + } as unknown) as FieldValidation; + }); + + it('calls isValidObjectName to validate value', () => { + entityNamePickerValidation('test value', mockFieldValidation); + + expect(mockIsValidObjectName).toHaveBeenCalled(); + }); + + it('does not add an error when isValidObjectName returns true', () => { + mockIsValidObjectName.mockReturnValue(true); + + entityNamePickerValidation('test value', mockFieldValidation); + + expect(mockFieldValidation.addError).not.toHaveBeenCalled(); + }); + + it('adds an error when isValidObjectName returns false', () => { + mockIsValidObjectName.mockReturnValue(false); + + entityNamePickerValidation('test value', mockFieldValidation); + + expect(mockFieldValidation.addError).toHaveBeenCalledWith( + expect.stringMatching( + /contain only alphanumeric characters, hyphens, underscores, and periods/, + ), + ); + }); +}); diff --git a/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts new file mode 100644 index 0000000000..7a40a460ba --- /dev/null +++ b/plugins/scaffolder/src/components/fields/EntityNamePicker/validation.ts @@ -0,0 +1,29 @@ +/* + * Copyright 2021 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 { FieldValidation } from '@rjsf/core'; +import { KubernetesValidatorFunctions } from '@backstage/catalog-model'; + +export const entityNamePickerValidation = ( + value: string, + validation: FieldValidation, +) => { + if (!KubernetesValidatorFunctions.isValidObjectName(value)) { + validation.addError( + 'must start and end with an alphanumeric character, and contain only alphanumeric characters, hyphens, underscores, and periods. Maximum length is 63 characters.', + ); + } +}; diff --git a/plugins/scaffolder/src/components/fields/TextValuePicker/TextValuePicker.tsx b/plugins/scaffolder/src/components/fields/TextValuePicker/TextValuePicker.tsx new file mode 100644 index 0000000000..f391843f73 --- /dev/null +++ b/plugins/scaffolder/src/components/fields/TextValuePicker/TextValuePicker.tsx @@ -0,0 +1,38 @@ +/* + * Copyright 2021 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 { FieldProps } from '@rjsf/core'; +import { TextField } from '@material-ui/core'; + +export const TextValuePicker = ({ + onChange, + required, + schema: { title, description }, + rawErrors, + formData, + uiSchema: { 'ui:autofocus': autoFocus }, +}: FieldProps) => ( + onChange(value)} + margin="normal" + error={rawErrors?.length > 0 && !formData} + inputProps={{ autoFocus }} + /> +); diff --git a/plugins/scaffolder/src/components/fields/TextValuePicker/index.ts b/plugins/scaffolder/src/components/fields/TextValuePicker/index.ts new file mode 100644 index 0000000000..8ab810b6ed --- /dev/null +++ b/plugins/scaffolder/src/components/fields/TextValuePicker/index.ts @@ -0,0 +1,16 @@ +/* + * Copyright 2021 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. + */ +export { TextValuePicker } from './TextValuePicker'; diff --git a/plugins/scaffolder/src/components/fields/index.ts b/plugins/scaffolder/src/components/fields/index.ts index 3d7875a742..5adc3d3141 100644 --- a/plugins/scaffolder/src/components/fields/index.ts +++ b/plugins/scaffolder/src/components/fields/index.ts @@ -13,6 +13,8 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +export * from './EntityNamePicker'; export * from './EntityPicker'; export * from './OwnerPicker'; export * from './RepoUrlPicker'; +export * from './TextValuePicker'; diff --git a/plugins/scaffolder/src/extensions/default.ts b/plugins/scaffolder/src/extensions/default.ts index b7af2f4a6b..be934546b3 100644 --- a/plugins/scaffolder/src/extensions/default.ts +++ b/plugins/scaffolder/src/extensions/default.ts @@ -14,6 +14,10 @@ * limitations under the License. */ import { EntityPicker } from '../components/fields/EntityPicker'; +import { + EntityNamePicker, + entityNamePickerValidation, +} from '../components/fields/EntityNamePicker'; import { OwnerPicker } from '../components/fields/OwnerPicker'; import { repoPickerValidation, @@ -26,6 +30,11 @@ export const DEFAULT_SCAFFOLDER_FIELD_EXTENSIONS: FieldExtensionOptions[] = [ component: EntityPicker, name: 'EntityPicker', }, + { + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + }, { component: RepoUrlPicker, name: 'RepoUrlPicker', diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 74091a1a85..989b3b53ce 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -23,9 +23,17 @@ export { export type { CustomFieldValidator, FieldExtensionOptions } from './extensions'; export { EntityPickerFieldExtension, + EntityNamePickerFieldExtension, OwnerPickerFieldExtension, RepoUrlPickerFieldExtension, ScaffolderPage, scaffolderPlugin as plugin, scaffolderPlugin, } from './plugin'; +export { + EntityNamePicker, + EntityPicker, + OwnerPicker, + RepoUrlPicker, + TextValuePicker, +} from './components/fields'; diff --git a/plugins/scaffolder/src/plugin.ts b/plugins/scaffolder/src/plugin.ts index e12305639e..8b543b52eb 100644 --- a/plugins/scaffolder/src/plugin.ts +++ b/plugins/scaffolder/src/plugin.ts @@ -17,6 +17,10 @@ import { scmIntegrationsApiRef } from '@backstage/integration-react'; import { scaffolderApiRef, ScaffolderClient } from './api'; import { EntityPicker } from './components/fields/EntityPicker'; +import { + entityNamePickerValidation, + EntityNamePicker, +} from './components/fields/EntityNamePicker'; import { OwnerPicker } from './components/fields/OwnerPicker'; import { repoPickerValidation, @@ -61,6 +65,14 @@ export const EntityPickerFieldExtension = scaffolderPlugin.provide( }), ); +export const EntityNamePickerFieldExtension = scaffolderPlugin.provide( + createScaffolderFieldExtension({ + component: EntityNamePicker, + name: 'EntityNamePicker', + validation: entityNamePickerValidation, + }), +); + export const RepoUrlPickerFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ component: RepoUrlPicker,