diff --git a/.changeset/replace-humanize-entity-ref-catalog-import.md b/.changeset/replace-humanize-entity-ref-catalog-import.md
deleted file mode 100644
index 544e91bac3..0000000000
--- a/.changeset/replace-humanize-entity-ref-catalog-import.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog-import': patch
----
-
-Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `StepPrepareCreatePullRequest`.
diff --git a/.changeset/replace-humanize-entity-ref-catalog-react.md b/.changeset/replace-humanize-entity-ref-catalog-react.md
index b44e796624..7f44b101c4 100644
--- a/.changeset/replace-humanize-entity-ref-catalog-react.md
+++ b/.changeset/replace-humanize-entity-ref-catalog-react.md
@@ -2,4 +2,4 @@
'@backstage/plugin-catalog-react': patch
---
-Replaced `humanizeEntityRef` with `defaultEntityPresentation` and `useEntityPresentation` from the Catalog Presentation API in `EntityOwnerPicker`, `EntityTable`, `EntityDataTable`, and `AncestryPage` components.
+Deprecated `humanizeEntityRef` and `humanizeEntity` in favor of the Catalog Presentation API. Use `useEntityPresentation`, `EntityDisplayName`, or `entityPresentationApiRef` instead.
diff --git a/.changeset/replace-humanize-entity-ref-catalog.md b/.changeset/replace-humanize-entity-ref-catalog.md
deleted file mode 100644
index 2a27c87f47..0000000000
--- a/.changeset/replace-humanize-entity-ref-catalog.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-catalog': patch
----
-
-Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `CatalogTable` and its column factories.
diff --git a/.changeset/replace-humanize-entity-ref-org-react.md b/.changeset/replace-humanize-entity-ref-org-react.md
deleted file mode 100644
index afb04d3348..0000000000
--- a/.changeset/replace-humanize-entity-ref-org-react.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-org-react': patch
----
-
-Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `GroupListPicker`.
diff --git a/.changeset/replace-humanize-entity-ref-plugins.md b/.changeset/replace-humanize-entity-ref-plugins.md
new file mode 100644
index 0000000000..5f523e7b88
--- /dev/null
+++ b/.changeset/replace-humanize-entity-ref-plugins.md
@@ -0,0 +1,9 @@
+---
+'@backstage/plugin-catalog': patch
+'@backstage/plugin-catalog-import': patch
+'@backstage/plugin-org-react': patch
+'@backstage/plugin-scaffolder': patch
+'@backstage/plugin-techdocs': patch
+---
+
+Replaced deprecated `humanizeEntityRef` usage with the Catalog Presentation API.
diff --git a/.changeset/replace-humanize-entity-ref-scaffolder.md b/.changeset/replace-humanize-entity-ref-scaffolder.md
deleted file mode 100644
index 5064875451..0000000000
--- a/.changeset/replace-humanize-entity-ref-scaffolder.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-scaffolder': patch
----
-
-Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `TemplateFormPreviewer`.
diff --git a/.changeset/replace-humanize-entity-ref-techdocs.md b/.changeset/replace-humanize-entity-ref-techdocs.md
deleted file mode 100644
index 374a0c2e8e..0000000000
--- a/.changeset/replace-humanize-entity-ref-techdocs.md
+++ /dev/null
@@ -1,5 +0,0 @@
----
-'@backstage/plugin-techdocs': patch
----
-
-Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in TechDocs table helpers.
diff --git a/docs/features/software-catalog/entity-presentation.md b/docs/features/software-catalog/entity-presentation.md
index 5d65baac07..c5ca8edada 100644
--- a/docs/features/software-catalog/entity-presentation.md
+++ b/docs/features/software-catalog/entity-presentation.md
@@ -11,7 +11,7 @@ name from fields such as `metadata.title` and `spec.profile.displayName`.
## Displaying entity names
-There are three ways to display entity names, depending on context:
+There are several ways to display entity names, depending on context:
### `EntityDisplayName` component
@@ -51,25 +51,36 @@ 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.
-If no presentation API is registered, it falls back to
-`defaultEntityPresentation`.
-### `defaultEntityPresentation` function
+### Using the API directly
-A synchronous helper for non-React contexts where hooks are not available.
-Use it in sort comparators, filter functions, table column factories, and
-data mappers:
+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 { defaultEntityPresentation } from '@backstage/plugin-catalog-react';
+import { entityPresentationApiRef } from '@backstage/plugin-catalog-react';
-const title = defaultEntityPresentation(entity, {
+const title = entityPresentationApi.forEntity(entity, {
defaultKind: 'Component',
-}).primaryTitle;
+}).snapshot.primaryTitle;
```
-This resolves `primaryTitle` as the first available value among
-`spec.profile.displayName`, `metadata.title`, and a shortened entity ref.
+- **`.promise`** for async contexts (for example inside data loaders):
+
+```ts
+const presentation = await entityPresentationApi.forEntity(entity, {
+ defaultKind: 'group',
+}).promise;
+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.
## Customizing entity presentation
@@ -109,9 +120,10 @@ from `metadata.title` or `spec.profile.displayName`.
Replace them as follows:
-| Old code | Replacement |
-| :------------------------------------------------------------------ | :---------------------------------------------------------------- |
-| `humanizeEntityRef(entity)` in JSX | `` |
-| `humanizeEntityRef(entity)` in a hook-accessible context | `useEntityPresentation(entity).primaryTitle` |
-| `humanizeEntityRef(entity, { defaultKind })` in a sort/filter/label | `defaultEntityPresentation(entity, { defaultKind }).primaryTitle` |
-| `humanizeEntity(entity, fallback)` | `defaultEntityPresentation(entity).primaryTitle` |
+| Old code | Replacement |
+| :---------------------------------------------------- | :--------------------------------------------------------------------- |
+| `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` |
+| `humanizeEntity(entity, fallback)` | `useEntityPresentation(entity).primaryTitle` |
diff --git a/microsite/sidebars.ts b/microsite/sidebars.ts
index 871fbe09e9..eb002736b2 100644
--- a/microsite/sidebars.ts
+++ b/microsite/sidebars.ts
@@ -263,6 +263,7 @@ export default {
'features/software-catalog/extending-the-model',
'features/software-catalog/external-integrations',
'features/software-catalog/catalog-customization',
+ 'features/software-catalog/entity-presentation',
'features/software-catalog/audit-events',
{
type: 'category',
diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx
index 6f69b739bd..8c21966ed5 100644
--- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx
+++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.test.tsx
@@ -15,7 +15,11 @@
*/
import { configApiRef, errorApiRef } from '@backstage/core-plugin-api';
-import { catalogApiRef } from '@backstage/plugin-catalog-react';
+import {
+ catalogApiRef,
+ defaultEntityPresentation,
+ entityPresentationApiRef,
+} from '@backstage/plugin-catalog-react';
import { catalogApiMock } from '@backstage/plugin-catalog-react/testUtils';
import {
mockApis,
@@ -42,6 +46,17 @@ describe('', () => {
const catalogApi = catalogApiMock.mock();
+ const entityPresentationApi: typeof entityPresentationApiRef.T = {
+ forEntity(entityOrRef, context) {
+ const presentation = defaultEntityPresentation(entityOrRef, context);
+ return {
+ snapshot: presentation,
+ update$: { subscribe: () => ({ unsubscribe: () => {} }) } as any,
+ promise: Promise.resolve(presentation),
+ };
+ },
+ };
+
const errorApi: jest.Mocked = {
error$: jest.fn(),
post: jest.fn(),
@@ -54,6 +69,7 @@ describe('', () => {
apis={[
[catalogImportApiRef, catalogImportApi],
[catalogApiRef, catalogApi],
+ [entityPresentationApiRef, entityPresentationApi],
[errorApiRef, errorApi],
[configApiRef, configApi],
]}
diff --git a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx
index 3cb9fbbd39..758ade4ee9 100644
--- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx
+++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx
@@ -20,7 +20,7 @@ import { assertError } from '@backstage/errors';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import {
catalogApiRef,
- defaultEntityPresentation,
+ entityPresentationApiRef,
} from '@backstage/plugin-catalog-react';
import Box from '@material-ui/core/Box';
import FormHelperText from '@material-ui/core/FormHelperText';
@@ -133,6 +133,7 @@ export const StepPrepareCreatePullRequest = (
const { t } = useTranslationRef(catalogImportTranslationRef);
const classes = useStyles();
const catalogApi = useApi(catalogApiRef);
+ const entityPresentationApi = useApi(entityPresentationApiRef);
const catalogImportApi = useApi(catalogImportApiRef);
const errorApi = useApi(errorApiRef);
@@ -161,12 +162,13 @@ export const StepPrepareCreatePullRequest = (
filter: { kind: 'group' },
});
- return groupEntities.items
- .map(
+ const presentations = await Promise.all(
+ groupEntities.items.map(
e =>
- defaultEntityPresentation(e, { defaultKind: 'group' }).primaryTitle,
- )
- .sort();
+ entityPresentationApi.forEntity(e, { defaultKind: 'group' }).promise,
+ ),
+ );
+ return presentations.map(p => p.primaryTitle).sort();
});
const handleResult = useCallback(
diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md
index 0135fe7d64..17bd8de8af 100644
--- a/plugins/catalog-react/report-alpha.api.md
+++ b/plugins/catalog-react/report-alpha.api.md
@@ -6,16 +6,19 @@
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { ColumnConfig } from '@backstage/ui';
import { ComponentType } from 'react';
+import { CompoundEntityRef } from '@backstage/catalog-model';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { FilterPredicate } from '@backstage/filter-predicates';
+import { IconComponent } from '@backstage/core-plugin-api';
import { IconLinkVerticalProps } from '@backstage/core-components';
import { JSX as JSX_2 } from 'react';
import { JSX as JSX_3 } from 'react/jsx-runtime';
import { JSXElementConstructor } from 'react';
+import { Observable } from '@backstage/types';
import { ReactElement } from 'react';
import { ReactNode } from 'react';
import { ResourcePermission } from '@backstage/plugin-permission-common';
@@ -183,6 +186,15 @@ export const defaultEntityContentGroups: Record<
string
>;
+// @public
+export function defaultEntityPresentation(
+ entityOrRef: Entity | CompoundEntityRef | string,
+ context?: {
+ defaultKind?: string;
+ defaultNamespace?: string;
+ },
+): EntityRefPresentationSnapshot;
+
// @alpha
export const EntityCardBlueprint: ExtensionBlueprint<{
kind: 'entity-card';
@@ -488,6 +500,7 @@ export const entityDataTableColumns: Readonly<{
createEntityRefColumn(options: {
defaultKind?: string;
isRowHeader?: boolean;
+ entityPresentation?: EntityPresentationApi;
}): EntityColumnConfig;
createEntityRelationColumn(options: {
id: string;
@@ -497,6 +510,7 @@ export const entityDataTableColumns: Readonly<{
filter?: {
kind: string;
};
+ entityPresentation?: EntityPresentationApi;
}): EntityColumnConfig;
createOwnerColumn(): EntityColumnConfig;
createSystemColumn(): EntityColumnConfig;
@@ -520,6 +534,18 @@ export interface EntityDataTableProps {
loading?: boolean;
}
+// @public
+export const EntityDisplayName: (props: EntityDisplayNameProps) => JSX.Element;
+
+// @public
+export type EntityDisplayNameProps = {
+ entityRef: Entity | CompoundEntityRef | string;
+ hideIcon?: boolean;
+ disableTooltip?: boolean;
+ defaultKind?: string;
+ defaultNamespace?: string;
+};
+
// @alpha (undocumented)
export const EntityHeaderBlueprint: ExtensionBlueprint<{
kind: 'entity-header';
@@ -627,6 +653,32 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{
};
}>;
+// @public
+export interface EntityPresentationApi {
+ forEntity(
+ entityOrRef: Entity | string,
+ context?: {
+ defaultKind?: string;
+ defaultNamespace?: string;
+ },
+ ): EntityRefPresentation;
+}
+
+// @public
+export interface EntityRefPresentation {
+ promise: Promise;
+ snapshot: EntityRefPresentationSnapshot;
+ update$?: Observable;
+}
+
+// @public
+export interface EntityRefPresentationSnapshot {
+ entityRef: string;
+ Icon?: IconComponent | undefined | false;
+ primaryTitle: string;
+ secondaryTitle?: string;
+}
+
// @public (undocumented)
export function EntityRelationCard(
props: EntityRelationCardProps,
@@ -700,6 +752,15 @@ export function useEntityPermission(
error?: Error;
};
+// @public
+export function useEntityPresentation(
+ entityOrRef: Entity | CompoundEntityRef | string,
+ context?: {
+ defaultKind?: string;
+ defaultNamespace?: string;
+ },
+): EntityRefPresentationSnapshot;
+
// @alpha (undocumented)
export type UseProps = () =>
| {
diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md
index bb9d43edb0..806d329fe0 100644
--- a/plugins/catalog-react/report.api.md
+++ b/plugins/catalog-react/report.api.md
@@ -266,6 +266,7 @@ export type CatalogReactUserListPickerClassKey =
export const columnFactories: Readonly<{
createEntityRefColumn(options: {
defaultKind?: string;
+ entityPresentation?: EntityPresentationApi;
}): TableColumn;
createEntityRelationColumn(options: {
title: string | JSX.Element;
@@ -274,6 +275,7 @@ export const columnFactories: Readonly<{
filter?: {
kind: string;
};
+ entityPresentation?: EntityPresentationApi;
}): TableColumn;
createOwnerColumn(): TableColumn;
createDomainColumn(): TableColumn;
@@ -687,6 +689,7 @@ export const EntityTable: {
columns: Readonly<{
createEntityRefColumn(options: {
defaultKind?: string;
+ entityPresentation?: EntityPresentationApi;
}): TableColumn;
createEntityRelationColumn(options: {
title: string | JSX.Element;
@@ -695,6 +698,7 @@ export const EntityTable: {
filter?: {
kind: string;
};
+ entityPresentation?: EntityPresentationApi;
}): TableColumn;
createOwnerColumn(): TableColumn;
createDomainColumn(): TableColumn;
diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts
index d473b0923f..47418eaa2a 100644
--- a/plugins/catalog-react/src/alpha/index.ts
+++ b/plugins/catalog-react/src/alpha/index.ts
@@ -26,5 +26,13 @@ export const catalogReactTranslationRef = _catalogReactTranslationRef;
export { isOwnerOf } from '../utils/isOwnerOf';
export { useEntityPermission } from '../hooks/useEntityPermission';
export * from '../components/EntityTable/TitleColumn';
+export type {
+ EntityPresentationApi,
+ EntityRefPresentation,
+ EntityRefPresentationSnapshot,
+} from '../apis';
+export { useEntityPresentation, defaultEntityPresentation } from '../apis';
+export { EntityDisplayName } from '../components/EntityDisplayName';
+export type { EntityDisplayNameProps } from '../components/EntityDisplayName';
export * from '../components/EntityDataTable';
export * from '../components/EntityRelationCard';
diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts
index e293d1e0ab..316bc9aff5 100644
--- a/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts
+++ b/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts
@@ -36,9 +36,9 @@ import { Observable } from '@backstage/types';
* which wraps the hook and renders a styled entity name with optional icon
* and tooltip.
*
- * - In non-React contexts such as sort comparators, filter functions, or data
- * mappers, use the {@link defaultEntityPresentation} function which
- * synchronously extracts a display name from an already-loaded entity.
+ * - 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.
*
* @public
*/
@@ -143,8 +143,9 @@ export interface EntityRefPresentation {
* - {@link EntityDisplayName} — React component that renders an entity name
* with optional icon and tooltip.
*
- * - {@link defaultEntityPresentation} — synchronous helper for non-React
- * contexts where you already have the entity object.
+ * For non-React contexts, you can use the API directly via
+ * `forEntity().snapshot` for synchronous access, or `forEntity().promise`
+ * for async contexts.
*
* Implement this interface to customize how entities are displayed throughout
* the Backstage interface.
diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts
index 9c1cbd5325..1984593cfa 100644
--- a/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts
+++ b/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts
@@ -33,11 +33,12 @@ import { EntityRefPresentationSnapshot } from './EntityPresentationApi';
* first available value among `spec.profile.displayName`, `metadata.title`,
* and a shortened entity ref string.
*
- * Use this in non-React contexts where hooks are not available, such as sort
- * comparators, filter functions, table column factories, and data mappers.
- * In React components, prefer the {@link useEntityPresentation} hook or the
- * {@link EntityDisplayName} component, which support async enrichment via
- * the {@link 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.
*
* @public
* @param entityOrRef - Either an entity, or a ref to it.
diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts
index 2e01485dcc..125557ceb7 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
- * {@link defaultEntityPresentation} directly.
+ * the {@link EntityPresentationApi} directly via `forEntity().snapshot`.
*
* @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 647e7c7f3b..aed29dbd56 100644
--- a/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx
+++ b/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx
@@ -21,7 +21,7 @@ import {
} from '@backstage/catalog-model';
import { Cell, CellText, Column, ColumnConfig, TableItem } from '@backstage/ui';
import { EntityRefLink, EntityRefLinks } from '../EntityRefLink';
-import { defaultEntityPresentation } from '../../apis';
+import { defaultEntityPresentation, EntityPresentationApi } from '../../apis';
import { EntityTableColumnTitle } from '../EntityTable/TitleColumn';
import { getEntityRelations } from '../../utils';
@@ -33,11 +33,24 @@ export interface EntityColumnConfig extends ColumnConfig {
sortValue?: (entity: EntityRow) => string;
}
+function getEntityTitle(
+ entityOrRef: Entity | { kind: string; namespace?: string; name: string },
+ context: { defaultKind?: string },
+ entityPresentation?: EntityPresentationApi,
+): string {
+ if (entityPresentation) {
+ return entityPresentation.forEntity(entityOrRef as Entity, context).snapshot
+ .primaryTitle;
+ }
+ return defaultEntityPresentation(entityOrRef as Entity, context).primaryTitle;
+}
+
/** @public */
export const columnFactories = Object.freeze({
createEntityRefColumn(options: {
defaultKind?: string;
isRowHeader?: boolean;
+ entityPresentation?: EntityPresentationApi;
}): EntityColumnConfig {
const isRowHeader = options.isRowHeader ?? true;
return {
@@ -60,8 +73,11 @@ export const columnFactories = Object.freeze({
),
sortValue: entity =>
- defaultEntityPresentation(entity, { defaultKind: options.defaultKind })
- .primaryTitle,
+ getEntityTitle(
+ entity,
+ { defaultKind: options.defaultKind },
+ options.entityPresentation,
+ ),
};
},
@@ -71,6 +87,7 @@ export const columnFactories = Object.freeze({
relation: string;
defaultKind?: string;
filter?: { kind: string };
+ entityPresentation?: EntityPresentationApi;
}): EntityColumnConfig {
return {
id: options.id,
@@ -95,11 +112,12 @@ export const columnFactories = Object.freeze({
),
sortValue: entity =>
getEntityRelations(entity, options.relation, options.filter)
- .map(
- r =>
- defaultEntityPresentation(r, {
- defaultKind: options.defaultKind,
- }).primaryTitle,
+ .map(r =>
+ getEntityTitle(
+ r,
+ { defaultKind: options.defaultKind },
+ options.entityPresentation,
+ ),
)
.join(', '),
};
diff --git a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx
index 6ed863f180..a6b4ed94fe 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
- * {@link defaultEntityPresentation}.
+ * the {@link EntityPresentationApi} directly via `forEntity().snapshot`.
*
* @public
*/
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
index 16e00e5ccd..1375140ebc 100644
--- a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
+++ b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
@@ -33,11 +33,15 @@ import { EntityOwnerFilter } from '../../filters';
import { useDebouncedEffect } from '@react-hookz/web';
import PersonIcon from '@material-ui/icons/Person';
import GroupIcon from '@material-ui/icons/Group';
-import { defaultEntityPresentation } from '../../apis';
+import {
+ defaultEntityPresentation,
+ entityPresentationApiRef,
+} from '../../apis';
import { useFetchEntities } from './useFetchEntities';
import { withStyles } from '@material-ui/core/styles';
import { useEntityPresentation } from '../../apis';
import { catalogReactTranslationRef } from '../../translation';
+import { useApiHolder } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { CatalogAutocomplete } from '../CatalogAutocomplete';
@@ -124,6 +128,8 @@ function RenderOptionLabel(props: { entity: Entity; isSelected: boolean }) {
export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => {
const classes = useStyles();
const { mode = 'owners-only' } = props || {};
+ const apis = useApiHolder();
+ const entityPresentationApi = apis.get(entityPresentationApiRef);
const {
updateFilters,
filters,
@@ -203,6 +209,10 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => {
defaultNamespace: 'default',
})
: o;
+ if (entityPresentationApi) {
+ return entityPresentationApi.forEntity(entity as Entity).snapshot
+ .primaryTitle;
+ }
return defaultEntityPresentation(entity).primaryTitle;
}}
onChange={(_: object, owners) => {
diff --git a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
index ae1fb5e685..80bd290806 100644
--- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
+++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
@@ -25,9 +25,9 @@ import get from 'lodash/get';
* @param defaultNamespace - if set to false then namespace is never omitted,
* if set to string which matches namespace of entity then omitted
*
- * @deprecated Use {@link defaultEntityPresentation} for non-React contexts,
- * or {@link useEntityPresentation} / {@link EntityDisplayName} in React
- * components. These provide richer display names using `metadata.title` and
+ * @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.
*
* @public
@@ -81,8 +81,8 @@ export function humanizeEntityRef(
*
* If neither of those are found or populated, fallback to `defaultName`.
*
- * @deprecated Use {@link defaultEntityPresentation} instead, which provides
- * the same resolution logic via `primaryTitle`.
+ * @deprecated Use {@link useEntityPresentation} or {@link EntityDisplayName}
+ * in React components, or access the {@link entityPresentationApiRef} directly.
*
* @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 6b47fa02ff..3768cf595e 100644
--- a/plugins/catalog-react/src/components/EntityTable/columns.tsx
+++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx
@@ -23,17 +23,30 @@ import {
import { OverflowTooltip, TableColumn } from '@backstage/core-components';
import { getEntityRelations } from '../../utils';
import { EntityRefLink, EntityRefLinks } from '../EntityRefLink';
-import { defaultEntityPresentation } from '../../apis';
+import { defaultEntityPresentation, EntityPresentationApi } from '../../apis';
import { EntityTableColumnTitle } from './TitleColumn';
+function getEntityTitle(
+ entityOrRef: Entity | CompoundEntityRef,
+ context: { defaultKind?: string },
+ entityPresentation?: EntityPresentationApi,
+): string {
+ if (entityPresentation) {
+ return entityPresentation.forEntity(entityOrRef as Entity, context).snapshot
+ .primaryTitle;
+ }
+ return defaultEntityPresentation(entityOrRef, context).primaryTitle;
+}
+
/** @public */
export const columnFactories = Object.freeze({
createEntityRefColumn(options: {
defaultKind?: string;
+ entityPresentation?: EntityPresentationApi;
}): TableColumn {
- const { defaultKind } = options;
+ const { defaultKind, entityPresentation } = options;
function formatContent(entity: T): string {
- return defaultEntityPresentation(entity, { defaultKind }).primaryTitle;
+ return getEntityTitle(entity, { defaultKind }, entityPresentation);
}
return {
@@ -67,8 +80,15 @@ export const columnFactories = Object.freeze({
relation: string;
defaultKind?: string;
filter?: { kind: string };
+ entityPresentation?: EntityPresentationApi;
}): TableColumn {
- const { title, relation, defaultKind, filter: entityFilter } = options;
+ const {
+ title,
+ relation,
+ defaultKind,
+ filter: entityFilter,
+ entityPresentation,
+ } = options;
function getRelations(entity: T): CompoundEntityRef[] {
return getEntityRelations(entity, relation, entityFilter);
@@ -76,7 +96,7 @@ export const columnFactories = Object.freeze({
function formatContent(entity: T): string {
return getRelations(entity)
- .map(r => defaultEntityPresentation(r, { defaultKind }).primaryTitle)
+ .map(r => getEntityTitle(r, { defaultKind }, entityPresentation))
.join(', ');
}
diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md
index 84d9a8bd91..816d469d13 100644
--- a/plugins/catalog/report.api.md
+++ b/plugins/catalog/report.api.md
@@ -153,6 +153,7 @@ export const CatalogTable: {
columns: Readonly<{
createNameColumn(options?: {
defaultKind?: string;
+ entityPresentation?: EntityPresentationApi;
}): TableColumn;
createSystemColumn(): TableColumn;
createOwnerColumn(): TableColumn;
diff --git a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
index 200505f5be..a57a5f3d31 100644
--- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
@@ -16,6 +16,7 @@
import {
ANNOTATION_EDIT_URL,
ANNOTATION_VIEW_URL,
+ CompoundEntityRef,
Entity,
RELATION_OWNED_BY,
RELATION_PART_OF,
@@ -30,9 +31,11 @@ import {
} from '@backstage/core-components';
import {
defaultEntityPresentation,
+ entityPresentationApiRef,
getEntityRelations,
useEntityList,
useStarredEntities,
+ type EntityPresentationApi,
} from '@backstage/plugin-catalog-react';
import CircularProgress from '@material-ui/core/CircularProgress';
import Typography from '@material-ui/core/Typography';
@@ -47,6 +50,7 @@ import { CatalogTableColumnsFunc, CatalogTableRow } from './types';
import { OffsetPaginatedCatalogTable } from './OffsetPaginatedCatalogTable';
import { CursorPaginatedCatalogTable } from './CursorPaginatedCatalogTable';
import { defaultCatalogTableColumnsFunc } from './defaultCatalogTableColumnsFunc';
+import { useApiHolder } from '@backstage/core-plugin-api';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
import { catalogTranslationRef } from '../../alpha';
import { FavoriteToggleIcon } from '@backstage/core-components';
@@ -69,12 +73,23 @@ export interface CatalogTableProps {
subtitle?: string;
}
-const refCompare = (a: Entity, b: Entity) => {
- const toRef = (entity: Entity) =>
- defaultEntityPresentation(entity, { defaultKind: 'Component' })
- .primaryTitle;
+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;
+}
- return toRef(a).localeCompare(toRef(b));
+const refCompare = (a: Entity, b: Entity, api?: EntityPresentationApi) => {
+ return getTitle(a, { defaultKind: 'Component' }, api).localeCompare(
+ getTitle(b, { defaultKind: 'Component' }, api),
+ );
};
/**
@@ -95,6 +110,8 @@ export const CatalogTable = (props: CatalogTableProps) => {
emptyContent,
} = props;
const { isStarredEntity, toggleStarredEntity } = useStarredEntities();
+ const apis = useApiHolder();
+ const entityPresentationApi = apis.get(entityPresentationApiRef);
const entityListContext = useEntityList();
const {
@@ -232,7 +249,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
actions={actions}
subtitle={subtitle}
options={options}
- data={entities.map(toEntityRow)}
+ data={entities.map(e => toEntityRow(e, entityPresentationApi))}
next={pageInfo?.next}
prev={pageInfo?.prev}
/>
@@ -247,12 +264,14 @@ export const CatalogTable = (props: CatalogTableProps) => {
actions={actions}
subtitle={subtitle}
options={options}
- data={entities.map(toEntityRow)}
+ data={entities.map(e => toEntityRow(e, entityPresentationApi))}
/>
);
}
- const rows = entities.sort(refCompare).map(toEntityRow);
+ const rows = entities
+ .sort((a, b) => refCompare(a, b, entityPresentationApi))
+ .map(e => toEntityRow(e, entityPresentationApi));
const pageSize = 20;
const showPagination = rows.length > pageSize;
@@ -278,7 +297,7 @@ export const CatalogTable = (props: CatalogTableProps) => {
CatalogTable.columns = columnFactories;
CatalogTable.defaultColumnsFunc = defaultCatalogTableColumnsFunc;
-function toEntityRow(entity: Entity) {
+function toEntityRow(entity: Entity, api?: EntityPresentationApi) {
const partOfSystemRelations = getEntityRelations(entity, RELATION_PART_OF, {
kind: 'system',
});
@@ -290,22 +309,14 @@ function toEntityRow(entity: Entity) {
// 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: defaultEntityPresentation(entity, { defaultKind: 'Component' })
- .primaryTitle,
+ name: getTitle(entity, { defaultKind: 'Component' }, api),
entityRef: stringifyEntityRef(entity),
ownedByRelationsTitle: ownedByRelations
- .map(
- r =>
- defaultEntityPresentation(r, { defaultKind: 'group' }).primaryTitle,
- )
+ .map(r => getTitle(r, { defaultKind: 'group' }, api))
.join(', '),
ownedByRelations,
partOfSystemRelationTitle: partOfSystemRelations
- .map(
- r =>
- defaultEntityPresentation(r, { defaultKind: 'system' })
- .primaryTitle,
- )
+ .map(r => getTitle(r, { defaultKind: 'system' }, api))
.join(', '),
partOfSystemRelations,
},
diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx
index 8a18b301e6..05a8b1979b 100644
--- a/plugins/catalog/src/components/CatalogTable/columns.tsx
+++ b/plugins/catalog/src/components/CatalogTable/columns.tsx
@@ -17,6 +17,7 @@ import {
defaultEntityPresentation,
EntityRefLink,
EntityRefLinks,
+ type EntityPresentationApi,
} from '@backstage/plugin-catalog-react';
import Chip from '@material-ui/core/Chip';
import { CatalogTableRow } from './types';
@@ -31,8 +32,14 @@ import { EntityTableColumnTitle } from '@backstage/plugin-catalog-react/alpha';
export const columnFactories = Object.freeze({
createNameColumn(options?: {
defaultKind?: string;
+ entityPresentation?: EntityPresentationApi;
}): TableColumn {
function formatContent(entity: Entity): string {
+ if (options?.entityPresentation) {
+ return options.entityPresentation.forEntity(entity, {
+ defaultKind: options?.defaultKind,
+ }).snapshot.primaryTitle;
+ }
return defaultEntityPresentation(entity, {
defaultKind: options?.defaultKind,
}).primaryTitle;
diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx
index b836d30aff..b193d4e092 100644
--- a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx
+++ b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx
@@ -18,12 +18,13 @@ import { MouseEvent, useState, useCallback } from 'react';
import {
catalogApiRef,
defaultEntityPresentation,
+ entityPresentationApiRef,
} from '@backstage/plugin-catalog-react';
import TextField from '@material-ui/core/TextField';
import Autocomplete from '@material-ui/lab/Autocomplete';
import useAsync from 'react-use/esm/useAsync';
import Popover from '@material-ui/core/Popover';
-import { useApi } from '@backstage/core-plugin-api';
+import { useApi, useApiHolder } from '@backstage/core-plugin-api';
import { ResponseErrorPanel } from '@backstage/core-components';
import { GroupEntity } from '@backstage/catalog-model';
import { GroupListPickerButton } from './GroupListPickerButton';
@@ -43,6 +44,8 @@ export type GroupListPickerProps = {
/** @public */
export const GroupListPicker = (props: GroupListPickerProps) => {
const catalogApi = useApi(catalogApiRef);
+ const apis = useApiHolder();
+ const entityPresentationApi = apis.get(entityPresentationApiRef);
const { onChange, groupTypes, placeholder = '', defaultValue = '' } = props;
const [anchorEl, setAnchorEl] = useState(null);
@@ -99,7 +102,9 @@ export const GroupListPicker = (props: GroupListPickerProps) => {
options={groups ?? []}
groupBy={option => option.spec.type}
getOptionLabel={option =>
- defaultEntityPresentation(option).primaryTitle
+ entityPresentationApi
+ ? entityPresentationApi.forEntity(option).snapshot.primaryTitle
+ : defaultEntityPresentation(option).primaryTitle
}
inputValue={inputValue}
onInputChange={(_, value) => setInputValue(value)}
diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.test.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.test.tsx
index 1174d84b11..1fd7544ea4 100644
--- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.test.tsx
+++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPage.test.tsx
@@ -18,14 +18,34 @@ import { screen } from '@testing-library/react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { TemplateFormPage } from './TemplateFormPage';
import { rootRouteRef } from '../../../routes';
-import { catalogApiRef } from '@backstage/plugin-catalog-react';
+import {
+ catalogApiRef,
+ defaultEntityPresentation,
+ entityPresentationApiRef,
+} from '@backstage/plugin-catalog-react';
describe('TemplateFormPage', () => {
const catalogApiMock = { getEntities: jest.fn().mockResolvedValue([]) };
+ const entityPresentationApi: typeof entityPresentationApiRef.T = {
+ forEntity(entityOrRef, context) {
+ const presentation = defaultEntityPresentation(entityOrRef, context);
+ return {
+ snapshot: presentation,
+ update$: { subscribe: () => ({ unsubscribe: () => {} }) } as any,
+ promise: Promise.resolve(presentation),
+ };
+ },
+ };
+
it('Should render without exploding', async () => {
await renderInTestApp(
-
+
,
{
@@ -41,7 +61,12 @@ describe('TemplateFormPage', () => {
it('Should have an link back to the edit page', async () => {
await renderInTestApp(
-
+
,
{
diff --git a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx
index d2db8c4a62..b436f56427 100644
--- a/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx
+++ b/plugins/scaffolder/src/alpha/components/TemplateEditorPage/TemplateFormPreviewer.tsx
@@ -24,7 +24,7 @@ import { makeStyles } from '@material-ui/core/styles';
import { alertApiRef, useApi, useRouteRef } from '@backstage/core-plugin-api';
import {
catalogApiRef,
- defaultEntityPresentation,
+ entityPresentationApiRef,
} from '@backstage/plugin-catalog-react';
import {
LayoutOptions,
@@ -139,6 +139,7 @@ export const TemplateFormPreviewer = ({
const classes = useStyles();
const alertApi = useApi(alertApiRef);
const catalogApi = useApi(catalogApiRef);
+ const entityPresentationApi = useApi(entityPresentationApiRef);
const navigate = useNavigate();
const editLink = useRouteRef(editRouteRef);
@@ -166,16 +167,19 @@ export const TemplateFormPreviewer = ({
'spec.output',
],
})
- .then(({ items }) =>
- setTemplateOptions(
- items.map(template => ({
- label: defaultEntityPresentation(template, {
- defaultKind: 'template',
- }).primaryTitle,
+ .then(async ({ items }) => {
+ const options = await Promise.all(
+ items.map(async template => ({
+ label: (
+ await entityPresentationApi.forEntity(template, {
+ defaultKind: 'template',
+ }).promise
+ ).primaryTitle,
value: template,
})),
- ),
- )
+ );
+ setTemplateOptions(options);
+ })
.catch(e =>
alertApi.post({
message: `Error loading existing templates: ${e.message}`,
diff --git a/plugins/techdocs/src/home/components/Tables/helpers.ts b/plugins/techdocs/src/home/components/Tables/helpers.ts
index 5c972b0233..4ec3123fe8 100644
--- a/plugins/techdocs/src/home/components/Tables/helpers.ts
+++ b/plugins/techdocs/src/home/components/Tables/helpers.ts
@@ -14,10 +14,15 @@
* 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,
getEntityRelations,
+ type EntityPresentationApi,
} from '@backstage/plugin-catalog-react';
import { toLowerMaybe } from '../../../helpers';
import { ConfigApi, RouteFunc } from '@backstage/core-plugin-api';
@@ -32,6 +37,7 @@ export function entitiesToDocsMapper(
entities: Entity[],
getRouteToReaderPageFor: getRouteFunc,
config: ConfigApi,
+ entityPresentation?: EntityPresentationApi,
) {
return entities.map(entity => {
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
@@ -48,11 +54,15 @@ export function entitiesToDocsMapper(
}),
ownedByRelations,
ownedByRelationsTitle: ownedByRelations
- .map(
- r =>
- defaultEntityPresentation(r, { defaultKind: 'group' })
- .primaryTitle,
- )
+ .map(r => {
+ if (entityPresentation) {
+ return entityPresentation.forEntity(stringifyEntityRef(r), {
+ defaultKind: 'group',
+ }).snapshot.primaryTitle;
+ }
+ return defaultEntityPresentation(r, { defaultKind: 'group' })
+ .primaryTitle;
+ })
.join(', '),
},
};