diff --git a/.changeset/lovely-frogs-share.md b/.changeset/lovely-frogs-share.md new file mode 100644 index 0000000000..5c630f301e --- /dev/null +++ b/.changeset/lovely-frogs-share.md @@ -0,0 +1,6 @@ +--- +'@backstage/plugin-catalog-react': patch +'@backstage/plugin-catalog': patch +--- + +Catalog table columns support i18n diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 1db7a8e5e4..ebdf9f0e79 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -86,6 +86,18 @@ export const catalogReactTranslationRef: TranslationRef< readonly 'userListPicker.personalFilter.title': 'Personal'; readonly 'userListPicker.personalFilter.ownedLabel': 'Owned'; readonly 'userListPicker.personalFilter.starredLabel': 'Starred'; + readonly 'entityTableColumnTitle.name': 'Name'; + readonly 'entityTableColumnTitle.type': 'Type'; + readonly 'entityTableColumnTitle.label': 'Label'; + readonly 'entityTableColumnTitle.title': 'Title'; + readonly 'entityTableColumnTitle.description': 'Description'; + readonly 'entityTableColumnTitle.domain': 'Domain'; + readonly 'entityTableColumnTitle.namespace': 'Namespace'; + readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle'; + readonly 'entityTableColumnTitle.owner': 'Owner'; + readonly 'entityTableColumnTitle.system': 'System'; + readonly 'entityTableColumnTitle.targets': 'Targets'; + readonly 'entityTableColumnTitle.tags': 'Tags'; } >; @@ -517,6 +529,40 @@ export type EntityPredicateValue = $contains: EntityPredicateExpression; }; +// @alpha (undocumented) +export const EntityTableColumnTitle: ({ + translationKey, +}: EntityTableColumnTitleProps) => + | 'Domain' + | 'System' + | 'Name' + | 'Description' + | 'Lifecycle' + | 'Namespace' + | 'Owner' + | 'Tags' + | 'Type' + | 'Targets' + | 'Title' + | 'Label'; + +// @alpha (undocumented) +export type EntityTableColumnTitleProps = { + translationKey: + | 'name' + | 'system' + | 'owner' + | 'type' + | 'lifecycle' + | 'namespace' + | 'description' + | 'tags' + | 'targets' + | 'title' + | 'label' + | 'domain'; +}; + // @alpha export function isOwnerOf(owner: Entity, entity: Entity): boolean; diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index bfebb2b13a..df32c51416 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -171,7 +171,7 @@ export const columnFactories: Readonly<{ defaultKind?: string; }): TableColumn; createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { @@ -575,7 +575,7 @@ export const EntityTable: { defaultKind?: string; }): TableColumn; createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index 4ff4dbf0dd..f5909c9aa2 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -20,3 +20,4 @@ export * from './predicates'; export { catalogReactTranslationRef } from '../translation'; export { isOwnerOf } from '../utils/isOwnerOf'; export { useEntityPermission } from '../hooks/useEntityPermission'; +export * from '../components/EntityTable/TitleColumn'; diff --git a/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx b/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx new file mode 100644 index 0000000000..e08741d274 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/TitleColumn.test.tsx @@ -0,0 +1,25 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { screen } from '@testing-library/react'; +import { EntityTableColumnTitle } from './TitleColumn'; +import { renderInTestApp } from '@backstage/test-utils'; + +describe('', () => { + it('renders the translated title for the given key', async () => { + await renderInTestApp(); + expect(screen.getByText('Name')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx b/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx new file mode 100644 index 0000000000..36bce84882 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityTable/TitleColumn.tsx @@ -0,0 +1,47 @@ +/* + * Copyright 2025 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { useTranslationRef } from '@backstage/frontend-plugin-api'; +import { catalogReactTranslationRef } from '../../translation'; + +/** + * @alpha + */ +export type EntityTableColumnTitleProps = { + translationKey: + | 'name' + | 'system' + | 'owner' + | 'type' + | 'lifecycle' + | 'namespace' + | 'description' + | 'tags' + | 'targets' + | 'title' + | 'label' + | 'domain'; +}; + +/** + * @alpha + */ +export const EntityTableColumnTitle = ({ + translationKey, +}: EntityTableColumnTitleProps) => { + const { t } = useTranslationRef(catalogReactTranslationRef); + return t(`entityTableColumnTitle.${translationKey}`); +}; diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index 00e1e5ad43..94b1141057 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -27,8 +27,7 @@ import { EntityRefLinks, humanizeEntityRef, } from '../EntityRefLink'; - -// TODO: column title support i18n +import { EntityTableColumnTitle } from './TitleColumn'; /** @public */ export const columnFactories = Object.freeze({ @@ -46,7 +45,7 @@ export const columnFactories = Object.freeze({ } return { - title: 'Name', + title: , highlight: true, customFilterAndSearch(filter, entity) { // TODO: We could implement this more efficiently, like searching over @@ -72,7 +71,7 @@ export const columnFactories = Object.freeze({ }; }, createEntityRelationColumn(options: { - title: string; + title: string | JSX.Element; relation: string; defaultKind?: string; filter?: { kind: string }; @@ -109,14 +108,14 @@ export const columnFactories = Object.freeze({ }, createOwnerColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'Owner', + title: , relation: RELATION_OWNED_BY, defaultKind: 'group', }); }, createDomainColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'Domain', + title: , relation: RELATION_PART_OF, defaultKind: 'domain', filter: { @@ -126,7 +125,7 @@ export const columnFactories = Object.freeze({ }, createSystemColumn(): TableColumn { return this.createEntityRelationColumn({ - title: 'System', + title: , relation: RELATION_PART_OF, defaultKind: 'system', filter: { @@ -136,7 +135,7 @@ export const columnFactories = Object.freeze({ }, createMetadataDescriptionColumn(): TableColumn { return { - title: 'Description', + title: , field: 'metadata.description', render: entity => ( (): TableColumn { return { - title: 'Lifecycle', + title: , field: 'spec.lifecycle', }; }, createSpecTypeColumn(): TableColumn { return { - title: 'Type', + title: , field: 'spec.type', }; }, diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts index 7f9661380f..6e831886cd 100644 --- a/plugins/catalog-react/src/translation.ts +++ b/plugins/catalog-react/src/translation.ts @@ -124,5 +124,19 @@ export const catalogReactTranslationRef = createTranslationRef({ }, orgFilterAllLabel: 'All', }, + entityTableColumnTitle: { + name: 'Name', + system: 'System', + owner: 'Owner', + type: 'Type', + lifecycle: 'Lifecycle', + namespace: 'Namespace', + description: 'Description', + tags: 'Tags', + targets: 'Targets', + title: 'Title', + label: 'Label', + domain: 'Domain', + }, }, }); diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx index 05674f3e1b..65094bc568 100644 --- a/plugins/catalog/src/components/CatalogTable/columns.tsx +++ b/plugins/catalog/src/components/CatalogTable/columns.tsx @@ -23,6 +23,7 @@ import { CatalogTableRow } from './types'; import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import { Entity } from '@backstage/catalog-model'; import { JsonArray } from '@backstage/types'; +import { EntityTableColumnTitle } from '@backstage/plugin-catalog-react/alpha'; // The columnFactories symbol is not directly exported, but through the // CatalogTable.columns field. @@ -41,7 +42,7 @@ export const columnFactories = Object.freeze({ } return { - title: 'Name', + title: , field: 'resolved.entityRef', highlight: true, customSort({ entity: entity1 }, { entity: entity2 }) { @@ -59,7 +60,7 @@ export const columnFactories = Object.freeze({ }, createSystemColumn(): TableColumn { return { - title: 'System', + title: , field: 'resolved.partOfSystemRelationTitle', customFilterAndSearch: (query, row) => { if (!row.resolved.partOfSystemRelations) { @@ -83,7 +84,7 @@ export const columnFactories = Object.freeze({ }, createOwnerColumn(): TableColumn { return { - title: 'Owner', + title: , field: 'resolved.ownedByRelationsTitle', render: ({ resolved }) => ( { return { - title: 'Targets', + title: , field: 'entity.spec.targets', customFilterAndSearch: (query, row) => { let targets: JsonArray = []; @@ -132,7 +133,7 @@ export const columnFactories = Object.freeze({ } = { hidden: false }, ): TableColumn { return { - title: 'Type', + title: , field: 'entity.spec.type', hidden: options.hidden, width: 'auto', @@ -140,13 +141,13 @@ export const columnFactories = Object.freeze({ }, createSpecLifecycleColumn(): TableColumn { return { - title: 'Lifecycle', + title: , field: 'entity.spec.lifecycle', }; }, createMetadataDescriptionColumn(): TableColumn { return { - title: 'Description', + title: , field: 'entity.metadata.description', render: ({ entity }) => ( { return { - title: 'Tags', + title: , field: 'entity.metadata.tags', cellStyle: { padding: '0px 16px 0px 20px', @@ -185,7 +186,7 @@ export const columnFactories = Object.freeze({ hidden?: boolean; }): TableColumn { return { - title: 'Title', + title: , field: 'entity.metadata.title', hidden: options?.hidden, searchable: true, @@ -202,7 +203,9 @@ export const columnFactories = Object.freeze({ } return { - title: options?.title || 'Label', + title: options?.title || ( + + ), field: 'entity.metadata.labels', cellStyle: { padding: '0px 16px 0px 20px', @@ -235,7 +238,7 @@ export const columnFactories = Object.freeze({ }, createNamespaceColumn(): TableColumn { return { - title: 'Namespace', + title: , field: 'entity.metadata.namespace', width: 'auto', };