Merge pull request #6723 from backstage/mob/entity-name-picker

Add EntityNamePicker component to validate entity names in templates
This commit is contained in:
Fredrik Adelöw
2021-08-09 15:29:19 +02:00
committed by GitHub
15 changed files with 353 additions and 3 deletions
+22
View File
@@ -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`.
+1
View File
@@ -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",
+19 -3
View File
@@ -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.');
}
},
}),
);
+56
View File
@@ -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',
+8
View File
@@ -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';
+12
View File
@@ -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,