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,