커스텀 필드 확장 작성
커스텀 필드 확장 작성 (이전 프론트엔드 시스템)
info
출처: 문서
본문
info
이 문서는 여전히 이전 프론트엔드 시스템을 사용하는 Backstage 앱을 위한 것입니다. 앱이 새 프론트엔드 시스템을 사용한다면, 현재 가이드를 대신 읽으세요.
사용자로부터 입력을 수집하는 것은 스캐폴딩 과정과 소프트웨어 템플릿 전체에서 아주 큰 부분입니다. 때로는 내장 컴포넌트와 필드가 충분하지 않을 때가 있고, 때로는 더 잘 맞는 더 나은 입력으로 사용자가 보는 폼을 풍부하게 만들고 싶을 때가 있습니다.
여기서 Custom Field Extensions(커스텀 필드 확장)이 등장합니다.
이것들을 사용하면 자신만의 React 컴포넌트를 보여주고, 그것을 사용해 JSON 스키마의 상태를 제어할 수 있으며, 데이터를 검증하기 위한 자신만의 검증 함수도 제공할 수 있습니다.
필드 확장 만들기
필드 확장은 ID, React 컴포넌트, validation 함수를 모듈식으로 결합하는 방법으로, 그런 다음 자신의 App.tsx에서 Scaffolder 프론트엔드 플러그인에 전달할 수 있습니다.
아래처럼 createScaffolderFieldExtension API를 사용해 나만의 필드 확장을 만들 수 있습니다.
예시로, 문자열이 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';/* This is the actual component that will get rendered in the form*/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> );};/* This is a validation function that will run when the form is submitted. You will get the value from the `onChange` handler before as the value here to make sure that the types are aligned*/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 ("_").`, ); }};
// packages/app/src/scaffolder/ValidateKebabCase/extensions.ts/* This is where the magic happens and creates the custom field extension. Note that if you're writing extensions part of a separate plugin, then please use `scaffolderPlugin.provide` from there instead and export it part of your `plugin.ts` rather than re-using the `scaffolder.plugin`.*/import { scaffolderPlugin } from '@backstage/plugin-scaffolder';import { createScaffolderFieldExtension } from '@backstage/plugin-scaffolder-react';import { ValidateKebabCase, validateKebabCaseValidation,} from './ValidateKebabCaseExtension';export const ValidateKebabCaseFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ name: 'ValidateKebabCase', component: ValidateKebabCase, validation: validateKebabCaseValidation, }),);
// packages/app/src/scaffolder/ValidateKebabCase/index.tsexport { ValidateKebabCaseFieldExtension } from './extensions';
이 모든 파일이 자리 잡으면, 커스텀 확장을 scaffolder 플러그인에 제공해야 합니다.
이것은 packages/app/src/App.tsx에서 합니다. customFieldExtensions를 ScaffolderPage의 자식으로 제공해야 합니다.
const routes = ( <FlatRoutes> ... <Route path="/create" element={<ScaffolderPage />} /> ... </FlatRoutes>);
이렇게 보여야 합니다.
import { ValidateKebabCaseFieldExtension } from './scaffolder/ValidateKebabCase';import { ScaffolderFieldExtensions } from '@backstage/plugin-scaffolder-react';const routes = ( <FlatRoutes> ... <Route path="/create" element={<ScaffolderPage />}> <ScaffolderFieldExtensions> <ValidateKebabCaseFieldExtension /> </ScaffolderFieldExtensions> </Route> ... </FlatRoutes>);
비동기 검증 함수
검증 함수는 비동기일 수 있고, 필드 검증 컨텍스트의 ApiHolder를 통해 Utility API를 사용할 수 있습니다. 아래 예시는 catalogApiRef를 사용해 제출된 값(이 시나리오에서는 엔티티 참조)이 카탈로그에 존재하는지 확인합니다.
import { FieldValidation } from '@rjsf/utils';import { ApiHolder } from '@backstage/core-plugin-api';import { catalogApiRef } from '@backstage/plugin-catalog-react';/* This validation function checks if the submitted entity ref value is present in the catalog.*/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'); }};
커스텀 필드 확장 사용
ScaffolderPage에 전달되면, 템플릿에서 ui:field 속성을 사용해 등록한 customFieldExtension의 이름을 가리킬 수 있습니다.
이런 식입니다.
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: [...]
다른 필드의 데이터 접근
커스텀 필드 확장은 폼 컨텍스트를 통해 폼의 다른 필드에서 데이터를 읽을 수 있습니다. 이것은 결합을 만든다는 점에서 권장되지 않지만, 때로는 여전히 가장 합리적인 해결책입니다.
const CustomFieldExtensionComponent = (props: FieldExtensionComponentProps<string[]>) => { const { formData } = props.formContext; ...};const CustomFieldExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ name: ..., component: CustomFieldExtensionComponent, validation: ... }));
커스텀 필드 확장 미리보기
Custom Field Explorer(기본적으로 /create/edit 라우트로 접근)를 사용해 Backstage UI에서 작성한 커스텀 필드 확장을 미리 볼 수 있습니다.
탐색기에서 새 커스텀 필드 확장을 사용 가능하게 하려면, 필드의 입력/출력 타입을 설명하는 JSON 스키마를 다음과 같은 예시처럼 정의해야 합니다.
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsxexport 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,}: FieldExtensionComponentProps<string, { focused?: boolean }>) => { return ( <FormControl margin="normal" required={required} error={rawErrors?.length > 0 && !formData} onChange={onChange} focused={focused} /> );};
// packages/app/src/scaffolder/MyCustomExtensionWithOptions/extensions.ts...import { MyCustomExtensionWithOptions, MyCustomExtensionWithOptionsSchema } from './MyCustomExtensionWithOptions';export const MyCustomFieldWithOptionsExtension = scaffolderPlugin.provide( createScaffolderFieldExtension({ name: 'MyCustomExtensionWithOptions', component: MyCustomExtensionWithOptions, schema: MyCustomExtensionWithOptionsSchema, }),);
스키마를 정의하고 제공된 makeFieldSchemaFromZod 헬퍼 유틸리티 함수를 사용해 필드 props에 대한 JSON 스키마와 타입을 모두 생성해 정의를 중복하지 않도록, zod 같은 라이브러리를 사용할 것을 권장합니다.
//packages/app/src/scaffolder/MyCustomExtensionWithOptions/MyCustomExtensionWithOptions.tsx...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,}: MyCustomExtensionWithOptionsProps) => { return ( <FormControl margin="normal" required={required} error={rawErrors?.length > 0 && !formData} onChange={onChange} focused={focused} /> );};