Fix unsafe type casts via shared entityPresentationSnapshot utility

Extract entityPresentationSnapshot helper to eliminate unsafe `as Entity`
casts when passing CompoundEntityRef to EntityPresentationApi.forEntity(),
which only accepts Entity | string. The helper safely discriminates input
types and stringifies CompoundEntityRef before calling the API.

- Add entityPresentationSnapshot as a public export from catalog-react
- Remove duplicated getEntityTitle/getTitle helpers across 5 files
- Fix useAsync dependency array in TemplateFormPreviewer
- Update TSDoc across all presentation API surfaces to reference the
  new helper
- Update entity-presentation.md docs with usage guidance and migration
  table

Made-with: Cursor
Signed-off-by: Marat Dyatko <maratd@spotify.com>
This commit is contained in:
Marat Dyatko
2026-04-07 12:41:29 +02:00
parent f55c195f03
commit b32ab39670
16 changed files with 371 additions and 111 deletions
@@ -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 | `<EntityDisplayName entityRef={entity} />` |
| `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` |