diff --git a/.changeset/replace-humanize-entity-ref-catalog-react.md b/.changeset/replace-humanize-entity-ref-catalog-react.md
new file mode 100644
index 0000000000..7f44b101c4
--- /dev/null
+++ b/.changeset/replace-humanize-entity-ref-catalog-react.md
@@ -0,0 +1,5 @@
+---
+'@backstage/plugin-catalog-react': patch
+---
+
+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-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/docs/features/software-catalog/entity-presentation.md b/docs/features/software-catalog/entity-presentation.md
new file mode 100644
index 0000000000..a883f93339
--- /dev/null
+++ b/docs/features/software-catalog/entity-presentation.md
@@ -0,0 +1,147 @@
+---
+id: entity-presentation
+title: Entity Presentation
+description: How to display entity names and control how entities are represented in the Backstage interface
+---
+
+The _Entity Presentation API_ controls how catalog entities are displayed
+throughout the Backstage interface. Instead of rendering raw entity refs like
+`component:default/my-service`, the API resolves a human-friendly display
+name from fields such as `metadata.title` and `spec.profile.displayName`.
+
+## Displaying entity names
+
+There are several ways to display entity names, depending on context:
+
+### `EntityDisplayName` component
+
+The simplest option for React components. Renders a styled entity name with
+an optional icon and tooltip:
+
+```tsx
+import { EntityDisplayName } from '@backstage/plugin-catalog-react';
+
+;
+```
+
+You can pass an entity ref string, an `Entity` object, or a
+`CompoundEntityRef`. The component supports optional `hideIcon` and
+`disableTooltip` props.
+
+### `useEntityPresentation` hook
+
+Use this hook when you need access to the raw presentation data in a React
+component, for example to render the title in a custom layout:
+
+```tsx
+import { useEntityPresentation } from '@backstage/plugin-catalog-react';
+
+function MyComponent({ entityRef }: { entityRef: string }) {
+ const { primaryTitle, secondaryTitle, Icon } =
+ useEntityPresentation(entityRef);
+
+ return (
+
+ {Icon && }
+ {primaryTitle}
+
+ );
+}
+```
+
+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 (async, preferred for non-React)
+
+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, {
+ defaultKind: 'group',
+}).promise;
+const title = presentation.primaryTitle;
+```
+
+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
+
+To customize how entities are rendered, provide your own implementation of
+the `EntityPresentationApi` interface and register it with the app's API
+factory:
+
+```ts
+import {
+ entityPresentationApiRef,
+ type EntityPresentationApi,
+} from '@backstage/plugin-catalog-react';
+import { createApiFactory } from '@backstage/core-plugin-api';
+
+const myPresentationApi: EntityPresentationApi = {
+ forEntity(entityOrRef, context) {
+ // Return an EntityRefPresentation with snapshot, update$, and promise
+ },
+};
+
+createApiFactory({
+ api: entityPresentationApiRef,
+ deps: {},
+ factory: () => myPresentationApi,
+});
+```
+
+The presentation snapshot includes `primaryTitle`, an optional
+`secondaryTitle` for tooltips, and an optional `Icon` component. You can
+also emit updated snapshots over time via the `update$` observable.
+
+## Migrating from `humanizeEntityRef`
+
+The `humanizeEntityRef` and `humanizeEntity` functions are deprecated. They
+only produce a shortened entity ref string and do not resolve display names
+from `metadata.title` or `spec.profile.displayName`.
+
+Replace them as follows:
+
+| Old code | Replacement |
+| :---------------------------------------------------- | :--------------------------------------------------------------------- |
+| `humanizeEntityRef(entity)` in JSX | `` |
+| `humanizeEntityRef(entity)` in a React component | `useEntityPresentation(entity).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/microsite/sidebars.ts b/microsite/sidebars.ts
index f93731a427..df39e911ff 100644
--- a/microsite/sidebars.ts
+++ b/microsite/sidebars.ts
@@ -271,6 +271,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/mkdocs.yml b/mkdocs.yml
index 4b32556c96..5f02cfeaa2 100644
--- a/mkdocs.yml
+++ b/mkdocs.yml
@@ -62,6 +62,7 @@ nav:
- Extending the model: 'features/software-catalog/extending-the-model.md'
- External integrations: 'features/software-catalog/external-integrations.md'
- Catalog Customization: 'features/software-catalog/catalog-customization.md'
+ - Entity Presentation: 'features/software-catalog/entity-presentation.md'
- API: 'features/software-catalog/api.md'
- FAQ: 'features/software-catalog/faq.md'
- Kubernetes:
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 40b639f14a..c03696b7a5 100644
--- a/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx
+++ b/plugins/catalog-import/src/components/StepPrepareCreatePullRequest/StepPrepareCreatePullRequest.tsx
@@ -20,7 +20,7 @@ import { toError } from '@backstage/errors';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import {
catalogApiRef,
- humanizeEntityRef,
+ 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,9 +162,13 @@ export const StepPrepareCreatePullRequest = (
filter: { kind: 'group' },
});
- return groupEntities.items
- .map(e => humanizeEntityRef(e, { defaultKind: 'group' }))
- .sort();
+ const presentations = await Promise.all(
+ groupEntities.items.map(
+ e =>
+ 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..19f1b2ce3a 100644
--- a/plugins/catalog-react/report-alpha.api.md
+++ b/plugins/catalog-react/report-alpha.api.md
@@ -8,6 +8,7 @@ import { ColumnConfig } from '@backstage/ui';
import { ComponentType } from 'react';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
+import { EntityPresentationApi } from '@backstage/plugin-catalog-react';
import { ExtensionBlueprint } from '@backstage/frontend-plugin-api';
import { ExtensionDataRef } from '@backstage/frontend-plugin-api';
import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
@@ -488,6 +489,7 @@ export const entityDataTableColumns: Readonly<{
createEntityRefColumn(options: {
defaultKind?: string;
isRowHeader?: boolean;
+ entityPresentationApi?: EntityPresentationApi;
}): EntityColumnConfig;
createEntityRelationColumn(options: {
id: string;
@@ -497,6 +499,7 @@ export const entityDataTableColumns: Readonly<{
filter?: {
kind: string;
};
+ entityPresentationApi?: EntityPresentationApi;
}): EntityColumnConfig;
createOwnerColumn(): EntityColumnConfig;
createSystemColumn(): EntityColumnConfig;
diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md
index f7f195c226..419a3c3f94 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;
+ entityPresentationApi?: EntityPresentationApi;
}): TableColumn;
createEntityRelationColumn(options: {
title: string | JSX.Element;
@@ -274,6 +275,7 @@ export const columnFactories: Readonly<{
filter?: {
kind: string;
};
+ entityPresentationApi?: EntityPresentationApi;
}): TableColumn;
createOwnerColumn(): TableColumn;
createDomainColumn(): TableColumn;
@@ -591,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;
@@ -687,6 +699,7 @@ export const EntityTable: {
columns: Readonly<{
createEntityRefColumn(options: {
defaultKind?: string;
+ entityPresentationApi?: EntityPresentationApi;
}): TableColumn;
createEntityRelationColumn(options: {
title: string | JSX.Element;
@@ -695,6 +708,7 @@ export const EntityTable: {
filter?: {
kind: string;
};
+ entityPresentationApi?: EntityPresentationApi;
}): TableColumn;
createOwnerColumn(): TableColumn;
createDomainColumn(): TableColumn;
@@ -834,7 +848,7 @@ export function getEntitySourceLocation(
scmIntegrationsApi: typeof scmIntegrationsApiRef.T,
): EntitySourceLocation | undefined;
-// @public (undocumented)
+// @public @deprecated (undocumented)
export function humanizeEntityRef(
entityRef: Entity | CompoundEntityRef,
opts?: {
diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts
index 642dd6dbac..99ae0ceacc 100644
--- a/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts
+++ b/plugins/catalog-react/src/apis/EntityPresentationApi/EntityPresentationApi.ts
@@ -25,6 +25,21 @@ import { Observable } from '@backstage/types';
/**
* An API that handles how to represent entities in the interface.
*
+ * @remarks
+ *
+ * There are several ways to consume this API depending on context:
+ *
+ * - In React components, use the {@link useEntityPresentation} hook to get a
+ * reactive presentation snapshot that updates over time.
+ *
+ * - For simple inline rendering, use the {@link EntityDisplayName} component
+ * which wraps the hook and renders a styled entity name with optional icon
+ * and tooltip.
+ *
+ * - In non-React contexts such as sort comparators or data mappers, use the
+ * {@link entityPresentationSnapshot} helper for synchronous access, or
+ * the API directly via `forEntity().promise` in async loaders.
+ *
* @public
*/
export const entityPresentationApiRef: ApiRef =
@@ -120,8 +135,20 @@ export interface EntityRefPresentation {
*
* @remarks
*
- * Most consumers will want to use the {@link useEntityPresentation} hook
- * instead of this interface directly.
+ * Most consumers will not need to interact with this interface directly.
+ * Instead, use one of the following:
+ *
+ * - {@link useEntityPresentation} — React hook for reactive presentation data.
+ *
+ * - {@link EntityDisplayName} — React component that renders an entity name
+ * with optional icon and tooltip.
+ *
+ * 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
+ * the Backstage interface.
*
* @public
*/
diff --git a/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts b/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts
index b546f9aa93..98f200c67a 100644
--- a/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts
+++ b/plugins/catalog-react/src/apis/EntityPresentationApi/defaultEntityPresentation.ts
@@ -24,7 +24,21 @@ import get from 'lodash/get';
import { EntityRefPresentationSnapshot } from './EntityPresentationApi';
/**
- * This returns the default representation of an entity.
+ * Returns the default representation of an entity.
+ *
+ * @remarks
+ *
+ * This is a synchronous helper that extracts a display name from an
+ * already-loaded entity or entity ref. It resolves `primaryTitle` as the
+ * first available value among `spec.profile.displayName`, `metadata.title`,
+ * and a shortened entity ref string.
+ *
+ * This function is primarily used as the internal fallback within the
+ * {@link EntityPresentationApi} when no custom implementation is registered.
+ * In React components, use the {@link useEntityPresentation} hook or the
+ * {@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 e397558ed0..68694f167a 100644
--- a/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts
+++ b/plugins/catalog-react/src/apis/EntityPresentationApi/useEntityPresentation.ts
@@ -32,6 +32,19 @@ import { useUpdatingObservable } from './useUpdatingObservable';
/**
* Returns information about how to represent an entity in the interface.
*
+ * @remarks
+ *
+ * This hook subscribes to the {@link EntityPresentationApi} and returns a
+ * snapshot that may update over time as richer data is fetched (for example,
+ * resolving `metadata.title` from a string entity ref). If no presentation
+ * API is registered, it falls back to {@link defaultEntityPresentation}.
+ *
+ * For simple inline rendering, consider using the {@link EntityDisplayName}
+ * component instead, which wraps this hook with icon and tooltip support.
+ *
+ * For non-React contexts such as sort comparators or data mappers, use
+ * {@link entityPresentationSnapshot}.
+ *
* @public
* @param entityOrRef - The entity to represent, or an entity ref to it. If you
* pass in an entity, it is assumed that it is NOT a partial one - i.e. only
diff --git a/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx b/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx
index 49fb64caa8..80a4cc8070 100644
--- a/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx
+++ b/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx
@@ -20,11 +20,11 @@ import {
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Cell, CellText, Column, ColumnConfig, TableItem } from '@backstage/ui';
+import { EntityRefLink, EntityRefLinks } from '../EntityRefLink';
import {
- EntityRefLink,
- EntityRefLinks,
- humanizeEntityRef,
-} from '../EntityRefLink';
+ entityPresentationSnapshot,
+ EntityPresentationApi,
+} from '@backstage/plugin-catalog-react';
import { EntityTableColumnTitle } from '../EntityTable/TitleColumn';
import { getEntityRelations } from '../../utils';
@@ -41,6 +41,7 @@ export const columnFactories = Object.freeze({
createEntityRefColumn(options: {
defaultKind?: string;
isRowHeader?: boolean;
+ entityPresentationApi?: EntityPresentationApi;
}): EntityColumnConfig {
const isRowHeader = options.isRowHeader ?? true;
return {
@@ -63,8 +64,11 @@ export const columnFactories = Object.freeze({
),
sortValue: entity =>
- entity.metadata?.title ||
- humanizeEntityRef(entity, { defaultKind: options.defaultKind }),
+ entityPresentationSnapshot(
+ entity,
+ { defaultKind: options.defaultKind },
+ options.entityPresentationApi,
+ ).primaryTitle,
};
},
@@ -74,6 +78,7 @@ export const columnFactories = Object.freeze({
relation: string;
defaultKind?: string;
filter?: { kind: string };
+ entityPresentationApi?: EntityPresentationApi;
}): EntityColumnConfig {
return {
id: options.id,
@@ -98,7 +103,14 @@ export const columnFactories = Object.freeze({
),
sortValue: entity =>
getEntityRelations(entity, options.relation, options.filter)
- .map(r => humanizeEntityRef(r, { defaultKind: options.defaultKind }))
+ .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 a4134df465..1f60333acd 100644
--- a/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx
+++ b/plugins/catalog-react/src/components/EntityDisplayName/EntityDisplayName.tsx
@@ -62,6 +62,16 @@ export type EntityDisplayNameProps = {
/**
* Shows a nice representation of a reference to an entity.
*
+ * @remarks
+ *
+ * This component uses the {@link useEntityPresentation} hook internally and
+ * renders the entity's primary title with optional icon and tooltip. It is
+ * the simplest way to display an entity name in JSX.
+ *
+ * For more control over the presentation data, use the
+ * {@link useEntityPresentation} hook directly. For non-React contexts, use
+ * {@link entityPresentationSnapshot}.
+ *
* @public
*/
export const EntityDisplayName = (
diff --git a/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx b/plugins/catalog-react/src/components/EntityOwnerPicker/EntityOwnerPicker.tsx
index ed200c77c4..291fcf5376 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 { humanizeEntity, humanizeEntityRef } from '../EntityRefLink/humanize';
+import {
+ entityPresentationApiRef,
+ entityPresentationSnapshot,
+} 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,7 +209,11 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => {
defaultNamespace: 'default',
})
: o;
- return humanizeEntity(entity, humanizeEntityRef(entity));
+ 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 478f0dce9d..fe697365b6 100644
--- a/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
+++ b/plugins/catalog-react/src/components/EntityRefLink/humanize.ts
@@ -25,6 +25,12 @@ 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 useEntityPresentation} or {@link EntityDisplayName}
+ * 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
**/
export function humanizeEntityRef(
@@ -76,6 +82,10 @@ export function humanizeEntityRef(
*
* If neither of those are found or populated, fallback to `defaultName`.
*
+ * @deprecated Use {@link useEntityPresentation} or {@link EntityDisplayName}
+ * 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.
* @returns Readable name, defaults to `defaultName`.
diff --git a/plugins/catalog-react/src/components/EntityTable/columns.tsx b/plugins/catalog-react/src/components/EntityTable/columns.tsx
index 1bea56dc27..82dd8e7309 100644
--- a/plugins/catalog-react/src/components/EntityTable/columns.tsx
+++ b/plugins/catalog-react/src/components/EntityTable/columns.tsx
@@ -22,26 +22,23 @@ import {
} from '@backstage/catalog-model';
import { OverflowTooltip, TableColumn } from '@backstage/core-components';
import { getEntityRelations } from '../../utils';
-import {
- EntityRefLink,
- EntityRefLinks,
- humanizeEntityRef,
-} from '../EntityRefLink';
+import { EntityRefLink, EntityRefLinks } from '../EntityRefLink';
+import { entityPresentationSnapshot, EntityPresentationApi } from '../../apis';
import { EntityTableColumnTitle } from './TitleColumn';
/** @public */
export const columnFactories = Object.freeze({
createEntityRefColumn(options: {
defaultKind?: string;
+ entityPresentationApi?: EntityPresentationApi;
}): TableColumn {
- const { defaultKind } = options;
+ const { defaultKind, entityPresentationApi } = options;
function formatContent(entity: T): string {
- return (
- entity.metadata?.title ||
- humanizeEntityRef(entity, {
- defaultKind,
- })
- );
+ return entityPresentationSnapshot(
+ entity,
+ { defaultKind },
+ entityPresentationApi,
+ ).primaryTitle;
}
return {
@@ -75,8 +72,15 @@ export const columnFactories = Object.freeze({
relation: string;
defaultKind?: string;
filter?: { kind: string };
+ entityPresentationApi?: EntityPresentationApi;
}): TableColumn {
- const { title, relation, defaultKind, filter: entityFilter } = options;
+ const {
+ title,
+ relation,
+ defaultKind,
+ filter: entityFilter,
+ entityPresentationApi,
+ } = options;
function getRelations(entity: T): CompoundEntityRef[] {
return getEntityRelations(entity, relation, entityFilter);
@@ -84,7 +88,14 @@ export const columnFactories = Object.freeze({
function formatContent(entity: T): string {
return getRelations(entity)
- .map(r => humanizeEntityRef(r, { defaultKind }))
+ .map(
+ r =>
+ entityPresentationSnapshot(
+ r,
+ { defaultKind },
+ entityPresentationApi,
+ ).primaryTitle,
+ )
.join(', ');
}
diff --git a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx
index 771eb7b279..ec7c0dd54f 100644
--- a/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx
+++ b/plugins/catalog-react/src/components/InspectEntityDialog/components/AncestryPage.tsx
@@ -35,8 +35,8 @@ import { useLayoutEffect, useRef, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import useAsync from 'react-use/esm/useAsync';
import { catalogApiRef } from '../../../api';
-import { humanizeEntityRef } from '../../EntityRefLink';
import { entityRouteRef } from '../../../routes';
+import { useEntityPresentation } from '../../../apis';
import { EntityKindIcon } from './EntityKindIcon';
import { catalogReactTranslationRef } from '../../../translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
@@ -137,15 +137,7 @@ function CustomNode({ node }: DependencyGraphTypes.RenderNodeProps) {
const paddedWidth = paddedIconWidth + width + padding * 2;
const paddedHeight = height + padding * 2;
- const displayTitle =
- node.metadata.title ||
- (node.kind && node.metadata.name && node.metadata.namespace
- ? humanizeEntityRef({
- kind: node.kind,
- name: node.metadata.name,
- namespace: node.metadata.namespace || '',
- })
- : node.id);
+ const { primaryTitle: displayTitle } = useEntityPresentation(node);
const onClick = () => {
navigate(
diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md
index 3c88849706..12104ec8ac 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;
+ entityPresentationApi?: 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 80b37002c5..b021aae0e1 100644
--- a/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
+++ b/plugins/catalog/src/components/CatalogTable/CatalogTable.tsx
@@ -29,17 +29,19 @@ import {
WarningPanel,
} from '@backstage/core-components';
import {
+ entityPresentationApiRef,
+ entityPresentationSnapshot,
getEntityRelations,
- humanizeEntityRef,
useEntityList,
useStarredEntities,
+ type EntityPresentationApi,
} from '@backstage/plugin-catalog-react';
import CircularProgress from '@material-ui/core/CircularProgress';
import Typography from '@material-ui/core/Typography';
import { visuallyHidden } from '@mui/utils';
import Edit from '@material-ui/icons/Edit';
import OpenInNew from '@material-ui/icons/OpenInNew';
-import { capitalize } from 'lodash';
+import { capitalize, sortBy } from 'lodash';
import pluralize from 'pluralize';
import { ReactNode, useMemo } from 'react';
import { columnFactories } from './columns';
@@ -47,6 +49,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,14 +72,13 @@ export interface CatalogTableProps {
subtitle?: string;
}
-const refCompare = (a: Entity, b: Entity) => {
- const toRef = (entity: Entity) =>
- entity.metadata.title ||
- humanizeEntityRef(entity, {
- defaultKind: 'Component',
- });
-
- return toRef(a).localeCompare(toRef(b));
+const sortEntities = (entities: Entity[], api?: EntityPresentationApi) => {
+ return sortBy(
+ entities,
+ e =>
+ entityPresentationSnapshot(e, { defaultKind: 'Component' }, api)
+ .primaryTitle,
+ );
};
/**
@@ -97,6 +99,8 @@ export const CatalogTable = (props: CatalogTableProps) => {
emptyContent,
} = props;
const { isStarredEntity, toggleStarredEntity } = useStarredEntities();
+ const apis = useApiHolder();
+ const entityPresentationApi = apis.get(entityPresentationApiRef);
const entityListContext = useEntityList();
const {
@@ -234,7 +238,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}
/>
@@ -249,12 +253,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 = sortEntities(entities, entityPresentationApi).map(e =>
+ toEntityRow(e, entityPresentationApi),
+ );
const pageSize = 20;
const showPagination = rows.length > pageSize;
@@ -280,7 +286,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',
});
@@ -292,19 +298,25 @@ 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: humanizeEntityRef(entity, {
- defaultKind: 'Component',
- }),
+ name: entityPresentationSnapshot(
+ entity,
+ { defaultKind: 'Component' },
+ api,
+ ).primaryTitle,
entityRef: stringifyEntityRef(entity),
ownedByRelationsTitle: ownedByRelations
- .map(r => humanizeEntityRef(r, { defaultKind: 'group' }))
+ .map(
+ r =>
+ entityPresentationSnapshot(r, { defaultKind: 'group' }, api)
+ .primaryTitle,
+ )
.join(', '),
ownedByRelations,
partOfSystemRelationTitle: partOfSystemRelations
- .map(r =>
- humanizeEntityRef(r, {
- defaultKind: 'system',
- }),
+ .map(
+ r =>
+ entityPresentationSnapshot(r, { defaultKind: 'system' }, api)
+ .primaryTitle,
)
.join(', '),
partOfSystemRelations,
diff --git a/plugins/catalog/src/components/CatalogTable/columns.tsx b/plugins/catalog/src/components/CatalogTable/columns.tsx
index d2216149cd..55b2d5c6a8 100644
--- a/plugins/catalog/src/components/CatalogTable/columns.tsx
+++ b/plugins/catalog/src/components/CatalogTable/columns.tsx
@@ -14,9 +14,10 @@
* limitations under the License.
*/
import {
- humanizeEntityRef,
+ defaultEntityPresentation,
EntityRefLink,
EntityRefLinks,
+ type EntityPresentationApi,
} from '@backstage/plugin-catalog-react';
import Chip from '@material-ui/core/Chip';
import { CatalogTableRow } from './types';
@@ -31,14 +32,17 @@ import { EntityTableColumnTitle } from '@backstage/plugin-catalog-react/alpha';
export const columnFactories = Object.freeze({
createNameColumn(options?: {
defaultKind?: string;
+ entityPresentationApi?: EntityPresentationApi;
}): TableColumn {
function formatContent(entity: Entity): string {
- return (
- entity.metadata?.title ||
- humanizeEntityRef(entity, {
+ if (options?.entityPresentationApi) {
+ return options.entityPresentationApi.forEntity(entity, {
defaultKind: options?.defaultKind,
- })
- );
+ }).snapshot.primaryTitle;
+ }
+ return defaultEntityPresentation(entity, {
+ defaultKind: options?.defaultKind,
+ }).primaryTitle;
}
return {
diff --git a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx
index 12dc69073b..b193d4e092 100644
--- a/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx
+++ b/plugins/org-react/src/components/GroupListPicker/GroupListPicker.tsx
@@ -17,15 +17,16 @@
import { MouseEvent, useState, useCallback } from 'react';
import {
catalogApiRef,
- humanizeEntityRef,
+ 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 { Entity, GroupEntity } from '@backstage/catalog-model';
+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);
@@ -85,8 +88,6 @@ export const GroupListPicker = (props: GroupListPickerProps) => {
return ;
}
- const getHumanEntityRef = (entity: Entity) => humanizeEntityRef(entity);
-
return (
<>
{
options={groups ?? []}
groupBy={option => option.spec.type}
getOptionLabel={option =>
- option.spec.profile?.displayName ?? getHumanEntityRef(option)
+ 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 cb40ff2766..efb1a06822 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,
- humanizeEntityRef,
+ 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,23 +167,26 @@ export const TemplateFormPreviewer = ({
'spec.output',
],
})
- .then(({ items }) =>
- setTemplateOptions(
- items.map(template => ({
- label:
- template.metadata.title ??
- humanizeEntityRef(template, { defaultKind: 'template' }),
+ .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}`,
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 4c6383c35e..b0b5c41fcd 100644
--- a/plugins/techdocs/src/home/components/Tables/helpers.ts
+++ b/plugins/techdocs/src/home/components/Tables/helpers.ts
@@ -16,8 +16,9 @@
import { RELATION_OWNED_BY, Entity } from '@backstage/catalog-model';
import {
+ entityPresentationSnapshot,
getEntityRelations,
- humanizeEntityRef,
+ type EntityPresentationApi,
} from '@backstage/plugin-catalog-react';
import { toLowerMaybe } from '../../../helpers';
import { ConfigApi, RouteFunc } from '@backstage/core-plugin-api';
@@ -32,6 +33,7 @@ export function entitiesToDocsMapper(
entities: Entity[],
getRouteToReaderPageFor: getRouteFunc,
config: ConfigApi,
+ entityPresentationApi?: EntityPresentationApi,
) {
return entities.map(entity => {
const ownedByRelations = getEntityRelations(entity, RELATION_OWNED_BY);
@@ -48,7 +50,14 @@ export function entitiesToDocsMapper(
}),
ownedByRelations,
ownedByRelationsTitle: ownedByRelations
- .map(r => humanizeEntityRef(r, { defaultKind: 'group' }))
+ .map(
+ r =>
+ entityPresentationSnapshot(
+ r,
+ { defaultKind: 'group' },
+ entityPresentationApi,
+ ).primaryTitle,
+ )
.join(', '),
},
};