Programming/MSA

Backstage) Relation - Catalog Graph 의 가시성 향상시키기

armyost 2026. 1. 30. 06:59
728x90

이것이 기존의 Catalog Graph 인데, 모든 Kind가 같은 색상으로 아이콘으로 구분되다 보니 가시성이 떨어진다. 

 

Custom UI 를 사용하여 가시성을 향상시킬수 있다. 설명은 공식적으로 여기에 소개되어 있다.

https://github.com/backstage/backstage/blob/master/plugins/catalog-graph/README.md#customizing-the-ui

 

backstage/plugins/catalog-graph/README.md at master · backstage/backstage

Backstage is an open framework for building developer portals - backstage/backstage

github.com

 

수정사항은 다음과 같다.

 

우선 CustomUI Rendering Component를 정의한다.

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

import React, { useLayoutEffect, useRef, useState } from 'react';
import { DependencyGraphTypes } from '@backstage/core-components';
import { makeStyles } from '@material-ui/core/styles';
import classNames from 'classnames';

const useStyles = makeStyles(_theme => ({
  node: {
    stroke: '#000',
    strokeWidth: 2,
    '&.focused': {
      strokeWidth: 3,
    },
    '&.clickable': {
      cursor: 'pointer',
    },
    // Entity Kind specific styles
    '&.system': {
      fill: '#F5DC70',
      stroke: '#F2CE34',
    },
    '&.domain': {
      fill: '#F5DC70',
      stroke: '#F2CE34',
    },
    '&.component': {
      fill: '#85E1FF',
      stroke: '#2196F3',
    },
    '&.service': {
      fill: '#85E1FF',
      stroke: '#2196F3',
    },
    '&.api': {
      fill: '#98FB98',
      stroke: '#4CAF50',
    },
    '&.group': {
      fill: '#FFB366',
      stroke: '#FF9800',
    },
    '&.user': {
      fill: '#E1BEE7',
      stroke: '#9C27B0',
    },
    '&.resource': {
      fill: '#FFCCBC',
      stroke: '#FF6E40',
    },
    '&.template': {
      fill: '#B2DFDB',
      stroke: '#009688',
    },
    '&.unknown': {
      fill: '#BDBDBD',
      stroke: '#757575',
    },
  },
  text: {
    fontSize: 12,
    fontFamily: 'Arial, sans-serif',
    fill: '#000',
    pointerEvents: 'none',
    '&.focused': {
      fontWeight: 'bold',
    },
  },
  clickable: {
    cursor: 'pointer',
  },
}));

export const CustomRenderNode = (
  props: DependencyGraphTypes.RenderNodeProps<any>,
) => {
  const classes = useStyles();
  const { node } = props;
  const { id } = node;
  
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
  const idRef = useRef<SVGTextElement | null>(null);

  useLayoutEffect(() => {
    // set the width to the length of the ID
    if (idRef.current) {
      let { height: renderedHeight, width: renderedWidth } =
        idRef.current.getBBox();
      renderedHeight = Math.round(renderedHeight);
      renderedWidth = Math.round(renderedWidth);

      if (renderedHeight !== height || renderedWidth !== width) {
        setWidth(renderedWidth);
        setHeight(renderedHeight);
      }
    }
  }, [width, height]);

  const padding = 10;
  const paddedWidth = width + padding * 2;
  const paddedHeight = height + padding * 2;
  
  // Extract kind from node ID (format: "kind:namespace/name")
  const kind = id.split(':')[0]?.toLowerCase();

  return (
    <g 
      onClick={node.onClick} 
      className={classNames(node.onClick && classes.clickable)}
    >
      <rect
        className={classNames(
          classes.node,
          kind,
          node.focused && 'focused',
        )}
        width={paddedWidth}
        height={paddedHeight}
        rx={4}
        ry={4}
      />
      <text
        ref={idRef}
        className={classNames(classes.text, node.focused && 'focused')}
        y={paddedHeight / 2}
        x={paddedWidth / 2}
        textAnchor="middle"
        alignmentBaseline="middle"
      >
        {id}
      </text>
    </g>
  );
};

 

그리고 정의한 Component를 필요한 곳에. import한다. 그리고 이렇게 사용한다.

    renderNode={CustomRenderNode}

 

우선 "/catalog-graph" route를 정의한 곳에 적용해보자

 

packages/app/src/App.tsx

import { CustomRenderNode } from './components/catalog/CustomRenderNode';

...

    <Route path="/catalog-graph" element={<CatalogGraphPage renderNode={CustomRenderNode} />} />
    
    ...

 

 

그리고 EntityPage에도 적용한다.

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

import { CustomRenderNode } from './CustomRenderNode';
...
const overviewContent = (
  <Grid container spacing={3} alignItems="stretch">
    ...
    <Grid item md={6} xs={12}>
      <EntityCatalogGraphCard variant="gridItem" renderNode={CustomRenderNode} height={400} />
    </Grid>
    ...
  </Grid>
);
...
const systemPage = (
  <EntityLayout>
    <EntityLayout.Route path="/diagram" title="Diagram">
      <EntityCatalogGraphCard
        variant="gridItem"
        renderNode={CustomRenderNode}
        direction={Direction.TOP_BOTTOM}
        title="System Diagram"
        height={700}
        relations={[
          RELATION_PART_OF,
          RELATION_HAS_PART,
          RELATION_API_CONSUMED_BY,
          RELATION_API_PROVIDED_BY,
          RELATION_CONSUMES_API,
          RELATION_PROVIDES_API,
          RELATION_DEPENDENCY_OF,
          RELATION_DEPENDS_ON,
        ]}
        unidirectional={false}
      />
    </EntityLayout.Route>
  </EntityLayout>
);
...

 

 

Full SourceCode는 여기서 확인할 수 있다. 

https://github.com/armyost/backstage-armyost

 

GitHub - armyost/backstage-armyost

Contribute to armyost/backstage-armyost development by creating an account on GitHub.

github.com

 

이렇게 CustomUI가 씌워졌다.