Merge pull request #6723 from backstage/mob/entity-name-picker
Add EntityNamePicker component to validate entity names in templates
This commit is contained in:
@@ -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';
|
||||
|
||||
<Route path="/create" element={<ScaffolderPage />}>
|
||||
<ScaffolderFieldExtensions>
|
||||
{/* ...custom field extensions... */}
|
||||
|
||||
+ <EntityNamePickerFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
</Route>;
|
||||
```
|
||||
|
||||
- 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`.
|
||||
@@ -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",
|
||||
|
||||
@@ -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 = (
|
||||
</Route>
|
||||
<Route path="/catalog-import" element={<CatalogImportPage />} />
|
||||
<Route path="/docs" element={<TechdocsPage />} />
|
||||
<Route path="/create" element={<ScaffolderPage />} />
|
||||
<Route path="/create" element={<ScaffolderPage />}>
|
||||
<ScaffolderFieldExtensions>
|
||||
<EntityPickerFieldExtension />
|
||||
<EntityNamePickerFieldExtension />
|
||||
<RepoUrlPickerFieldExtension />
|
||||
<OwnerPickerFieldExtension />
|
||||
<LowerCaseValuePickerFieldExtension />
|
||||
</ScaffolderFieldExtensions>
|
||||
</Route>
|
||||
<Route path="/explore" element={<ExplorePage />} />
|
||||
<Route
|
||||
path="/tech-radar"
|
||||
@@ -116,7 +133,6 @@ const routes = (
|
||||
/>
|
||||
<Route path="/graphiql" element={<GraphiQLPage />} />
|
||||
<Route path="/lighthouse" element={<LighthousePage />} />
|
||||
|
||||
<Route path="/api-docs" element={<ApiExplorerPage />} />
|
||||
<Route path="/gcp-projects" element={<GcpProjectsPage />} />
|
||||
<Route path="/newrelic" element={<NewRelicPage />} />
|
||||
|
||||
@@ -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.');
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
@@ -43,6 +43,31 @@ export type CustomFieldValidator<T> =
|
||||
},
|
||||
) => 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<string>) => 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<string>) => 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<T = any> = {
|
||||
validation?: CustomFieldValidator<T>;
|
||||
};
|
||||
|
||||
// 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<string>) => 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<string>) => 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<string>) => JSX.Element;
|
||||
|
||||
// (No @packageDocumentation comment for this package)
|
||||
```
|
||||
|
||||
@@ -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<string>) => (
|
||||
<TextValuePicker schema={{ title, description }} {...props} />
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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/,
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -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.',
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -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<string>) => (
|
||||
<TextField
|
||||
label={title}
|
||||
helperText={description}
|
||||
required={required}
|
||||
value={formData ?? ''}
|
||||
onChange={({ target: { value } }) => onChange(value)}
|
||||
margin="normal"
|
||||
error={rawErrors?.length > 0 && !formData}
|
||||
inputProps={{ autoFocus }}
|
||||
/>
|
||||
);
|
||||
@@ -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';
|
||||
@@ -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';
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user