diff --git a/docs/features/software-catalog/entity-presentation.md b/docs/features/software-catalog/entity-presentation.md index c5ca8edada..a883f93339 100644 --- a/docs/features/software-catalog/entity-presentation.md +++ b/docs/features/software-catalog/entity-presentation.md @@ -52,24 +52,12 @@ function MyComponent({ entityRef }: { entityRef: string }) { The hook subscribes to the `EntityPresentationApi` and returns a snapshot that may update over time as additional data is fetched in the background. -### Using the API directly +### Using the API directly (async, preferred for non-React) -In contexts where hooks are not available, you can use the -`entityPresentationApiRef` API directly. The API provides two access -patterns: - -- **`.snapshot`** for synchronous access (for example in sort comparators or - filter callbacks): - -```ts -import { entityPresentationApiRef } from '@backstage/plugin-catalog-react'; - -const title = entityPresentationApi.forEntity(entity, { - defaultKind: 'Component', -}).snapshot.primaryTitle; -``` - -- **`.promise`** for async contexts (for example inside data loaders): +In non-React **async** contexts where you can `await` -- such as data +loaders, `useAsync` callbacks, or event handlers -- use the +`entityPresentationApiRef` API directly with `.promise` for the richest +possible presentation: ```ts const presentation = await entityPresentationApi.forEntity(entity, { @@ -78,9 +66,39 @@ const presentation = await entityPresentationApi.forEntity(entity, { const title = presentation.primaryTitle; ``` -The `.snapshot` path uses cached data when available, so it performs well -even in tight loops like sorting. The `.promise` path resolves to a richer -presentation that may include data fetched from the catalog. +The `.promise` path resolves to a full presentation that may include data +fetched from the catalog. **This is the preferred approach whenever an +async context is available.** + +### `entityPresentationSnapshot` helper (synchronous fallback) + +When a synchronous return value is required and `await` is not possible -- +such as in sort comparators, column factories, or filter callbacks -- use +`entityPresentationSnapshot` as a fallback. It accepts `Entity`, +`CompoundEntityRef`, or string ref inputs and uses the presentation API +when available, falling back to `defaultEntityPresentation` otherwise: + +```ts +import { + entityPresentationSnapshot, + entityPresentationApiRef, +} from '@backstage/plugin-catalog-react'; + +// In a column factory or sort comparator where you have +// the API instance (or undefined if not registered): +const title = entityPresentationSnapshot( + entity, + { + defaultKind: 'Component', + }, + entityPresentationApi, +).primaryTitle; +``` + +Because this function is synchronous, it uses cached data from the +presentation API. If the entity has been seen before, the snapshot will +contain the full resolved title; otherwise it falls back to what can be +extracted from the ref alone. ## Customizing entity presentation @@ -124,6 +142,6 @@ Replace them as follows: | :---------------------------------------------------- | :--------------------------------------------------------------------- | | `humanizeEntityRef(entity)` in JSX | `` | | `humanizeEntityRef(entity)` in a React component | `useEntityPresentation(entity).primaryTitle` | -| `humanizeEntityRef(entity)` in a sort/filter callback | `entityPresentationApi.forEntity(entity).snapshot.primaryTitle` | | `humanizeEntityRef(entity)` in an async loader | `(await entityPresentationApi.forEntity(entity).promise).primaryTitle` | +| `humanizeEntityRef(entity)` in a sort/filter callback | `entityPresentationSnapshot(entity, ctx, api).primaryTitle` | | `humanizeEntity(entity, fallback)` | `useEntityPresentation(entity).primaryTitle` | diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 251a0e853a..419a3c3f94 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -593,6 +593,16 @@ export interface EntityPresentationApi { // @public export const entityPresentationApiRef: ApiRef_2; +// @public +export function entityPresentationSnapshot( + entityOrRef: Entity | CompoundEntityRef | string, + context?: { + defaultKind?: string; + defaultNamespace?: string; + }, + entityPresentationApi?: EntityPresentationApi, +): EntityRefPresentationSnapshot; + // @public (undocumented) export const EntityProcessingStatusPicker: () => JSX_2.Element; diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts index 316bc9aff5..99ae0ceacc 100644 --- a/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts +++ b/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts @@ -37,8 +37,8 @@ import { Observable } from '@backstage/types'; * and tooltip. * * - In non-React contexts such as sort comparators or data mappers, use the - * API directly via `forEntity().snapshot` for synchronous access, or - * `forEntity().promise` in async loaders. + * {@link entityPresentationSnapshot} helper for synchronous access, or + * the API directly via `forEntity().promise` in async loaders. * * @public */ @@ -143,8 +143,8 @@ export interface EntityRefPresentation { * - {@link EntityDisplayName} — React component that renders an entity name * with optional icon and tooltip. * - * For non-React contexts, you can use the API directly via - * `forEntity().snapshot` for synchronous access, or `forEntity().promise` + * For non-React contexts, use the {@link entityPresentationSnapshot} helper + * for synchronous access, or the API directly via `forEntity().promise` * for async contexts. * * Implement this interface to customize how entities are displayed throughout diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts index 1984593cfa..98f200c67a 100644 --- a/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts +++ b/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts @@ -35,10 +35,10 @@ import { EntityRefPresentationSnapshot } from './EntityPresentationApi'; * * This function is primarily used as the internal fallback within the * {@link EntityPresentationApi} when no custom implementation is registered. - * Prefer using the API directly via `forEntity().snapshot` or - * `forEntity().promise`, which respects custom presentation overrides. * In React components, use the {@link useEntityPresentation} hook or the - * {@link EntityDisplayName} component. + * {@link EntityDisplayName} component. In non-React contexts, use + * {@link entityPresentationSnapshot} which respects custom presentation + * overrides and falls back to this function when no API is registered. * * @public * @param entityOrRef - Either an entity, or a ref to it. diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/entityPresentationSnapshot.test.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/entityPresentationSnapshot.test.ts new file mode 100644 index 0000000000..8ff27c3282 --- /dev/null +++ b/plugins/catalog-react/src/apis/EntityPresentationApi/entityPresentationSnapshot.test.ts @@ -0,0 +1,173 @@ +/* + * Copyright 2026 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 { CompoundEntityRef, Entity } from '@backstage/catalog-model'; +import { entityPresentationSnapshot } from './entityPresentationSnapshot'; +import { + EntityPresentationApi, + EntityRefPresentation, + EntityRefPresentationSnapshot, +} from './EntityPresentationApi'; + +function createMockApi(): EntityPresentationApi & { + calls: Array<{ entityOrRef: Entity | string; context?: object }>; +} { + const calls: Array<{ entityOrRef: Entity | string; context?: object }> = []; + return { + calls, + forEntity(entityOrRef, context) { + calls.push({ entityOrRef, context }); + const snapshot: EntityRefPresentationSnapshot = { + entityRef: typeof entityOrRef === 'string' ? entityOrRef : 'mock-ref', + primaryTitle: 'from-api', + secondaryTitle: undefined, + Icon: undefined, + }; + return { + snapshot, + promise: Promise.resolve(snapshot), + } as EntityRefPresentation; + }, + }; +} + +describe('entityPresentationSnapshot', () => { + describe('with EntityPresentationApi', () => { + it('passes Entity directly to forEntity', () => { + const api = createMockApi(); + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'test', namespace: 'default' }, + }; + + const result = entityPresentationSnapshot(entity, undefined, api); + + expect(result.primaryTitle).toBe('from-api'); + expect(api.calls).toHaveLength(1); + expect(api.calls[0].entityOrRef).toBe(entity); + }); + + it('stringifies CompoundEntityRef before passing to forEntity', () => { + const api = createMockApi(); + const ref: CompoundEntityRef = { + kind: 'group', + namespace: 'default', + name: 'my-team', + }; + + const result = entityPresentationSnapshot(ref, undefined, api); + + expect(result.primaryTitle).toBe('from-api'); + expect(api.calls).toHaveLength(1); + expect(api.calls[0].entityOrRef).toBe('group:default/my-team'); + expect(typeof api.calls[0].entityOrRef).toBe('string'); + }); + + it('passes string ref directly to forEntity', () => { + const api = createMockApi(); + + const result = entityPresentationSnapshot( + 'component:default/test', + undefined, + api, + ); + + expect(result.primaryTitle).toBe('from-api'); + expect(api.calls).toHaveLength(1); + expect(api.calls[0].entityOrRef).toBe('component:default/test'); + }); + + it('forwards context to forEntity', () => { + const api = createMockApi(); + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { name: 'test', namespace: 'default' }, + }; + + entityPresentationSnapshot( + entity, + { defaultKind: 'component', defaultNamespace: 'custom' }, + api, + ); + + expect(api.calls[0].context).toEqual({ + defaultKind: 'component', + defaultNamespace: 'custom', + }); + }); + }); + + describe('without EntityPresentationApi', () => { + it('falls back to defaultEntityPresentation for Entity', () => { + const entity: Entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + namespace: 'default', + title: 'My Component', + }, + }; + + const result = entityPresentationSnapshot(entity); + + expect(result).toEqual({ + entityRef: 'component:default/test', + primaryTitle: 'My Component', + secondaryTitle: 'component:default/test', + Icon: undefined, + }); + }); + + it('falls back to defaultEntityPresentation for CompoundEntityRef', () => { + const ref: CompoundEntityRef = { + kind: 'group', + namespace: 'default', + name: 'my-team', + }; + + const result = entityPresentationSnapshot(ref); + + expect(result).toEqual({ + entityRef: 'group:default/my-team', + primaryTitle: 'my-team', + secondaryTitle: 'group:default/my-team', + Icon: undefined, + }); + }); + + it('falls back to defaultEntityPresentation for string ref', () => { + const result = entityPresentationSnapshot('component:default/test'); + + expect(result).toEqual({ + entityRef: 'component:default/test', + primaryTitle: 'test', + secondaryTitle: 'component:default/test', + Icon: undefined, + }); + }); + + it('forwards context to defaultEntityPresentation', () => { + const result = entityPresentationSnapshot('component:default/test', { + defaultKind: 'component', + }); + + expect(result.primaryTitle).toBe('test'); + }); + }); +}); diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/entityPresentationSnapshot.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/entityPresentationSnapshot.ts new file mode 100644 index 0000000000..a5ee876bdf --- /dev/null +++ b/plugins/catalog-react/src/apis/EntityPresentationApi/entityPresentationSnapshot.ts @@ -0,0 +1,70 @@ +/* + * Copyright 2026 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 { + CompoundEntityRef, + Entity, + stringifyEntityRef, +} from '@backstage/catalog-model'; +import { + EntityPresentationApi, + EntityRefPresentationSnapshot, +} from './EntityPresentationApi'; +import { defaultEntityPresentation } from './defaultEntityPresentation'; + +/** + * Returns a synchronous presentation snapshot for an entity in non-React + * contexts. + * + * @remarks + * + * This is the synchronous, non-React counterpart to + * {@link useEntityPresentation}. It handles `Entity`, `CompoundEntityRef`, + * and string ref inputs uniformly, using the provided + * {@link EntityPresentationApi} when available and falling back to + * {@link defaultEntityPresentation} otherwise. + * + * Because this function is synchronous, it uses cached data from the + * presentation API (via `.snapshot`). If the entity has been seen before, + * the snapshot will contain the full resolved title; otherwise it falls + * back to what can be extracted from the ref alone. This is the correct + * trade-off for sort comparators, column factories, filter callbacks, and + * data mappers where a synchronous return value is required. + * + * In async contexts such as data loaders where you can `await`, prefer + * using the {@link EntityPresentationApi} directly via + * `forEntity().promise` for the richest possible presentation. + * + * @public + * @param entityOrRef - An entity, a compound entity ref, or a string entity ref. + * @param context - Optional context that may affect the presentation. + * @param entityPresentationApi - Optional presentation API instance. When not + * provided, falls back to {@link defaultEntityPresentation}. + */ +export function entityPresentationSnapshot( + entityOrRef: Entity | CompoundEntityRef | string, + context?: { defaultKind?: string; defaultNamespace?: string }, + entityPresentationApi?: EntityPresentationApi, +): EntityRefPresentationSnapshot { + if (entityPresentationApi) { + const ref = + typeof entityOrRef === 'string' || 'metadata' in entityOrRef + ? entityOrRef + : stringifyEntityRef(entityOrRef); + return entityPresentationApi.forEntity(ref, context).snapshot; + } + return defaultEntityPresentation(entityOrRef, context); +} diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/index.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/index.ts index 405afe23f6..8369a69167 100644 --- a/plugins/catalog-react/src/apis/EntityPresentationApi/index.ts +++ b/plugins/catalog-react/src/apis/EntityPresentationApi/index.ts @@ -21,4 +21,5 @@ export { type EntityRefPresentationSnapshot, } from './EntityPresentationApi'; export { defaultEntityPresentation } from './defaultEntityPresentation'; +export { entityPresentationSnapshot } from './entityPresentationSnapshot'; export { useEntityPresentation } from './useEntityPresentation'; diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts index 125557ceb7..68694f167a 100644 --- a/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts +++ b/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts @@ -43,7 +43,7 @@ import { useUpdatingObservable } from './useUpdatingObservable'; * component instead, which wraps this hook with icon and tooltip support. * * For non-React contexts such as sort comparators or data mappers, use - * the {@link EntityPresentationApi} directly via `forEntity().snapshot`. + * {@link entityPresentationSnapshot}. * * @public * @param entityOrRef - The entity to represent, or an entity ref to it. If you diff --git a/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx b/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx index b0ff221e9d..80a4cc8070 100644 --- a/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx +++ b/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx @@ -22,7 +22,7 @@ import { import { Cell, CellText, Column, ColumnConfig, TableItem } from '@backstage/ui'; import { EntityRefLink, EntityRefLinks } from '../EntityRefLink'; import { - defaultEntityPresentation, + entityPresentationSnapshot, EntityPresentationApi, } from '@backstage/plugin-catalog-react'; import { EntityTableColumnTitle } from '../EntityTable/TitleColumn'; @@ -36,18 +36,6 @@ export interface EntityColumnConfig extends ColumnConfig { sortValue?: (entity: EntityRow) => string; } -function getEntityTitle( - entityOrRef: Entity | { kind: string; namespace?: string; name: string }, - context: { defaultKind?: string }, - entityPresentationApi?: EntityPresentationApi, -): string { - if (entityPresentationApi) { - return entityPresentationApi.forEntity(entityOrRef as Entity, context) - .snapshot.primaryTitle; - } - return defaultEntityPresentation(entityOrRef as Entity, context).primaryTitle; -} - /** @public */ export const columnFactories = Object.freeze({ createEntityRefColumn(options: { @@ -76,11 +64,11 @@ export const columnFactories = Object.freeze({ ), sortValue: entity => - getEntityTitle( + entityPresentationSnapshot( entity, { defaultKind: options.defaultKind }, options.entityPresentationApi, - ), + ).primaryTitle, }; }, @@ -115,12 +103,13 @@ export const columnFactories = Object.freeze({ ), sortValue: entity => getEntityRelations(entity, options.relation, options.filter) - .map(r => - getEntityTitle( - r, - { defaultKind: options.defaultKind }, - options.entityPresentationApi, - ), + .map( + r => + entityPresentationSnapshot( + r, + { defaultKind: options.defaultKind }, + options.entityPresentationApi, + ).primaryTitle, ) .join(', '), }; diff --git a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx index a6b4ed94fe..1f60333acd 100644 --- a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx +++ b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx @@ -70,7 +70,7 @@ export type EntityDisplayNameProps = { * * For more control over the presentation data, use the * {@link useEntityPresentation} hook directly. For non-React contexts, use - * the {@link EntityPresentationApi} directly via `forEntity().snapshot`. + * {@link entityPresentationSnapshot}. * * @public */ diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx index 1375140ebc..291fcf5376 100644 --- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx +++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx @@ -34,8 +34,8 @@ import { useDebouncedEffect } from '@react-hookz/web'; import PersonIcon from '@material-ui/icons/Person'; import GroupIcon from '@material-ui/icons/Group'; import { - defaultEntityPresentation, entityPresentationApiRef, + entityPresentationSnapshot, } from '../../apis'; import { useFetchEntities } from './useFetchEntities'; import { withStyles } from '@material-ui/core/styles'; @@ -209,11 +209,11 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => { defaultNamespace: 'default', }) : o; - if (entityPresentationApi) { - return entityPresentationApi.forEntity(entity as Entity).snapshot - .primaryTitle; - } - return defaultEntityPresentation(entity).primaryTitle; + return entityPresentationSnapshot( + entity, + undefined, + entityPresentationApi, + ).primaryTitle; }} onChange={(_: object, owners) => { setText(''); diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts index 80bd290806..fe697365b6 100644 --- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts +++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts @@ -26,9 +26,10 @@ import get from 'lodash/get'; * if set to string which matches namespace of entity then omitted * * @deprecated Use {@link useEntityPresentation} or {@link EntityDisplayName} - * in React components, or access the {@link entityPresentationApiRef} directly. - * These provide richer display names using `metadata.title` and - * `spec.profile.displayName` in addition to the entity ref. + * in React components. In non-React contexts such as sort comparators or + * data mappers, use {@link entityPresentationSnapshot}. These provide richer + * display names using `metadata.title` and `spec.profile.displayName` in + * addition to the entity ref. * * @public **/ @@ -82,7 +83,8 @@ export function humanizeEntityRef( * If neither of those are found or populated, fallback to `defaultName`. * * @deprecated Use {@link useEntityPresentation} or {@link EntityDisplayName} - * in React components, or access the {@link entityPresentationApiRef} directly. + * in React components. In non-React contexts, use + * {@link entityPresentationSnapshot}. * * @param entity - Entity to convert. * @param defaultName - If entity readable name is not available, `defaultName` will be returned. diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx index 01d1c7e1f3..82dd8e7309 100644 --- a/plugins/catalog-react/src/components/EntityTable/columns.tsx +++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx @@ -23,21 +23,9 @@ import { import { OverflowTooltip, TableColumn } from '@backstage/core-components'; import { getEntityRelations } from '../../utils'; import { EntityRefLink, EntityRefLinks } from '../EntityRefLink'; -import { defaultEntityPresentation, EntityPresentationApi } from '../../apis'; +import { entityPresentationSnapshot, EntityPresentationApi } from '../../apis'; import { EntityTableColumnTitle } from './TitleColumn'; -function getEntityTitle( - entityOrRef: Entity | CompoundEntityRef, - context: { defaultKind?: string }, - entityPresentationApi?: EntityPresentationApi, -): string { - if (entityPresentationApi) { - return entityPresentationApi.forEntity(entityOrRef as Entity, context) - .snapshot.primaryTitle; - } - return defaultEntityPresentation(entityOrRef, context).primaryTitle; -} - /** @public */ export const columnFactories = Object.freeze({ createEntityRefColumn(options: { @@ -46,7 +34,11 @@ export const columnFactories = Object.freeze({ }): TableColumn { const { defaultKind, entityPresentationApi } = options; function formatContent(entity: T): string { - return getEntityTitle(entity, { defaultKind }, entityPresentationApi); + return entityPresentationSnapshot( + entity, + { defaultKind }, + entityPresentationApi, + ).primaryTitle; } return { @@ -96,7 +88,14 @@ export const columnFactories = Object.freeze({ function formatContent(entity: T): string { return getRelations(entity) - .map(r => getEntityTitle(r, { defaultKind }, entityPresentationApi)) + .map( + r => + entityPresentationSnapshot( + r, + { defaultKind }, + entityPresentationApi, + ).primaryTitle, + ) .join(', '); } diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx index 6c55ff21a3..b021aae0e1 100644 --- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx +++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx @@ -16,7 +16,6 @@ import { ANNOTATION_EDIT_URL, ANNOTATION_VIEW_URL, - CompoundEntityRef, Entity, RELATION_OWNED_BY, RELATION_PART_OF, @@ -30,8 +29,8 @@ import { WarningPanel, } from '@backstage/core-components'; import { - defaultEntityPresentation, entityPresentationApiRef, + entityPresentationSnapshot, getEntityRelations, useEntityList, useStarredEntities, @@ -73,21 +72,13 @@ export interface CatalogTableProps { subtitle?: string; } -function getTitle( - entityOrRef: Entity | CompoundEntityRef, - context: { defaultKind?: string }, - api?: EntityPresentationApi, -): string { - if (api) { - const ref = - 'metadata' in entityOrRef ? entityOrRef : stringifyEntityRef(entityOrRef); - return api.forEntity(ref, context).snapshot.primaryTitle; - } - return defaultEntityPresentation(entityOrRef, context).primaryTitle; -} - const sortEntities = (entities: Entity[], api?: EntityPresentationApi) => { - return sortBy(entities, e => getTitle(e, { defaultKind: 'Component' }, api)); + return sortBy( + entities, + e => + entityPresentationSnapshot(e, { defaultKind: 'Component' }, api) + .primaryTitle, + ); }; /** @@ -307,14 +298,26 @@ function toEntityRow(entity: Entity, api?: EntityPresentationApi) { // This name is here for backwards compatibility mostly; the // presentation of refs in the table should in general be handled with // EntityRefLink / EntityName components - name: getTitle(entity, { defaultKind: 'Component' }, api), + name: entityPresentationSnapshot( + entity, + { defaultKind: 'Component' }, + api, + ).primaryTitle, entityRef: stringifyEntityRef(entity), ownedByRelationsTitle: ownedByRelations - .map(r => getTitle(r, { defaultKind: 'group' }, api)) + .map( + r => + entityPresentationSnapshot(r, { defaultKind: 'group' }, api) + .primaryTitle, + ) .join(', '), ownedByRelations, partOfSystemRelationTitle: partOfSystemRelations - .map(r => getTitle(r, { defaultKind: 'system' }, api)) + .map( + r => + entityPresentationSnapshot(r, { defaultKind: 'system' }, api) + .primaryTitle, + ) .join(', '), partOfSystemRelations, }, diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx index b436f56427..efb1a06822 100644 --- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx +++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx @@ -186,7 +186,7 @@ export const TemplateFormPreviewer = ({ severity: 'error', }), ), - [catalogApi], + [catalogApi, entityPresentationApi, alertApi], ); const handleSelectChange = useCallback( diff --git a/plugins/techdocs/src/home/components/Tables/helpers.ts b/plugins/techdocs/src/home/components/Tables/helpers.ts index 4f2424340e..b0b5c41fcd 100644 --- a/plugins/techdocs/src/home/components/Tables/helpers.ts +++ b/plugins/techdocs/src/home/components/Tables/helpers.ts @@ -14,13 +14,9 @@ * limitations under the License. */ +import { RELATION_OWNED_BY, Entity } from '@backstage/catalog-model'; import { - RELATION_OWNED_BY, - Entity, - stringifyEntityRef, -} from '@backstage/catalog-model'; -import { - defaultEntityPresentation, + entityPresentationSnapshot, getEntityRelations, type EntityPresentationApi, } from '@backstage/plugin-catalog-react'; @@ -54,15 +50,14 @@ export function entitiesToDocsMapper( }), ownedByRelations, ownedByRelationsTitle: ownedByRelations - .map(r => { - if (entityPresentationApi) { - return entityPresentationApi.forEntity(stringifyEntityRef(r), { - defaultKind: 'group', - }).snapshot.primaryTitle; - } - return defaultEntityPresentation(r, { defaultKind: 'group' }) - .primaryTitle; - }) + .map( + r => + entityPresentationSnapshot( + r, + { defaultKind: 'group' }, + entityPresentationApi, + ).primaryTitle, + ) .join(', '), }, };