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

카탈로그 사용자 지정

원문 보기 위키 갱신

카탈로그 사용자 지정 (이전 프론트엔드 시스템)

이전 프론트엔드 시스템을 사용하는 Backstage 앱에서 카탈로그 index 페이지를 사용자 지정하는 방법을 설명하는 가이드예요.

출처: 문서

본문

:::info 이 문서는 여전히 이전 프론트엔드 시스템을 사용하는 Backstage 앱을 위한 것이에요. 앱이 새 프론트엔드 시스템을 사용한다면 현재 가이드를 읽으세요. :::

Backstage 소프트웨어 카탈로그에는 카탈로그 엔티티를 필터링하고 찾기 위한 기본 CatalogIndexPage가 제공돼요. 이는 @backstage/create-app이 기본으로 이미 설정해 줘요. 기본 index 페이지를 변경하려면 — 초기에 선택된 필터를 설정하거나, 열을 조정하거나, 동작을 추가하거나, 카탈로그에 사용자 지정 필터를 추가하려면 — 다음 섹션들이 방법을 보여줘요.

페이지네이션 (Pagination)

CatalogIndexPage 페이지네이션의 초기 지원은 Backstage v1.21.0에서 추가됐어요. 이 기능을 사용하려면 해당 버전 이상인지 확인하세요. 페이지네이션을 활성화하려면 다음과 같이 pagination prop을 전달하면 돼요.

packages/app/src/App.tsx

<Route path="/catalog" element={<CatalogIndexPage pagination />} />

내보내기 (Export)

카탈로그 내보내기를 활성화하려면 enabled: true를 가진 exportSettings prop을 전달해야 해요.

packages/app/src/App.tsx

<Route  path="/catalog"  element={<CatalogIndexPage exportSettings={{ enabled: true }} />}/>

이렇게 하면 카탈로그 테이블에서 데이터를 CSV와 JSON 형식으로 내보낼 수 있는 옵션이 기본으로 포함된 버튼이 활성화돼요. 내보낼 때 사용자가 내보내기 형식을 선택하고 어떤 열을 포함할지 선택할 수 있는 대화상자가 열려요.

내보내기 사용자 지정

CatalogExportSettings 인터페이스를 통해 exportSettings prop에 다양한 옵션을 구성해 내보내기 동작을 사용자 지정할 수 있어요.

export interface CatalogExportSettings {  enabled?: boolean;  /**   * Array of columns to include in the export.   *   * Each column requires an `entityFilterKey` (dot-separated path into the entity object that is returned by the catalog api) and an optional `title` for display.   * When `title` is omitted, `entityFilterKey` is used as the display title.   *   * Default columns are: name, type, owner and description.   **/  columns?: CatalogExportSettingsColumn[];  /**   * Map of custom export format handlers.   *   * Each map entry provides an exporter function and an optional display label.   * Custom formats appear in the export dialog alongside built-in CSV and JSON options.   **/  exporters?: Record<string, CatalogExporterConfig>;  /** Callback function invoked after successful export completion. Useful for displaying notifications or triggering post-export actions. */  onSuccess?: () => void;  /** Callback function invoked if export fails. Receives an object containing the Error for error handling and user notification. */  onError?: (options: { error: Error }) => void;  /** When true, hides the built-in CSV and JSON export options. Useful when only custom exporters should be available. */  disableBuiltinExporters?: boolean;}

사용자 지정 내보내기 열

기본적으로 내보내기에는 name, type, owner, description 열이 포함돼요. 내보내기 대화상자가 열리면 모든 구성된 열이 체크박스로 표시되고 미리 선택돼요. 사용자는 내보내기를 확정하기 전에 제외하려는 열을 선택 해제할 수 있어요. 사용 가능한 열을 사용자 지정할 수 있어요.

packages/app/src/App.tsx

