커스텀 필드 확장 작성
info
출처: 문서
본문
info
이 문서는 새 Backstage 앱에서 기본인 새 프론트엔드 시스템을 위해 작성되었습니다. 여러분의 Backstage 앱이 여전히 이전 프론트엔드 시스템을 사용한다면, 이 가이드의 이전 프론트엔드 시스템 버전을 대신 읽으세요.
사용자로부터 입력을 수집하는 것은 스캐폴딩 과정과 소프트웨어 템플릿 전체에서 아주 큰 부분입니다. 때로는 내장 컴포넌트와 필드가 충분하지 않을 때가 있고, 때로는 더 잘 맞는 더 나은 입력으로 사용자가 보는 폼을 풍부하게 만들고 싶을 때가 있습니다.
여기서 Custom Field Extensions(커스텀 필드 확장)이 등장합니다.
이것들을 사용하면 자신만의 React 컴포넌트를 보여주고, 그것을 사용해 JSON 스키마의 상태를 제어할 수 있으며, 데이터를 검증하기 위한 자신만의 검증 함수도 제공할 수 있습니다.
필드 확장 만들기
필드 확장은 ID, React 컴포넌트, validation 함수를 모듈식으로 결합하는 방법으로, 그런 다음 Backstage 앱에서 확장으로 등록할 수 있습니다.
@backstage/plugin-scaffolder-react/alpha의 FormFieldBlueprint를 createFormField와 함께 사용해 나만의 필드 확장을 만들 수 있습니다. createFormField는 컴포넌트, 검증, 선택적 스키마를 타입화합니다.
예시로, 문자열이 Kebab-case 패턴에 있는지 검증하는 컴포넌트를 만들겠습니다.
먼저 컴포넌트와 검증 함수를 만듭니다.
// packages/app/src/scaffolder/ValidateKebabCase/ValidateKebabCaseExtension.tsximport { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';import type { FieldValidation } from '@rjsf/utils';import FormControl from '@material-ui/core/FormControl';import FormHelperText from '@material-ui/core/FormHelperText';import Input from '@material-ui/core/Input';import InputLabel from '@material-ui/core/InputLabel';export const ValidateKebabCase = ({ onChange, rawErrors, required, formData,}: FieldExtensionComponentProps<string>) => { return ( <FormControl margin="normal" required={required} error={rawErrors?.length > 0 && !formData} > <InputLabel htmlFor="validateName">Name</InputLabel> <Input id="validateName" aria-describedby="entityName" onChange={e => onChange(e.target?.value)} /> <FormHelperText id="entityName"> Use only letters, numbers, hyphens and underscores </FormHelperText> </FormControl> );};export const validateKebabCaseValidation = ( value: string, validation: FieldValidation,) => { const kebabCase = /^[a-z0-9-_]+$/g.test(value); if (kebabCase === false) { validation.addError( `Only use letters, numbers, hyphen ("-") and underscore ("_").`, ); }};
그런 다음 FormFieldBlueprint와 createFormField를 사용해 확장을 만듭니다.
// packages/app/src/scaffolder/ValidateKebabCase/extensions.tsimport { FormFieldBlueprint, createFormField,} from '@backstage/plugin-scaffolder-react/alpha';import { ValidateKebabCase, validateKebabCaseValidation,} from './ValidateKebabCaseExtension';export const ValidateKebabCaseFieldExtension = FormFieldBlueprint.make({ name: 'validate-kebab-case', params: { field: async () => createFormField({ name: 'ValidateKebabCase', component: ValidateKebabCase, validation: validateKebabCaseValidation, }), },});
// packages/app/src/scaffolder/ValidateKebabCase/index.tsexport { ValidateKebabCaseFieldExtension } from './extensions';
확장이 생성되면, 프론트엔드 모듈로 감싸고 createApp에 전달해 앱에 설치합니다.
packages/app/src/scaffolder/scaffolderModule.ts
import { createFrontendModule } from '@backstage/frontend-plugin-api';import { ValidateKebabCaseFieldExtension } from './ValidateKebabCase';export const scaffolderCustomizations = createFrontendModule({ pluginId: 'scaffolder', extensions: [ValidateKebabCaseFieldExtension],});
packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-defaults';import { scaffolderCustomizations } from './scaffolder/scaffolderModule';const app = createApp({ features: [scaffolderCustomizations],});export default app.createRoot();
비동기 검증 함수
검증 함수는 비동기일 수 있고, 필드 검증 컨텍스트의 ApiHolder를 통해 Utility API를 사용할 수 있습니다. 아래 예시는 catalogApiRef를 사용해 제출된 값(이 시나리오에서는 엔티티 참조)이 카탈로그에 존재하는지 확인합니다.
import { FieldValidation } from '@rjsf/utils';import { ApiHolder } from '@backstage/core-plugin-api';import { catalogApiRef } from '@backstage/plugin-catalog-react';export const customFieldExtensionValidator = async ( value: string, validation: FieldValidation, context: { apiHolder: ApiHolder },) => { const catalogApi = context.apiHolder.get(catalogApiRef); if ((await catalogApi?.getEntityByRef(value)) === undefined) { validation.addError('Entity not found'); }};
커스텀 필드 확장 사용
등록되면, 템플릿에서 ui:field 속성을 사용해 커스텀 필드 확장의 이름을 참조할 수 있습니다.
apiVersion: scaffolder.backstage.io/v1beta3kind: Templatemetadata: name: Test template title: Test template with custom extension description: Test templatespec: parameters: - title: Fill in some steps required: - name properties: name: title: Name type: string description: My custom name for the component ui:field: ValidateKebabCase steps: [...]
다른 필드의 데이터 접근
커스텀 필드 확장은 폼 컨텍스트를 통해 폼의 다른 필드에서 데이터를 읽을 수 있습니다. 이것은 결합을 만든다는 점에서 권장되지 않지만, 때로는 여전히 가장 합리적인 해결책입니다.
import { FormFieldBlueprint, createFormField,} from '@backstage/plugin-scaffolder-react/alpha';import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';const CustomFieldExtensionComponent = (props: FieldExtensionComponentProps<string[]>) => { const { formData } = props.formContext; ...};const CustomFieldExtension = FormFieldBlueprint.make({ name: 'custom-field', params: { field: async () => createFormField({ name: 'custom-field', component: CustomFieldExtensionComponent, validation: ..., }), },});
커스텀 필드 확장 미리보기
Custom Field Explorer(기본적으로 /create/edit 라우트로 접근)를 사용해 Backstage UI에서 작성한 커스텀 필드 확장을 미리 볼 수 있습니다.
탐색기에서 새 커스텀 필드 확장을 사용 가능하게 하려면, 필드의 입력/출력 타입을 설명하는 JSON 스키마를 다음과 같은 예시처럼 정의해야 합니다.
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsximport FormControl from '@material-ui/core/FormControl';import { FieldExtensionComponentProps } from '@backstage/plugin-scaffolder-react';export const MyCustomExtensionWithOptionsSchema = { uiOptions: { type: 'object', properties: { focused: { description: 'Whether to focus this field', type: 'boolean', }, }, }, returnValue: { type: 'string' },};export const MyCustomExtensionWithOptions = ({ onChange, rawErrors, required, formData, uiSchema,}: FieldExtensionComponentProps<string, { focused?: boolean }>) => { const focused = uiSchema['ui:options']?.focused; return ( <FormControl margin="normal" required={required} error={rawErrors?.length > 0 && !formData} onChange={onChange} focused={focused} /> );};
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.tsimport { FormFieldBlueprint, createFormField,} from '@backstage/plugin-scaffolder-react/alpha';import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema,} from './MyCustomExtensionWithOptions';export const MyCustomFieldWithOptionsExtension = FormFieldBlueprint.make({ name: 'MyCustomExtensionWithOptions', params: { field: async () => createFormField({ name: 'MyCustomExtensionWithOptions', component: MyCustomExtensionWithOptions, schema: MyCustomExtensionWithOptionsSchema, }), },});
스키마를 정의하고 제공된 makeFieldSchemaFromZod 헬퍼 유틸리티 함수를 사용해 필드 props에 대한 JSON 스키마와 타입을 모두 생성해 정의를 중복하지 않도록, zod 같은 라이브러리를 사용할 것을 권장합니다.
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsximport FormControl from '@material-ui/core/FormControl';import { z } from 'zod/v3';import { makeFieldSchemaFromZod } from '@backstage/plugin-scaffolder';const MyCustomExtensionWithOptionsFieldSchema = makeFieldSchemaFromZod( z.string(), z.object({ focused: z.boolean().optional().describe('Whether to focus this field'), }),);export const MyCustomExtensionWithOptionsSchema = MyCustomExtensionWithOptionsFieldSchema.schema;type MyCustomExtensionWithOptionsProps = typeof MyCustomExtensionWithOptionsFieldSchema.type;export const MyCustomExtensionWithOptions = ({ onChange, rawErrors, required, formData, uiSchema,}: MyCustomExtensionWithOptionsProps) => { const focused = uiSchema['ui:options']?.focused; return ( <FormControl margin="normal" required={required} error={rawErrors?.length > 0 && !formData} onChange={onChange} focused={focused} /> );};