카탈로그 사용자 지정
카탈로그 사용자 지정 (Catalog Customization)
새 프론트엔드 시스템에서 Backstage 소프트웨어 카탈로그 직원(user) index 페이지와 엔티티 페이지를 사용자 지정하는 방법을 설명하는 가이드예요.
출처: 문서
본문
:::info 이 문서는 새 Backstage 앱에서 기본값인 새 프론트엔드 시스템을 위해 작성됐어요. Backstage 앱이 여전히 이전 프론트엔드 시스템을 사용한다면 이 가이드의 이전 프론트엔드 시스템 버전을 읽으세요. :::
Backstage 소프트웨어 카탈로그에는 기본 카탈로그 index 페이지와 엔티티 페이지가 있으며, 이들은 app-config.yaml을 통해 매우 높은 수준으로 구성할 수 있어요. 이 가이드는 새 프론트엔드 시스템에서 카탈로그를 사용자 지정하는 방법을 다뤄요.
카탈로그 index 페이지
카탈로그 index 페이지는 app-config.yaml의 확장을 통해 구성할 수 있어요. 예를 들어 페이지네이션을 활성화하려면:
app-config.yaml
app: extensions: - page:catalog: config: pagination: true
추가 옵션으로 페이지네이션을 구성할 수도 있어요.
app-config.yaml
app: extensions: - page:catalog: config: pagination: mode: offset limit: 20
카탈로그 내보내기 구성하기
카탈로그 내보내기(export) 기능은 새 프론트엔드 시스템에서 사용할 수 있으며 app-config.yaml을 통해 활성화할 수 있어요. 이렇게 하면 카탈로그 테이블에서 데이터를 CSV와 JSON 형식으로 내보낼 수 있는 옵션이 기본으로 포함된 버튼이 활성화돼요. 내보낼 때 사용자가 내보내기 형식을 선택하고 어떤 열을 포함할지 선택할 수 있는 대화상자가 열려요.
기본 구성
카탈로그 내보내기를 활성화하려면 다음 구성을 추가해요.
app-config.yaml
app: extensions: - page:catalog: config: exportSettings: enabled: true # Optional: hide the built-in CSV and JSON formats, showing only your own supplied exporters disableBuiltinExporters: false
이렇게 하면 카탈로그 index 페이지에 "Export selection" 버튼이 표시되고, 사용자가 현재 필터링된 카탈로그 엔티티를 CSV 또는 JSON 형식으로 내보낼 수 있게 돼요.
고급 구성
사용자 지정 내보내기 형식 같은 고급 내보내기 사용자 지정의 경우 카탈로그 내보내기 확장을 제공하는 프론트엔드 모듈을 만들어요.
src/catalogExportExtension.tsx
import { createFrontendModule } from '@backstage/frontend-plugin-api';import { CatalogExportConfigBlueprint } from '@backstage/plugin-catalog/alpha';import type { CatalogExporter } from '@backstage/plugin-catalog';import { catalogApiRef } from '@backstage/plugin-catalog-react';// Define custom export formats using streaming async generatorsconst yamlExporter: CatalogExporter = ({ apis, columns, streamRequest }) => { const catalogApi = apis.get(catalogApiRef); // Return an async generator that yields YAML chunks async function* generateYaml() { for await (const page of catalogApi.streamEntities(streamRequest)) { for (const entity of page) { // Serialize each entity to YAML and yield immediately yield serializeEntityToYaml(entity, columns); yield '---\n'; // YAML document separator } } } return { generator: generateYaml(), contentType: 'application/x-yaml', };};// Create the extension using the blueprintconst catalogExportExtension = CatalogExportConfigBlueprint.make({ params: { exporters: { yaml: { exporter: yamlExporter, label: 'YAML' }, }, columns: [{ entityFilterKey: 'metadata.name', title: 'Name' }], onSuccess: () => { console.log('Export successful!'); }, onError: ({ error }) => { console.error('Export failed:', error); }, },});// Create the module that provides this extensionexport default createFrontendModule({ pluginId: 'catalog', extensions: [catalogExportExtension],});
그런 다음 이 모듈을 앱 기능(features)에 등록해요.
packages/app-next/src/App.tsx
import catalogExportExtension from './catalogExportExtension';const app = createApp({ features: [ // ... other features catalogExportExtension, ],});
CatalogExportConfigBlueprint는 다음 속성을 지원해요.
-
exporters- 사용자 지정 내보내기 형식 구성의 기록(Record) (예: XML, YAML). 각각은exporter함수와 선택적label을 가진CatalogExporterConfig형태를 사용해요. -
columns- 내보내기에 포함할 사용자 지정 열. 각 열은 엔티티 필드 경로(entityFilterKey)와 선택적 표시title을 지정해요. 제공하지 않으면 Name, Type, Owner, Description이 기본값이에요. -
onSuccess- 내보내기 성공 시 호출되는 콜백 함수. -
onError- 내보내기 실패 시 호출되는 콜백 함수로,{ error: Error }를 받아요.
카탈로그 필터
카탈로그 index 페이지에는 기본 필터 집합(kind, type, owner, lifecycle, tag, namespace, processing status)이 포함돼요. 이 필터들은 확장을 통해 구성할 수 있어요. 예를 들어 초기 kind 필터를 설정하려면:
app-config.yaml
app: extensions: - catalog-filter:catalog/kind: config: initialFilter: domain
초기 목록 필터를 "owned" 대신 "all"로 설정하려면:
app-config.yaml
app: extensions: - catalog-filter:catalog/list: config: initialFilter: all
사용자 지정 필터
@backstage/plugin-catalog-react/alpha의 CatalogFilterBlueprint를 사용해 사용자 지정 카탈로그 필터를 만들 수 있어요. 예를 들어 사용자 지정 보안 등급(security tier) 필터를 추가하려면:
packages/app/src/catalog/SecurityTierFilter.tsx
import { CatalogFilterBlueprint } from '@backstage/plugin-catalog-react/alpha';export const securityTierFilter = CatalogFilterBlueprint.make({ name: 'security-tier', params: { loader: async () => { const { EntitySecurityTierPicker } = await import( './EntitySecurityTierPicker' ); return <EntitySecurityTierPicker />; }, },});
그런 다음 프론트엔드 모듈로 설치해요.
packages/app/src/catalog/catalogCustomizations.tsx
import { createFrontendModule } from '@backstage/frontend-plugin-api';import { securityTierFilter } from './SecurityTierFilter';export default createFrontendModule({ pluginId: 'catalog', extensions: [securityTierFilter],});
그런 다음 앱에 모듈을 등록해요.
packages/app/src/App.tsx
import { createApp } from '@backstage/frontend-defaults';import catalogCustomizations from './catalog/catalogCustomizations';const app = createApp({ features: [catalogCustomizations],});export default app.createRoot();
기본 필터 제거하기
기본 필터는 app-config.yaml에서 false로 설정해 비활성화할 수 있어요.
app-config.yaml
app: extensions: - catalog-filter:catalog/lifecycle: false - catalog-filter:catalog/tag: false - catalog-filter:catalog/processing-status: false
열, 동작, 테이블 옵션 사용자 지정하기
이전 프론트엔드 시스템에서 카탈로그 테이블 열, 행 동작, 테이블 옵션을 사용자 지정하는 것은 CatalogIndexPage 컴포넌트에 props를 직접 전달해 이루어졌어요. 새 프론트엔드 시스템에서는 page:catalog 확장을 재정의해 이러한 사용자 지정을 해요.
예를 들어 사용자 지정 열이나 동작으로 카탈로그 index 페이지를 사용자 지정하려면 프론트엔드 모듈을 사용해 페이지 확장을 재정의할 수 있어요.
packages/app/src/catalog/customCatalogPage.tsx
import { PageBlueprint, createFrontendModule, createRouteRef,} from '@backstage/frontend-plugin-api';const customCatalogPage = PageBlueprint.make({ params: { path: '/catalog', routeRef: createRouteRef({ aliasFor: 'catalog.catalogIndex' }), loader: async () => { const { CustomCatalogPage } = await import('./CustomCatalogPage'); return <CustomCatalogPage />; }, },});export default createFrontendModule({ pluginId: 'catalog', extensions: [customCatalogPage],});
사용자 지정 카탈로그 페이지 컴포넌트 안에서 테이블 열, 동작, 옵션을 완전히 제어할 수 있어요. @backstage/plugin-catalog와 @backstage/plugin-catalog-react의 컴포넌트를 사용해 페이지를 구성할 수 있어요.
packages/app/src/catalog/CustomCatalogPage.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 ( <EntityListProvider pagination> <PageWithHeader title={orgName} themeId="home"> <Content> <ContentHeader title=""> <SupportButton>All your software catalog entities</SupportButton> </ContentHeader> <CatalogFilterLayout> <CatalogFilterLayout.Filters> <EntityKindPicker /> <EntityTypePicker /> <UserListPicker /> <EntityOwnerPicker /> <EntityLifecyclePicker /> <EntityTagPicker /> <EntityProcessingStatusPicker /> <EntityNamespacePicker /> </CatalogFilterLayout.Filters> <CatalogFilterLayout.Content> <CatalogTable /> </CatalogFilterLayout.Content> </CatalogFilterLayout> </Content> </PageWithHeader> </EntityListProvider> );};
:::note 카탈로그 index 페이지는 사용자 지정을 쉽게 하도록 코드 발자국을 최소화하도록 설계됐지만, 복제본을 만들면 시간이 지나며 최신 상태에서 벗어날(drifting) 가능성이 생겨요. 주기적으로 카탈로그 CHANGELOG를 확인하세요. :::
확장 재정의와 사용 가능한 다양한 재정의 패턴에 대한 자세한 내용은 확장 재정의 문서를 참고하세요.
엔티티 페이지
엔티티 필터
카탈로그 엔티티 페이지에 부착되는 많은 확장은 filter 구성을 받아들여요. filter 구성의 목적은 확장이 적용되거나 존재해야 할 엔티티를 선택하는 것이에요. 많은 확장에는 기본 필터가 정의되어 있지만, 나만의 필터를 제공해 재정의할 수 있어요. 코드에서 필터를 정의할 때는 predicate 함수나 엔티티 predicate 쿼리를 사용할 수 있으며, 구성에서는 엔티티 predicate 쿼리만 사용할 수 있어요.
엔티티 predicate 쿼리
엔티티 predicate 구문은 카탈로그 엔티티를 필터링하기 위한 최소한의 JSON 기반 쿼리 언어예요. MongoDB 쿼리 구문에서 느슨하게 영감을 받았으며, 거의 비슷하게 동작하지만 연산자 집합은 다르다는 특징이 있어요.
가장 단순한 엔티티 predicate는 키-값 매핑이 있는 객체 표현식으로, 키는 엔티티에서 값의 전체 점으로 구분된 경로이고 값은 대소문자 구분 없는 일치를 수행할 값이에요. 이 객체의 각 항목은 개별적으로 평가되지만, 전체 predicate가 일치하려면 모두 일치해야 해요. 예를 들어 다음은 type이 service인 모든 component 엔티티와 일치해요.
{ "filter": { "kind": "component", "spec.type": "service" }}
YAML 구문을 사용하면:
filter: kind: component spec.type: service
이 기본 구문 외에도, 엔티티 predicate는 이러한 객체 표현식 주변에 중첩·적용할 수 있는 논리 연산자를 지원해요. 예를 들어 다음은 type이 service 또는 website인 모든 component 엔티티와 일치해요.
{ "filter": { "$all": [ { "kind": "component" }, { "$any": [{ "spec.type": "service" }, { "spec.type": "website" }] } ] }}
YAML 구문을 사용하면:
filter: $all: - kind: component - $any: - spec.type: service - spec.type: website
마지막으로, 엔티티 predicate는 객체 표현식의 값 대신 사용할 수 있는 값 연산자도 지원해요. 예를 들어 다음은 이전 예시를 더 간단히 표현한 것이에요.
{ "filter": { "kind": "component", "spec.type": { "$in": ["service", "website"] } }}
YAML 구문을 사용하면:
filter: kind: component spec.type: $in: [service, website]
엔티티 predicate 논리 연산자
다음 섹션은 엔티티 predicate의 모든 논리 연산자를 나열해요.
$all
$all 연산자는 다음 구문을 가져요.
{ $all: [ { <expression1> }, { <expression2> }, ...] }
$all 연산자는 제공된 배열 안의 모든 표현식이 true로 평가되면 true로 평가돼요. 빈 배열도 포함하며, 즉 { "$all": [] }은 항상 true로 평가돼요.
$all 사용 예시
filter: $all: - kind: component - $not: spec.type: service
$any
$any 연산자는 다음 구문을 가져요.
{ $any: [ { <expression1> }, { <expression2> }, ...] }
$any 연산자는 제공된 배열 안의 표현식 중 적어도 하나가 true로 평가되면 true로 평가돼요. 빈 배열도 포함하며, 즉 { "$any": [] }은 항상 false로 평가돼요.
$any 사용 예시
filter: $any: - kind: component - metadata.annotations.github.com/project-slug: { $exists: true }
$not
$not 연산자는 다음 구문을 가져요.
{ $not: { <expression> } }
$not 연산자는 제공된 표현식의 결과를 뒤집어요. 표현식이 true로 평가되면 $not은 false로 평가되고, 그 반대도 마찬가지예요.
$not 사용 예시
filter: $not: kind: template
엔티티 predicate 값 연산자
다음 섹션은 엔티티 predicate의 모든 값 연산자를 나열해요.
$exists
$exists 연산자는 다음 구문을 가져요.
{ field: { $exists: <boolean> } }
$exists 연산자는 일치하는 값의 존재가 제공된 boolean과 일치하면 true로 평가돼요. 즉 { $exists: true }는 값이 정의된 경우에만 true로 평가되고, { $exists: false }는 값이 정의되지 않은 경우에만 true로 평가돼요.
$exists 사용 예시
filter: metadata.annotations.github.com/project-slug: { $exists: true }
$in
$in 연산자는 다음 구문을 가져요.
{ field: { $in: [ <primitive1>, <primitive2>, ... ] } }
$in 연산자는 일치하는 값이 primitive 배열 안에 존재하면 true로 평가돼요. 비교는 대소문자를 구분하지 않으며 primitive 값 사이에서만 수행할 수 있어요. 일치하는 값이 객체나 배열이면 연산자는 항상 false로 평가돼요.
$in 사용 예시
filter: kind: $in: [component, api]
$contains
$contains 연산자는 다음 구문을 가져요.
{ field: { $contains: { <expression> } } }
$contains 연산자는 일치하는 값이 배열이고, 배열의 요소 중 적어도 하나가 제공된 표현식과 완전히 일치하면 true로 평가돼요. 일치하는 값이 배열이 아니거나 배열이 비어 있으면 연산자는 항상 false로 평가돼요.
배열과 일치시키는 데 사용되는 표현식은 논리 연산자와 값 연산자를 포함한 유효한 엔티티 predicate 표현식이면 뭐든 될 수 있어요.
$contains 사용 예시
filter: relations: $contains: type: ownedBy targetRef: $in: [group:default/admins, group:default/viewers]
그룹, 제목, 아이콘 구성하기
엔티티 페이지에 나타나는 탭 그룹을 정의하고 사용자 지정할 수 있으며, 그룹과 개별 탭 모두에 아이콘을 활성화할 수 있어요.
app: extensions: # Entity page (new frontend system) - page:catalog/entity: config: # Show icons next to group and tab titles showNavItemIcons: true # Optionally override default groups and their icons groups: - overview: title: Overview icon: dashboard - quality: title: Quality icon: verified - documentation: title: Docs icon: description
참고 사항:
-
그룹과 탭의 아이콘은 앱의 IconsApi를 통해 해석돼요. 문자열 아이콘 id(예:
"dashboard")를 사용할 때는 해당 아이콘 번들이 앱에서 활성화/설치되어 있는지 확인하세요(IconBundleBlueprint 문서 참고). -
그룹 아이콘은
showNavItemIcons가true로 설정된 경우에만 렌더링돼요.
그룹 내 콘텐츠 순서
기본적으로 각 그룹 안의 콘텐츠 항목은 제목 기준 알파벳순으로 정렬돼요. defaultContentOrder 옵션으로 이를 변경할 수 있으며, 두 가지 모드를 지원해요.
-
title(기본값) — 콘텐츠 확장의 제목 기준 알파벳순 정렬(대소문자 구분 없음). -
natural— 자연스러운 확장 발견/등록 순서 유지.
페이지 수준의 defaultContentOrder는 모든 그룹의 기본값을 설정하고, 개별 그룹은 그룹별 contentOrder로 이를 재정의할 수 있어요.
app: extensions: - page:catalog/entity: config: # Default content order for all groups defaultContentOrder: title groups: - documentation: title: Docs # Override: keep natural order for this group contentOrder: natural
콘텐츠 순서는 그룹 안의 콘텐츠 항목에만 적용된다는 점에 유의하세요. 그룹화되지 않은 탭(어떤 그룹 정의와도 일치하지 않는 탭)은 항상 자연스러운 순서를 유지해요.
그룹 별칭 (Group aliases)
그룹은 aliases를 선언할 수 있어요. 이는 동등하게 취급되어야 하는 다른 그룹 ID들의 목록이에요. 별칭 그룹 ID를 대상으로 하는 모든 엔티티 콘텐츠 확장은 별칭을 가진 그룹에 포함돼요. 이것은 개별 확장을 다시 구성하지 않고 그룹을 이름 바꾸거나 병합할 때 유용해요.
app: extensions: - page:catalog/entity: config: groups: - develop: title: Develop # Content targeting 'development' will appear in this group aliases: - development
탭의 그룹 재정의 또는 비활성화 (확장별)
각 엔티티 콘텐츠 확장(엔티티 페이지의 탭)은 코드에서 기본 group을 선언할 수 있어요. app-config.yaml에서 확장의 구성으로 설치별로 이를 재정의하거나 비활성화할 수 있어요.
app: extensions: # ... # Example entity content extension instance id - entity-content:example/my-content: config: # Move this tab to a custom group you defined above group: custom # Show an icon for this entity content page but only if `showNavItemIcons` is enabled for the `page:catalog/entity` extension icon: my-icon # Disassociate from any group and show as a standalone tab - entity-content:example/another-content: config: group: false
엔티티 콘텐츠의 탭 아이콘
엔티티 콘텐츠 확장은 icon 매개변수도 선언할 수 있어요. 문자열로 제공되면 아이콘 id는 IconsApi를 통해 조회돼요. 아이콘이 렌더링되려면:
-
엔티티 페이지가
showNavItemIcons: true여야 해요(위 구성 참고). -
아이콘 id가 앱의 활성화된 아이콘 번들에서 사용 가능해야 해요.
엔티티 컨텍스트 메뉴 (Entity Context Menu)
EntityContextMenuItemBlueprint를 사용해 컨텍스트 메뉴 항목을 구성할 수 있어요. 현재 항목은 여기에서 정의돼요.
새 컨텍스트 항목을 추가하려면 다음과 같이 할 수 있어요.
import { EntityContextMenuItemBlueprint } from '@backstage/plugin-catalog-react/alpha';const customEntityMenuItem = EntityContextMenuItemBlueprint.make({ name: 'open-dialog', params: { icon: <ExampleIcon />, useProps() { const dialogApi = useApi(dialogApiRef); const { entity } = useEntity(); return { title: 'Open Custom Dialog', disabled: false, // you can also use href: '/example-path' onClick: async () => { dialogApi.open(({ dialog }) => ( <AsyncEntityProvider entity={entity} loading={false}> <CustomDialog open onClose={() => dialog.close()} /> </AsyncEntityProvider> )); }, }; }, },});