import { CatalogIndexPage } from '@backstage/plugin-catalog';const customColumns = [  { entityFilterKey: 'metadata.name', title: 'Name' },  { entityFilterKey: 'metadata.namespace', title: 'Namespace' },  { entityFilterKey: 'spec.owner', title: 'Owner' },];<CatalogIndexPage  exportSettings={{    enabled: true,    columns: customColumns,  }}/>;

사용자 지정 내보내기 형식

CSV와 JSON 외에 사용자 지정 내보내기 형식 유형을 추가할 수 있어요. 사용자 지정 exporter는 async generator를 사용해 스트리밍 다운로드를 가능하게 해요. 데이터는 생성됨에 따라 디스크에 기록되며, 지원되는 브라우저에서는 전체 내보내기를 메모리에 버퍼링하지 않아요.

packages/app/src/App.tsx

import {  CatalogIndexPage,  CatalogExporter,  CatalogExporterConfig,} from '@backstage/plugin-catalog';import { catalogApiRef } from '@backstage/plugin-catalog-react';// Custom exporter using async generator for streamingconst xmlExporter: CatalogExporter = ({ apis, columns, streamRequest }) => {  const catalogApi = apis.get(catalogApiRef);  // Return an async generator that yields XML chunks  async function* generateXml() {    yield '<?xml version="1.0" encoding="UTF-8"?>\n<entities>\n';    for await (const page of catalogApi.streamEntities(streamRequest)) {      for (const entity of page) {        // Serialize each entity to XML and yield immediately        yield serializeEntityToXml(entity, columns);      }    }    yield '</entities>';  }  return {    generator: generateXml(),    contentType: 'application/xml',  };};const yamlExporter: CatalogExporter = ({ apis, columns, streamRequest }) => {  const catalogApi = apis.get(catalogApiRef);  async function* generateYaml() {    for await (const page of catalogApi.streamEntities(streamRequest)) {      for (const entity of page) {        yield serializeEntityToYaml(entity, columns);        yield '---\n'; // YAML document separator      }    }  }  return {    generator: generateYaml(),    contentType: 'application/x-yaml',  };};const exporters: Record<string, CatalogExporterConfig> = {  xml: { exporter: xmlExporter, label: 'XML' },  yaml: { exporter: yamlExporter, label: 'YAML' },};<CatalogIndexPage  exportSettings={{    enabled: true,    exporters,  }}/>;

사용자 지정 내보내기 형식이 제공되면 내장된 CSV와 JSON 옵션과 함께 내보내기 대화상자에 나타나요.

성공/실패 콜백

성공하거나 실패한 내보내기를 처리하기 위해 콜백을 제공할 수도 있어요.

packages/app/src/App.tsx

