From fe1a2f46a25206ea0edd02f0f24420131dbfd000 Mon Sep 17 00:00:00 2001 From: mario ma Date: Wed, 28 May 2025 16:53:32 +0800 Subject: [PATCH] feat: catalog graph support i18n Signed-off-by: mario ma --- .changeset/clear-bananas-end.md | 5 ++ plugins/catalog-graph/report-alpha.api.md | 29 +++++++++ plugins/catalog-graph/src/alpha.tsx | 2 + .../CatalogGraphCard.test.tsx | 13 +++- .../CatalogGraphCard/CatalogGraphCard.tsx | 7 ++- .../CatalogGraphPage/CatalogGraphPage.tsx | 20 +++---- .../CatalogGraphPage/CurveFilter.test.tsx | 13 ++-- .../CatalogGraphPage/CurveFilter.tsx | 14 +++-- .../CatalogGraphPage/DirectionFilter.test.tsx | 9 +-- .../CatalogGraphPage/DirectionFilter.tsx | 18 +++--- .../CatalogGraphPage/MaxDepthFilter.test.tsx | 17 +++--- .../CatalogGraphPage/MaxDepthFilter.tsx | 13 +++- .../SelectedKindsFilter.test.tsx | 11 +++- .../CatalogGraphPage/SelectedKindsFilter.tsx | 9 ++- .../SelectedRelationsFilter.test.tsx | 13 ++-- .../SelectedRelationsFilter.tsx | 9 ++- plugins/catalog-graph/src/translation.ts | 60 +++++++++++++++++++ 17 files changed, 204 insertions(+), 58 deletions(-) create mode 100644 .changeset/clear-bananas-end.md create mode 100644 plugins/catalog-graph/src/translation.ts diff --git a/.changeset/clear-bananas-end.md b/.changeset/clear-bananas-end.md new file mode 100644 index 0000000000..50e722544c --- /dev/null +++ b/.changeset/clear-bananas-end.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-graph': patch +--- + +Catalog graph plugin support i18n diff --git a/plugins/catalog-graph/report-alpha.api.md b/plugins/catalog-graph/report-alpha.api.md index cf5835e35c..230edde3da 100644 --- a/plugins/catalog-graph/report-alpha.api.md +++ b/plugins/catalog-graph/report-alpha.api.md @@ -16,6 +16,35 @@ import { ExternalRouteRef } from '@backstage/frontend-plugin-api'; import { FrontendPlugin } from '@backstage/frontend-plugin-api'; import { JSX as JSX_2 } from 'react'; import { RouteRef } from '@backstage/frontend-plugin-api'; +import { TranslationRef } from '@backstage/frontend-plugin-api'; + +// @alpha (undocumented) +export const catalogGraphTranslationRef: TranslationRef< + 'catalog-graph', + { + readonly 'catalogGraphCard.title': 'Relations'; + readonly 'catalogGraphCard.deepLinkTitle': 'View graph'; + readonly 'catalogGraphPage.title': 'Catalog Graph'; + readonly 'catalogGraphPage.filterToggleButtonTitle': 'Filters'; + readonly 'catalogGraphPage.supportButtonDescription': 'Start tracking your component in by adding it to the software catalog.'; + readonly 'catalogGraphPage.simplifiedSwitchLabel': 'Simplified'; + readonly 'catalogGraphPage.mergeRelationsSwitchLabel': 'Merge relations'; + readonly 'catalogGraphPage.zoomOutDescription': 'Use pinch & zoom to move around the diagram. Click to change active node, shift click to navigate to entity.'; + readonly 'catalogGraphPage.curveFilter.title': 'Curve'; + readonly 'catalogGraphPage.curveFilter.curveStepBefore': 'Step Before'; + readonly 'catalogGraphPage.curveFilter.curveMonotoneX': 'Monotone X'; + readonly 'catalogGraphPage.directionFilter.title': 'Direction'; + readonly 'catalogGraphPage.directionFilter.leftToRight': 'Left to right'; + readonly 'catalogGraphPage.directionFilter.rightToLeft': 'Right to left'; + readonly 'catalogGraphPage.directionFilter.topToBottom': 'Top to bottom'; + readonly 'catalogGraphPage.directionFilter.bottomToTop': 'Bottom to top'; + readonly 'catalogGraphPage.maxDepthFilter.title': 'Max depth'; + readonly 'catalogGraphPage.maxDepthFilter.inputPlaceholder': '∞ Infinite'; + readonly 'catalogGraphPage.maxDepthFilter.clearButtonAriaLabel': 'clear max depth'; + readonly 'catalogGraphPage.selectedKindsFilter.title': 'Kinds'; + readonly 'catalogGraphPage.selectedRelationsFilter.title': 'Relations'; + } +>; // @public (undocumented) const _default: FrontendPlugin< diff --git a/plugins/catalog-graph/src/alpha.tsx b/plugins/catalog-graph/src/alpha.tsx index aa1f6a05cf..6ee16be1c4 100644 --- a/plugins/catalog-graph/src/alpha.tsx +++ b/plugins/catalog-graph/src/alpha.tsx @@ -96,3 +96,5 @@ export default createFrontendPlugin({ }, extensions: [CatalogGraphPage, CatalogGraphEntityCard], }); + +export { catalogGraphTranslationRef } from './translation'; diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx index e09d57bd31..ed8069a8f4 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.test.tsx @@ -34,6 +34,7 @@ import userEvent from '@testing-library/user-event'; import { catalogGraphRouteRef } from '../../routes'; import { CatalogGraphCard } from './CatalogGraphCard'; import Button from '@material-ui/core/Button'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; describe('', () => { let entity: Entity; @@ -52,7 +53,10 @@ describe('', () => { namespace: 'd', }, }; - apis = TestApiRegistry.from([catalogApiRef, catalog]); + apis = TestApiRegistry.from( + [catalogApiRef, catalog], + [translationApiRef, mockApis.translation()], + ); wrapper = ( @@ -213,7 +217,12 @@ describe('', () => { const analyticsApi = mockApis.analytics(); await renderInTestApp( - + {wrapper} , { diff --git a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx index a75993a1bc..733f209990 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphCard/CatalogGraphCard.tsx @@ -38,6 +38,8 @@ import { EntityRelationsGraph, EntityRelationsGraphProps, } from '../EntityRelationsGraph'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { catalogGraphTranslationRef } from '../../translation'; /** @public */ export type CatalogGraphCardClassKey = 'card' | 'graph'; @@ -66,6 +68,7 @@ export const CatalogGraphCard = ( action?: ReactNode; }, ) => { + const { t } = useTranslationRef(catalogGraphTranslationRef); const { variant = 'gridItem', relationPairs = ALL_RELATION_PAIRS, @@ -81,7 +84,7 @@ export const CatalogGraphCard = ( action, rootEntityNames, onNodeClick, - title = 'Relations', + title = t('catalogGraphCard.title'), zoom = 'enable-on-click', } = props; @@ -133,7 +136,7 @@ export const CatalogGraphCard = ( variant={variant} noPadding deepLink={{ - title: 'View graph', + title: t('catalogGraphCard.deepLinkTitle'), link: catalogGraphUrl, }} > diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx index cf3e628a98..69cb04bf3a 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CatalogGraphPage.tsx @@ -50,6 +50,8 @@ import { SelectedKindsFilter } from './SelectedKindsFilter'; import { SelectedRelationsFilter } from './SelectedRelationsFilter'; import { SwitchFilter } from './SwitchFilter'; import { useCatalogGraphPage } from './useCatalogGraphPage'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { catalogGraphTranslationRef } from '../../translation'; /** @public */ export type CatalogGraphPageClassKey = @@ -136,7 +138,7 @@ export const CatalogGraphPage = ( initialState, entityFilter, } = props; - + const { t } = useTranslationRef(catalogGraphTranslationRef); const navigate = useNavigate(); const classes = useStyles(); const catalogEntityRoute = useRouteRef(entityRouteRef); @@ -192,7 +194,7 @@ export const CatalogGraphPage = ( return (
humanizeEntityRef(e)).join(', ')} /> @@ -203,13 +205,12 @@ export const CatalogGraphPage = ( selected={showFilters} onChange={() => toggleShowFilters()} > - Filters + {t('catalogGraphPage.filterToggleButtonTitle')} } > - Start tracking your component in by adding it to the software - catalog. + {t('catalogGraphPage.supportButtonDescription')} @@ -230,12 +231,12 @@ export const CatalogGraphPage = ( )} @@ -247,9 +248,8 @@ export const CatalogGraphPage = ( display="block" className={classes.legend} > - Use pinch & zoom to move - around the diagram. Click to change active node, shift click to - navigate to entity. + {' '} + {t('catalogGraphPage.zoomOutDescription')} ', () => { - test('should display current curve label', () => { + test('should display current curve label', async () => { const onChange = jest.fn(); - render(); + await renderInTestApp( + , + ); expect(screen.getByText('Monotone X')).toBeInTheDocument(); }); test('should select an alternative curve factory', async () => { const onChange = jest.fn(); - render(); + await renderInTestApp( + , + ); expect(screen.getByText('Step Before')).toBeInTheDocument(); diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.tsx index 411c144b05..0e6a941b1a 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/CurveFilter.tsx @@ -16,12 +16,10 @@ import { Select, SelectedItems } from '@backstage/core-components'; import Box from '@material-ui/core/Box'; import { useCallback } from 'react'; +import { catalogGraphTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; type Curve = 'curveStepBefore' | 'curveMonotoneX'; -const CURVE_DISPLAY_NAMES: Record = { - curveMonotoneX: 'Monotone X', - curveStepBefore: 'Step Before', -}; export type Props = { value: Curve; @@ -31,6 +29,12 @@ export type Props = { const curves: Array = ['curveMonotoneX', 'curveStepBefore']; export const CurveFilter = ({ value, onChange }: Props) => { + const { t } = useTranslationRef(catalogGraphTranslationRef); + const CURVE_DISPLAY_NAMES: Record = { + curveMonotoneX: t('catalogGraphPage.curveFilter.curveMonotoneX'), + curveStepBefore: t('catalogGraphPage.curveFilter.curveStepBefore'), + }; + const handleChange = useCallback( (v: SelectedItems) => onChange(v as Curve), [onChange], @@ -39,7 +43,7 @@ export const CurveFilter = ({ value, onChange }: Props) => { return ( ({ label: DIRECTION_DISPLAY_NAMES[v], diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx index ef3df7eb53..2e4d5d44a3 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.test.tsx @@ -14,20 +14,21 @@ * limitations under the License. */ -import { render, screen } from '@testing-library/react'; +import { screen } from '@testing-library/react'; import user from '@testing-library/user-event'; import { MaxDepthFilter } from './MaxDepthFilter'; +import { renderInTestApp } from '@backstage/test-utils'; describe('', () => { - test('should display current value', () => { - render( {}} />); + test('should display current value', async () => { + await renderInTestApp( {}} />); expect(screen.getByLabelText('maxp')).toBeInTheDocument(); expect(screen.getByLabelText('maxp')).toHaveValue(5); }); - test('should display infinite if non finite', () => { - render( + test('should display infinite if non finite', async () => { + await renderInTestApp( {}} />, ); @@ -37,7 +38,7 @@ describe('', () => { test('should clear max depth', async () => { const onChange = jest.fn(); - render(); + await renderInTestApp(); expect(onChange).not.toHaveBeenCalled(); await user.click(screen.getByLabelText('clear max depth')); @@ -46,7 +47,7 @@ describe('', () => { test('should set max depth to undefined if below one', async () => { const onChange = jest.fn(); - render(); + await renderInTestApp(); await user.clear(screen.getByLabelText('maxp')); await user.type(screen.getByLabelText('maxp'), '0'); @@ -56,7 +57,7 @@ describe('', () => { test('should select direction', async () => { let value = 5; - render( + await renderInTestApp( { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx index 33742ad72e..cdd27b9adb 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/MaxDepthFilter.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +import { useTranslationRef } from '@backstage/frontend-plugin-api'; import Box from '@material-ui/core/Box'; import FormControl from '@material-ui/core/FormControl'; import IconButton from '@material-ui/core/IconButton'; @@ -22,6 +23,7 @@ import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import ClearIcon from '@material-ui/icons/Clear'; import { ChangeEvent, useCallback, useEffect, useRef, useState } from 'react'; +import { catalogGraphTranslationRef } from '../../translation'; export type Props = { value: number; @@ -45,6 +47,7 @@ export const MaxDepthFilter = ({ value, onChange }: Props) => { const classes = useStyles(); const onChangeRef = useRef(onChange); const [currentValue, setCurrentValue] = useState(value); + const { t } = useTranslationRef(catalogGraphTranslationRef); // Keep a fresh reference to the latest callback useEffect(() => { @@ -75,16 +78,20 @@ export const MaxDepthFilter = ({ value, onChange }: Props) => { return ( - Max Depth + + {t('catalogGraphPage.maxDepthFilter.title')} + diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx index 929b22755d..4349d7005a 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.test.tsx @@ -15,13 +15,18 @@ */ import { ApiProvider } from '@backstage/core-app-api'; -import { AlertApi, alertApiRef } from '@backstage/core-plugin-api'; +import { AlertApi, alertApiRef, errorApiRef } from '@backstage/core-plugin-api'; import { catalogApiRef } from '@backstage/plugin-catalog-react'; -import { renderWithEffects, TestApiRegistry } from '@backstage/test-utils'; +import { + mockApis, + renderWithEffects, + TestApiRegistry, +} from '@backstage/test-utils'; import { waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { SelectedKindsFilter } from './SelectedKindsFilter'; import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils'; +import { translationApiRef } from '@backstage/core-plugin-api/alpha'; const catalogApi = catalogApiMock.mock({ getEntityFacets: jest.fn().mockResolvedValue({ @@ -38,6 +43,8 @@ const catalogApi = catalogApiMock.mock({ const apis = TestApiRegistry.from( [catalogApiRef, catalogApi], [alertApiRef, {} as AlertApi], + [translationApiRef, mockApis.translation()], + [errorApiRef, { post: jest.fn() }], ); describe('', () => { diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx index 4970c4527a..d9a5ef9833 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedKindsFilter.tsx @@ -27,6 +27,8 @@ import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import Autocomplete from '@material-ui/lab/Autocomplete'; import { useCallback, useEffect, useMemo } from 'react'; import useAsync from 'react-use/esm/useAsync'; +import { catalogGraphTranslationRef } from '../../translation'; +import { useTranslationRef } from '@backstage/frontend-plugin-api'; /** @public */ export type SelectedKindsFilterClassKey = 'formControl'; @@ -49,6 +51,7 @@ export const SelectedKindsFilter = ({ value, onChange }: Props) => { const classes = useStyles(); const alertApi = useApi(alertApiRef); const catalogApi = useApi(catalogApiRef); + const { t } = useTranslationRef(catalogGraphTranslationRef); const { error, value: kinds } = useAsync(async () => { return await catalogApi @@ -91,13 +94,15 @@ export const SelectedKindsFilter = ({ value, onChange }: Props) => { return ( - Kinds + + {t('catalogGraphPage.selectedKindsFilter.title')} + kinds[normalizedKinds.indexOf(k)] ?? k} diff --git a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx index 41df29d072..057842bcdc 100644 --- a/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx +++ b/plugins/catalog-graph/src/components/CatalogGraphPage/SelectedRelationsFilter.test.tsx @@ -19,14 +19,15 @@ import { RELATION_HAS_MEMBER, RELATION_OWNED_BY, } from '@backstage/catalog-model'; -import { render, waitFor, screen } from '@testing-library/react'; +import { waitFor, screen } from '@testing-library/react'; import userEvent from '@testing-library/user-event'; import { ALL_RELATION_PAIRS } from '../EntityRelationsGraph'; import { SelectedRelationsFilter } from './SelectedRelationsFilter'; +import { renderInTestApp } from '@backstage/test-utils'; describe('', () => { - test('should render current value', () => { - render( + test('should render current value', async () => { + await renderInTestApp( ', () => { test('should select value', async () => { const onChange = jest.fn(); - render( + await renderInTestApp( ', () => { test('should return undefined if all values are selected', async () => { const onChange = jest.fn(); - render( + await renderInTestApp( p).filter( @@ -92,7 +93,7 @@ describe('', () => { test('should return all values when cleared', async () => { const onChange = jest.fn(); - render( + await renderInTestApp( { const classes = useStyles(); const relations = useMemo(() => relationPairs.flat(), [relationPairs]); + const { t } = useTranslationRef(catalogGraphTranslationRef); const handleChange = useCallback( (_: unknown, v: string[]) => { @@ -65,13 +68,15 @@ export const SelectedRelationsFilter = ({ return ( - Relations + + {t('catalogGraphPage.selectedRelationsFilter.title')} +