본문 바로가기
WIKI 기술 지식 베이스

커스텀 필드 확장 작성

원문 보기 위키 갱신

커스텀 필드 확장 작성 (이전 프론트엔드 시스템)

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}    />  );};

더 알아보기 (Learn more)