<CatalogIndexPage  exportSettings={{    enabled: true,    onSuccess: () => {      // Handle successful export      notificationApi.success({ message: 'Export completed!' });    },    onError: ({ error }) => {      // Handle export error      notificationApi.error({        message: `Export failed: ${error.message}`,      });    },  }}/>

결합 예시

모든 사용자 지정 옵션을 결합한 예시는 다음과 같아요.

packages/app/src/App.tsx

<CatalogIndexPage  exportSettings={{    enabled: true,    columns: [      { entityFilterKey: 'metadata.name', title: 'Name' },      { entityFilterKey: 'spec.type', title: 'Type' },      { entityFilterKey: 'spec.owner', title: 'Owner' },      { entityFilterKey: 'metadata.namespace', title: 'Namespace' },    ],    exporters: {      xml: { exporter: xmlExporter, label: 'XML' },      yaml: { exporter: yamlExporter, label: 'YAML' },    },    onSuccess: () => {      notificationApi.success({ message: 'Export completed!' });    },    onError: ({ error }) => {      notificationApi.error({        message: `Export failed: ${error.message}`,      });    },  }}/>

초기에 선택된 필터 (Initially Selected Filter)

기본적으로 초기에 선택된 필터는 Owned로 설정돼요. 아직 카탈로그를 구축 중이라면 처음에 빈 목록이 표시될 수 있어요. 기본값으로 All을 표시하는 것을 선호한다면 다음과 같이 변경할 수 있어요.

packages/app/src/App.tsx

<Route  path="/catalog"  element={<CatalogIndexPage initiallySelectedFilter="all" />}/>

가능한 옵션은 owned, starred, 또는 all이에요.

초기에 선택된 Kind

기본적으로 카탈로그를 볼 때 초기에 선택된 Kind는 Component예요. 하지만 이를 다르게 하고 싶은 이유가 있을 수 있어요. 예를 들어 조직에서 항상 Domain으로 기본 설정되길 원한다고 가정해 봐요. 다음과 같이 하면 돼요.

packages/app/src/App.tsx

<Route path="/catalog" element={<CatalogIndexPage initialKind="domain" />} />

가능한 옵션은 모든 기본 Kind와 추가한 사용자 지정 Kind예요.

Owner Picker 모드

Owner 필터는 기본적으로 카탈로그에서 실제로 엔티티를 소유한 User 및/또는 Group 목록만 포함해요. 이를 변경할 이유가 있을 수 있어요. 다음과 같이 해요.

packages/app/src/App.tsx

<Route path="/catalog" element={<CatalogIndexPage ownerPickerMode="all" />} />

가능한 옵션은 owners-only 또는 all이에요.

테이블 옵션 (Table Options)

Backstage 안에서 사용되는 테이블은 @material-table/core 위에 구축되며, CatalogIndexPage에는 기본 테이블을 어느 정도 사용자 지정할 수 있게 해주는 tableOptions prop이 있어요. 하지만 변경할 수 없는 하드 코딩된 Backstage 설정도 있어요. 다음은 이 prop을 사용해 테이블 머리글의 검색 필터 필드를 비활성화하는 예시예요.

packages/app/src/App.tsx

<Route  path="/catalog"  element={<CatalogIndexPage tableOptions={{ search: false }} />}/>

tableOptions로 설정할 수 있는 옵션은 많으며, 전체 설정 목록은 @material-table/core의 Options 인터페이스에서 찾을 수 있어요(이 링크는 Backstage가 현재 사용하는 버전인 @material-table/core v3.1.0으로 이동해요).

열 사용자 지정 (Customize Columns)

CatalogIndexPage에서 볼 수 있는 열은 대부분에게 좋은 시작점이 되도록 선택됐지만, 기존 또는 사용자 지정 Kind에서 열을 추가하거나 제거하고 싶은 경우가 있을 수 있어요.

기존 Kind에 열 추가하기

카탈로그의 User kind에 새 User Email 열을 추가하고 싶다고 가정해 봐요. App.tsx에서 CatalogIndexPage 컴포넌트에 전달하는 columns를 재정의해 이를 할 수 있어요. 먼저 재정의할 엔티티 kind를 일치시킨 다음 표시할 열을 정의해야 해요.

packages/app/src/App.tsx

{/* prettier-ignore */ /* highlight-add-start */}const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {  if (entityListContext.filters.kind?.value === 'user') {    return [      // Render existing columns      ...CatalogTable.defaultColumnsFunc(entityListContext),      // Add new columns here    ];  }  return CatalogTable.defaultColumnsFunc(entityListContext);};{/* prettier-ignore */ /* highlight-add-end */}

그런 다음 createUserEmailColumn 함수를 구현하고 열 목록에 추가할 수 있어요. field는 엔티티에서 데이터에 접근하는 데 사용되고, render는 데이터 표시 방식을 사용자 지정하게 해줘요.

packages/app/src/App.tsx

const createUserEmailColumn = (): TableColumn<CatalogTableRow> => ({  title: 'User Email',  field: 'entity.spec.profile.email',  render: ({ entity }) => (    <OverflowTooltip      text={entity.spec?.profile?.['email'] || 'N/A'}      placement="bottom-start"    />  ),});const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {  if (entityListContext.filters.kind?.value === 'user') {    return [      // Render existing columns      ...CatalogTable.defaultColumnsFunc(entityListContext),      // Add new columns here      createUserEmailColumn(),    ];  }  return CatalogTable.defaultColumnsFunc(entityListContext);};

마지막으로 myColumnsFunc를 CatalogIndexPage 컴포넌트에 전달할 수 있어요.

packages/app/src/App.tsx

const routes = (  <FlatRoutes>    <Route      path="/catalog"      element={        <CatalogIndexPage          pagination={{ mode: 'offset', limit: 20 }}          columns={myColumnsFunc}        />      }    />    {/* Other routes */}  </FlatRoutes>)

사용자 지정 또는 특정 Kind에 열 추가하기

사용자 지정을 위한 또 다른 사용 사례는 사용자 지정 Kind를 추가할 때예요. 이 기능은 Backstage >= v1.23.0에서 사용할 수 있어요. 예를 들어:

packages/app/src/App.tsx

import {  CatalogEntityPage,  CatalogIndexPage,  catalogPlugin,  CatalogTable,  CatalogTableColumnsFunc,} from '@backstage/plugin-catalog';const myColumnsFunc: CatalogTableColumnsFunc = entityListContext => {  if (entityListContext.filters.kind?.value === 'MyKind') {    return [      CatalogTable.columns.createNameColumn(),      CatalogTable.columns.createOwnerColumn(),    ];  }  return CatalogTable.defaultColumnsFunc(entityListContext);};<Route path="/catalog" element={<CatalogIndexPage />} /><Route path="/catalog" element={<CatalogIndexPage columns={myColumnsFunc} />} />

:::note 위 예시들에서 파일 내용은 단순함을 위해 축약됐어요. :::

동작 사용자 지정 (Customize Actions)

CatalogIndexPage에는 view, edit, star라는 세 가지 기본 동작이 제공돼요. 더 추가하고 싶을 수 있어요.

이를 위해 먼저 packages/app/package.json에 @mui/utils를 추가해야 해요.

yarn --cwd packages/app add @mui/utils

그런 다음 다음을 수행해요.

packages/app/src/App.tsx

import {  AlertDisplay,  OAuthRequestDialog,  SignInPage,  TableProps,} from '@backstage/core-components';import {  CatalogEntityPage,  CatalogIndexPage,  CatalogTableRow,  catalogPlugin,} from '@backstage/plugin-catalog';import { Typography } from '@material-ui/core';import OpenInNew from '@material-ui/icons/OpenInNew';import { visuallyHidden } from '@mui/utils';const customActions: TableProps<CatalogTableRow>['actions'] = [  ({ entity }) => {    const url = 'https://backstage.io/';    const title = `View - ${entity.metadata.name}`;    return {      icon: () => (        <>          <Typography style={visuallyHidden}>{title}</Typography>          <OpenInNew fontSize="small" />        </>      ),      tooltip: title,      disabled: !url,      onClick: () => {        if (!url) return;        window.open(url, '_blank');      },    };  },];<Route path="/catalog" element={<CatalogIndexPage />} /><Route path="/catalog" element={<CatalogIndexPage actions={customActions} />} />

:::note 위 예시에서 App.tsx의 내용은 단순함을 위해 축약됐어요. :::

위 사용자 지정은 기존 동작을 재정의해요. 현재로서는 기존 동작을 유지하면서 자신의 것을 추가하는 유일한 방법은 defaultActions에서 복사해 배열에 기존 동작도 포함하는 것이에요.

필터 사용자 지정 (Customize Filters)

필터를 사용자 지정하는 방법은 여러 가지가 있어요. props로 기존 필터를 조정하거나, 기본 필터를 추가하거나 제거하거나, 완전히 새로운 사용자 지정 필터를 만들거나. 다음 섹션들이 이 경우들을 다뤄요.

기본 필터 props (Default Filter Props)

이 문서 앞부분에서 언급된 모든 props를 표면화하는 기본 필터 집합이 있어요. 다음과 같이 사용할 수 있어요.

packages/app/src/App.tsx

import { DefaultFilters } from '@backstage/plugin-catalog-react';<Route  path="/catalog"  element={    <CatalogIndexPage      filters={        <>          <DefaultFilters            initialKind="Domain"            initiallySelectedFilter="all"            ownerPickerMode="all"          />        </>      }    />  }/>;

기본 필터 제거하기

Lifecycle, Tag, Processing Status 필터를 사용하지 않을 이유가 있다면 제거하는 예시는 다음과 같아요.

packages/app/src/App.tsx

import {  EntityKindPicker,  EntityTypePicker,  UserListPicker,  EntityOwnerPicker,  EntityNamespacePicker,} from '@backstage/plugin-catalog-react';<Route  path="/catalog"  element={    <CatalogIndexPage      filters={        <>          <EntityKindPicker />          <EntityTypePicker />          <UserListPicker />          <EntityOwnerPicker />          <EntityNamespacePicker />        </>      }    />  }/>;

사용자 지정 필터 (Custom Filters)

사용자 지정 필터를 추가할 수 있어요. 예를 들어 엔티티에 추가된 사용자 지정 어노테이션 company.com/security-tier로 필터링할 수 있게 하려고 한다고 가정해 봐요. 그 요구를 지원하는 필터를 만드는 방법은 다음과 같아요.

먼저 EntityFilter 인터페이스를 구현하는 새 필터를 만들어야 해요.

import { EntityFilter } from '@backstage/plugin-catalog-react';import { Entity } from '@backstage/catalog-model';class EntitySecurityTierFilter implements EntityFilter {  constructor(readonly values: string[]) {}  filterEntity(entity: Entity): boolean {    const tier = entity.metadata.annotations?.['company.com/security-tier'];    return tier !== undefined && this.values.includes(tier);  }}

EntityFilter 인터페이스는 catalog-backend로 전달되는 백엔드 필터 또는 엔티티가 백엔드에서 로드된 후 적용되는 프론트엔드 필터를 허용해요.

이 필터를 사용해 type-safe 방식으로 기본 필터를 확장할 거예요. 이 필터 옆 어딘가에 기본을 확장하는 사용자 지정 필터 형태를 만들어 봐요.

export type CustomFilters = DefaultEntityFilters & {  securityTiers?: EntitySecurityTierFilter;};

이 필터를 제어하기 위해 보안 등급에 대한 체크박스를 보여주는 React 컴포넌트를 만들 수 있어요. 이 컴포넌트는 이 확장된 필터 유형을 제네릭 매개변수로 받아들이는 useEntityList 훅을 사용해요.

export const EntitySecurityTierPicker = () => {  // The securityTiers key is recognized due to the CustomFilter generic  const {    filters: { securityTiers },    updateFilters,  } = useEntityList<CustomFilters>();  // Toggles the value, depending on whether it's already selected  function onChange(value: string) {    const newTiers = securityTiers?.values.includes(value)      ? securityTiers.values.filter(tier => tier !== value)      : [...(securityTiers?.values ?? []), value];    updateFilters({      securityTiers: newTiers.length        ? new EntitySecurityTierFilter(newTiers)        : undefined,    });  }  const tierOptions = ['1', '2', '3'];  return (    <FormControl component="fieldset">      <Typography variant="button">Security Tier</Typography>      <FormGroup>        {tierOptions.map(tier => (          <FormControlLabel            key={tier}            control={              <Checkbox                checked={securityTiers?.values.includes(tier)}                onChange={() => onChange(tier)}              />            }            label={`Tier ${tier}`}          />        ))}      </FormGroup>    </FormControl>  );};

이제 컴포넌트를 CatalogIndexPage에 추가할 수 있어요.

packages/app/src/App.tsx

{/* prettier-ignore */ /* highlight-add-start */}import { DefaultFilters } from '@backstage/plugin-catalog-react';{/* prettier-ignore */ /* highlight-add-end */}const routes = (  <FlatRoutes>    <Navigate key="/" to="catalog" />    <Route path="/catalog" element={<CatalogIndexPage />} />    <Route      path="/catalog"      element={        <CatalogIndexPage          filters={            <>              <DefaultFilters />              <EntitySecurityTierPicker />            </>          }        />      }    />    {/* ... */}  </FlatRoutes>);

같은 방법으로 다른 인터페이스로 기본 필터를 사용자 지정할 수 있어요. 그런 용도에서는 필터 형태가 기본과 동일하게 유지되므로 제네릭 인자가 필요 없어요.

고급 사용자 지정 (Advanced Customization)

위의 어느 것도 요구사항에 맞지 않는 사람들을 위해 완전히 사용자 지정된 CatalogIndexPage를 만드는 옵션이 있어요.

packages/app/src/components/catalog/CustomCatalogIndex.tsx

import {  PageWithHeader,  Content,  ContentHeader,  SupportButton,} from '@backstage/core-components';import { useApi, configApiRef } from '@backstage/core-plugin-api';import { CatalogTable } from '@backstage/plugin-catalog';import {  EntityListProvider,  CatalogFilterLayout,  EntityKindPicker,  EntityLifecyclePicker,  EntityNamespacePicker,  EntityOwnerPicker,  EntityProcessingStatusPicker,  EntityTagPicker,  EntityTypePicker,  UserListPicker,} from '@backstage/plugin-catalog-react';export const CustomCatalogPage = () => {  const orgName =    useApi(configApiRef).getOptionalString('organization.name') ?? 'Backstage';  return (    <PageWithHeader title={orgName} themeId="home">      <Content>        <ContentHeader title="">          <SupportButton>All your software catalog entities</SupportButton>        </ContentHeader>        <EntityListProvider pagination>          <CatalogFilterLayout>            <CatalogFilterLayout.Filters>              <EntityKindPicker />              <EntityTypePicker />              <UserListPicker />              <EntityOwnerPicker />              <EntityLifecyclePicker />              <EntityTagPicker />              <EntityProcessingStatusPicker />              <EntityNamespacePicker />            </CatalogFilterLayout.Filters>            <CatalogFilterLayout.Content>              <CatalogTable />            </CatalogFilterLayout.Content>          </CatalogFilterLayout>        </EntityListProvider>      </Content>    </PageWithHeader>  );};

위는 완전히 사용자 지정된 CatalogIndexPage의 아주 기본적인 버전이에요. 다양한 props를 탐색해 무엇을 할 수 있는지 살펴보고 싶을 거예요. 이것은 DefaultCatalogPage에서 볼 수 있는 구성 요소로 만들어졌어요.

:::note 카탈로그 index 페이지는 사용자 지정을 쉽게 하도록 코드 발자국을 최소화하도록 설계됐지만, 복제본을 만들면 시간이 지나며 최신 상태에서 벗어날(drifting) 가능성이 생겨요. 주기적으로 카탈로그 CHANGELOG를 확인하세요. :::

CustomCatalogPage라고 부르는 이 사용자 지정 CatalogIndexPage를 사용하려면 다음 변경을 해야 해요.

packages/app/src/App.tsx

const routes = (  <FlatRoutes>    <Navigate key="/" to="catalog" />    <Route path="/catalog" element={<CatalogIndexPage />} />    <Route path="/catalog" element={<CatalogIndexPage />}>      <CustomCatalogPage />    </Route>    {/* ... */}  </FlatRoutes>);

더 알아보기 (Learn more)