Fix unsafe type casts via shared entityPresentationSnapshot utility
Extract entityPresentationSnapshot helper to eliminate unsafe `as Entity` casts when passing CompoundEntityRef to EntityPresentationApi.forEntity(), which only accepts Entity | string. The helper safely discriminates input types and stringifies CompoundEntityRef before calling the API. - Add entityPresentationSnapshot as a public export from catalog-react - Remove duplicated getEntityTitle/getTitle helpers across 5 files - Fix useAsync dependency array in TemplateFormPreviewer - Update TSDoc across all presentation API surfaces to reference the new helper - Update entity-presentation.md docs with usage guidance and migration table Made-with: Cursor Signed-off-by: Marat Dyatko <maratd@spotify.com>
This commit is contained in:
@@ -52,24 +52,12 @@ function MyComponent({ entityRef }: { entityRef: string }) {
|
||||
The hook subscribes to the `EntityPresentationApi` and returns a snapshot
|
||||
that may update over time as additional data is fetched in the background.
|
||||
|
||||
### Using the API directly
|
||||
### Using the API directly (async, preferred for non-React)
|
||||
|
||||
In contexts where hooks are not available, you can use the
|
||||
`entityPresentationApiRef` API directly. The API provides two access
|
||||
patterns:
|
||||
|
||||
- **`.snapshot`** for synchronous access (for example in sort comparators or
|
||||
filter callbacks):
|
||||
|
||||
```ts
|
||||
import { entityPresentationApiRef } from '@backstage/plugin-catalog-react';
|
||||
|
||||
const title = entityPresentationApi.forEntity(entity, {
|
||||
defaultKind: 'Component',
|
||||
}).snapshot.primaryTitle;
|
||||
```
|
||||
|
||||
- **`.promise`** for async contexts (for example inside data loaders):
|
||||
In non-React **async** contexts where you can `await` -- such as data
|
||||
loaders, `useAsync` callbacks, or event handlers -- use the
|
||||
`entityPresentationApiRef` API directly with `.promise` for the richest
|
||||
possible presentation:
|
||||
|
||||
```ts
|
||||
const presentation = await entityPresentationApi.forEntity(entity, {
|
||||
@@ -78,9 +66,39 @@ const presentation = await entityPresentationApi.forEntity(entity, {
|
||||
const title = presentation.primaryTitle;
|
||||
```
|
||||
|
||||
The `.snapshot` path uses cached data when available, so it performs well
|
||||
even in tight loops like sorting. The `.promise` path resolves to a richer
|
||||
presentation that may include data fetched from the catalog.
|
||||
The `.promise` path resolves to a full presentation that may include data
|
||||
fetched from the catalog. **This is the preferred approach whenever an
|
||||
async context is available.**
|
||||
|
||||
### `entityPresentationSnapshot` helper (synchronous fallback)
|
||||
|
||||
When a synchronous return value is required and `await` is not possible --
|
||||
such as in sort comparators, column factories, or filter callbacks -- use
|
||||
`entityPresentationSnapshot` as a fallback. It accepts `Entity`,
|
||||
`CompoundEntityRef`, or string ref inputs and uses the presentation API
|
||||
when available, falling back to `defaultEntityPresentation` otherwise:
|
||||
|
||||
```ts
|
||||
import {
|
||||
entityPresentationSnapshot,
|
||||
entityPresentationApiRef,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
|
||||
// In a column factory or sort comparator where you have
|
||||
// the API instance (or undefined if not registered):
|
||||
const title = entityPresentationSnapshot(
|
||||
entity,
|
||||
{
|
||||
defaultKind: 'Component',
|
||||
},
|
||||
entityPresentationApi,
|
||||
).primaryTitle;
|
||||
```
|
||||
|
||||
Because this function is synchronous, it uses cached data from the
|
||||
presentation API. If the entity has been seen before, the snapshot will
|
||||
contain the full resolved title; otherwise it falls back to what can be
|
||||
extracted from the ref alone.
|
||||
|
||||
## Customizing entity presentation
|
||||
|
||||
@@ -124,6 +142,6 @@ Replace them as follows:
|
||||
| :---------------------------------------------------- | :--------------------------------------------------------------------- |
|
||||
| `humanizeEntityRef(entity)` in JSX | `<EntityDisplayName entityRef={entity} />` |
|
||||
| `humanizeEntityRef(entity)` in a React component | `useEntityPresentation(entity).primaryTitle` |
|
||||
| `humanizeEntityRef(entity)` in a sort/filter callback | `entityPresentationApi.forEntity(entity).snapshot.primaryTitle` |
|
||||
| `humanizeEntityRef(entity)` in an async loader | `(await entityPresentationApi.forEntity(entity).promise).primaryTitle` |
|
||||
| `humanizeEntityRef(entity)` in a sort/filter callback | `entityPresentationSnapshot(entity, ctx, api).primaryTitle` |
|
||||
| `humanizeEntity(entity, fallback)` | `useEntityPresentation(entity).primaryTitle` |
|
||||
|
||||
@@ -593,6 +593,16 @@ export interface EntityPresentationApi {
|
||||
// @public
|
||||
export const entityPresentationApiRef: ApiRef_2<EntityPresentationApi>;
|
||||
|
||||
// @public
|
||||
export function entityPresentationSnapshot(
|
||||
entityOrRef: Entity | CompoundEntityRef | string,
|
||||
context?: {
|
||||
defaultKind?: string;
|
||||
defaultNamespace?: string;
|
||||
},
|
||||
entityPresentationApi?: EntityPresentationApi,
|
||||
): EntityRefPresentationSnapshot;
|
||||
|
||||
// @public (undocumented)
|
||||
export const EntityProcessingStatusPicker: () => JSX_2.Element;
|
||||
|
||||
|
||||
@@ -37,8 +37,8 @@ import { Observable } from '@backstage/types';
|
||||
* and tooltip.
|
||||
*
|
||||
* - In non-React contexts such as sort comparators or data mappers, use the
|
||||
* API directly via `forEntity().snapshot` for synchronous access, or
|
||||
* `forEntity().promise` in async loaders.
|
||||
* {@link entityPresentationSnapshot} helper for synchronous access, or
|
||||
* the API directly via `forEntity().promise` in async loaders.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
@@ -143,8 +143,8 @@ export interface EntityRefPresentation {
|
||||
* - {@link EntityDisplayName} — React component that renders an entity name
|
||||
* with optional icon and tooltip.
|
||||
*
|
||||
* For non-React contexts, you can use the API directly via
|
||||
* `forEntity().snapshot` for synchronous access, or `forEntity().promise`
|
||||
* For non-React contexts, use the {@link entityPresentationSnapshot} helper
|
||||
* for synchronous access, or the API directly via `forEntity().promise`
|
||||
* for async contexts.
|
||||
*
|
||||
* Implement this interface to customize how entities are displayed throughout
|
||||
|
||||
@@ -35,10 +35,10 @@ import { EntityRefPresentationSnapshot } from './EntityPresentationApi';
|
||||
*
|
||||
* This function is primarily used as the internal fallback within the
|
||||
* {@link EntityPresentationApi} when no custom implementation is registered.
|
||||
* Prefer using the API directly via `forEntity().snapshot` or
|
||||
* `forEntity().promise`, which respects custom presentation overrides.
|
||||
* In React components, use the {@link useEntityPresentation} hook or the
|
||||
* {@link EntityDisplayName} component.
|
||||
* {@link EntityDisplayName} component. In non-React contexts, use
|
||||
* {@link entityPresentationSnapshot} which respects custom presentation
|
||||
* overrides and falls back to this function when no API is registered.
|
||||
*
|
||||
* @public
|
||||
* @param entityOrRef - Either an entity, or a ref to it.
|
||||
|
||||
+173
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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);
|
||||
}
|
||||
@@ -21,4 +21,5 @@ export {
|
||||
type EntityRefPresentationSnapshot,
|
||||
} from './EntityPresentationApi';
|
||||
export { defaultEntityPresentation } from './defaultEntityPresentation';
|
||||
export { entityPresentationSnapshot } from './entityPresentationSnapshot';
|
||||
export { useEntityPresentation } from './useEntityPresentation';
|
||||
|
||||
@@ -43,7 +43,7 @@ import { useUpdatingObservable } from './useUpdatingObservable';
|
||||
* component instead, which wraps this hook with icon and tooltip support.
|
||||
*
|
||||
* For non-React contexts such as sort comparators or data mappers, use
|
||||
* the {@link EntityPresentationApi} directly via `forEntity().snapshot`.
|
||||
* {@link entityPresentationSnapshot}.
|
||||
*
|
||||
* @public
|
||||
* @param entityOrRef - The entity to represent, or an entity ref to it. If you
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
import { Cell, CellText, Column, ColumnConfig, TableItem } from '@backstage/ui';
|
||||
import { EntityRefLink, EntityRefLinks } from '../EntityRefLink';
|
||||
import {
|
||||
defaultEntityPresentation,
|
||||
entityPresentationSnapshot,
|
||||
EntityPresentationApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
import { EntityTableColumnTitle } from '../EntityTable/TitleColumn';
|
||||
@@ -36,18 +36,6 @@ export interface EntityColumnConfig extends ColumnConfig<EntityRow> {
|
||||
sortValue?: (entity: EntityRow) => string;
|
||||
}
|
||||
|
||||
function getEntityTitle(
|
||||
entityOrRef: Entity | { kind: string; namespace?: string; name: string },
|
||||
context: { defaultKind?: string },
|
||||
entityPresentationApi?: EntityPresentationApi,
|
||||
): string {
|
||||
if (entityPresentationApi) {
|
||||
return entityPresentationApi.forEntity(entityOrRef as Entity, context)
|
||||
.snapshot.primaryTitle;
|
||||
}
|
||||
return defaultEntityPresentation(entityOrRef as Entity, context).primaryTitle;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const columnFactories = Object.freeze({
|
||||
createEntityRefColumn(options: {
|
||||
@@ -76,11 +64,11 @@ export const columnFactories = Object.freeze({
|
||||
</Cell>
|
||||
),
|
||||
sortValue: entity =>
|
||||
getEntityTitle(
|
||||
entityPresentationSnapshot(
|
||||
entity,
|
||||
{ defaultKind: options.defaultKind },
|
||||
options.entityPresentationApi,
|
||||
),
|
||||
).primaryTitle,
|
||||
};
|
||||
},
|
||||
|
||||
@@ -115,12 +103,13 @@ export const columnFactories = Object.freeze({
|
||||
),
|
||||
sortValue: entity =>
|
||||
getEntityRelations(entity, options.relation, options.filter)
|
||||
.map(r =>
|
||||
getEntityTitle(
|
||||
r,
|
||||
{ defaultKind: options.defaultKind },
|
||||
options.entityPresentationApi,
|
||||
),
|
||||
.map(
|
||||
r =>
|
||||
entityPresentationSnapshot(
|
||||
r,
|
||||
{ defaultKind: options.defaultKind },
|
||||
options.entityPresentationApi,
|
||||
).primaryTitle,
|
||||
)
|
||||
.join(', '),
|
||||
};
|
||||
|
||||
@@ -70,7 +70,7 @@ export type EntityDisplayNameProps = {
|
||||
*
|
||||
* For more control over the presentation data, use the
|
||||
* {@link useEntityPresentation} hook directly. For non-React contexts, use
|
||||
* the {@link EntityPresentationApi} directly via `forEntity().snapshot`.
|
||||
* {@link entityPresentationSnapshot}.
|
||||
*
|
||||
* @public
|
||||
*/
|
||||
|
||||
@@ -34,8 +34,8 @@ import { useDebouncedEffect } from '@react-hookz/web';
|
||||
import PersonIcon from '@material-ui/icons/Person';
|
||||
import GroupIcon from '@material-ui/icons/Group';
|
||||
import {
|
||||
defaultEntityPresentation,
|
||||
entityPresentationApiRef,
|
||||
entityPresentationSnapshot,
|
||||
} from '../../apis';
|
||||
import { useFetchEntities } from './useFetchEntities';
|
||||
import { withStyles } from '@material-ui/core/styles';
|
||||
@@ -209,11 +209,11 @@ export const EntityOwnerPicker = (props?: EntityOwnerPickerProps) => {
|
||||
defaultNamespace: 'default',
|
||||
})
|
||||
: o;
|
||||
if (entityPresentationApi) {
|
||||
return entityPresentationApi.forEntity(entity as Entity).snapshot
|
||||
.primaryTitle;
|
||||
}
|
||||
return defaultEntityPresentation(entity).primaryTitle;
|
||||
return entityPresentationSnapshot(
|
||||
entity,
|
||||
undefined,
|
||||
entityPresentationApi,
|
||||
).primaryTitle;
|
||||
}}
|
||||
onChange={(_: object, owners) => {
|
||||
setText('');
|
||||
|
||||
@@ -26,9 +26,10 @@ import get from 'lodash/get';
|
||||
* if set to string which matches namespace of entity then omitted
|
||||
*
|
||||
* @deprecated Use {@link useEntityPresentation} or {@link EntityDisplayName}
|
||||
* in React components, or access the {@link entityPresentationApiRef} directly.
|
||||
* These provide richer display names using `metadata.title` and
|
||||
* `spec.profile.displayName` in addition to the entity ref.
|
||||
* in React components. In non-React contexts such as sort comparators or
|
||||
* data mappers, use {@link entityPresentationSnapshot}. These provide richer
|
||||
* display names using `metadata.title` and `spec.profile.displayName` in
|
||||
* addition to the entity ref.
|
||||
*
|
||||
* @public
|
||||
**/
|
||||
@@ -82,7 +83,8 @@ export function humanizeEntityRef(
|
||||
* If neither of those are found or populated, fallback to `defaultName`.
|
||||
*
|
||||
* @deprecated Use {@link useEntityPresentation} or {@link EntityDisplayName}
|
||||
* in React components, or access the {@link entityPresentationApiRef} directly.
|
||||
* in React components. In non-React contexts, use
|
||||
* {@link entityPresentationSnapshot}.
|
||||
*
|
||||
* @param entity - Entity to convert.
|
||||
* @param defaultName - If entity readable name is not available, `defaultName` will be returned.
|
||||
|
||||
@@ -23,21 +23,9 @@ import {
|
||||
import { OverflowTooltip, TableColumn } from '@backstage/core-components';
|
||||
import { getEntityRelations } from '../../utils';
|
||||
import { EntityRefLink, EntityRefLinks } from '../EntityRefLink';
|
||||
import { defaultEntityPresentation, EntityPresentationApi } from '../../apis';
|
||||
import { entityPresentationSnapshot, EntityPresentationApi } from '../../apis';
|
||||
import { EntityTableColumnTitle } from './TitleColumn';
|
||||
|
||||
function getEntityTitle(
|
||||
entityOrRef: Entity | CompoundEntityRef,
|
||||
context: { defaultKind?: string },
|
||||
entityPresentationApi?: EntityPresentationApi,
|
||||
): string {
|
||||
if (entityPresentationApi) {
|
||||
return entityPresentationApi.forEntity(entityOrRef as Entity, context)
|
||||
.snapshot.primaryTitle;
|
||||
}
|
||||
return defaultEntityPresentation(entityOrRef, context).primaryTitle;
|
||||
}
|
||||
|
||||
/** @public */
|
||||
export const columnFactories = Object.freeze({
|
||||
createEntityRefColumn<T extends Entity>(options: {
|
||||
@@ -46,7 +34,11 @@ export const columnFactories = Object.freeze({
|
||||
}): TableColumn<T> {
|
||||
const { defaultKind, entityPresentationApi } = options;
|
||||
function formatContent(entity: T): string {
|
||||
return getEntityTitle(entity, { defaultKind }, entityPresentationApi);
|
||||
return entityPresentationSnapshot(
|
||||
entity,
|
||||
{ defaultKind },
|
||||
entityPresentationApi,
|
||||
).primaryTitle;
|
||||
}
|
||||
|
||||
return {
|
||||
@@ -96,7 +88,14 @@ export const columnFactories = Object.freeze({
|
||||
|
||||
function formatContent(entity: T): string {
|
||||
return getRelations(entity)
|
||||
.map(r => getEntityTitle(r, { defaultKind }, entityPresentationApi))
|
||||
.map(
|
||||
r =>
|
||||
entityPresentationSnapshot(
|
||||
r,
|
||||
{ defaultKind },
|
||||
entityPresentationApi,
|
||||
).primaryTitle,
|
||||
)
|
||||
.join(', ');
|
||||
}
|
||||
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
import {
|
||||
ANNOTATION_EDIT_URL,
|
||||
ANNOTATION_VIEW_URL,
|
||||
CompoundEntityRef,
|
||||
Entity,
|
||||
RELATION_OWNED_BY,
|
||||
RELATION_PART_OF,
|
||||
@@ -30,8 +29,8 @@ import {
|
||||
WarningPanel,
|
||||
} from '@backstage/core-components';
|
||||
import {
|
||||
defaultEntityPresentation,
|
||||
entityPresentationApiRef,
|
||||
entityPresentationSnapshot,
|
||||
getEntityRelations,
|
||||
useEntityList,
|
||||
useStarredEntities,
|
||||
@@ -73,21 +72,13 @@ export interface CatalogTableProps {
|
||||
subtitle?: string;
|
||||
}
|
||||
|
||||
function getTitle(
|
||||
entityOrRef: Entity | CompoundEntityRef,
|
||||
context: { defaultKind?: string },
|
||||
api?: EntityPresentationApi,
|
||||
): string {
|
||||
if (api) {
|
||||
const ref =
|
||||
'metadata' in entityOrRef ? entityOrRef : stringifyEntityRef(entityOrRef);
|
||||
return api.forEntity(ref, context).snapshot.primaryTitle;
|
||||
}
|
||||
return defaultEntityPresentation(entityOrRef, context).primaryTitle;
|
||||
}
|
||||
|
||||
const sortEntities = (entities: Entity[], api?: EntityPresentationApi) => {
|
||||
return sortBy(entities, e => getTitle(e, { defaultKind: 'Component' }, api));
|
||||
return sortBy(
|
||||
entities,
|
||||
e =>
|
||||
entityPresentationSnapshot(e, { defaultKind: 'Component' }, api)
|
||||
.primaryTitle,
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
@@ -307,14 +298,26 @@ function toEntityRow(entity: Entity, api?: EntityPresentationApi) {
|
||||
// This name is here for backwards compatibility mostly; the
|
||||
// presentation of refs in the table should in general be handled with
|
||||
// EntityRefLink / EntityName components
|
||||
name: getTitle(entity, { defaultKind: 'Component' }, api),
|
||||
name: entityPresentationSnapshot(
|
||||
entity,
|
||||
{ defaultKind: 'Component' },
|
||||
api,
|
||||
).primaryTitle,
|
||||
entityRef: stringifyEntityRef(entity),
|
||||
ownedByRelationsTitle: ownedByRelations
|
||||
.map(r => getTitle(r, { defaultKind: 'group' }, api))
|
||||
.map(
|
||||
r =>
|
||||
entityPresentationSnapshot(r, { defaultKind: 'group' }, api)
|
||||
.primaryTitle,
|
||||
)
|
||||
.join(', '),
|
||||
ownedByRelations,
|
||||
partOfSystemRelationTitle: partOfSystemRelations
|
||||
.map(r => getTitle(r, { defaultKind: 'system' }, api))
|
||||
.map(
|
||||
r =>
|
||||
entityPresentationSnapshot(r, { defaultKind: 'system' }, api)
|
||||
.primaryTitle,
|
||||
)
|
||||
.join(', '),
|
||||
partOfSystemRelations,
|
||||
},
|
||||
|
||||
+1
-1
@@ -186,7 +186,7 @@ export const TemplateFormPreviewer = ({
|
||||
severity: 'error',
|
||||
}),
|
||||
),
|
||||
[catalogApi],
|
||||
[catalogApi, entityPresentationApi, alertApi],
|
||||
);
|
||||
|
||||
const handleSelectChange = useCallback(
|
||||
|
||||
@@ -14,13 +14,9 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { RELATION_OWNED_BY, Entity } from '@backstage/catalog-model';
|
||||
import {
|
||||
RELATION_OWNED_BY,
|
||||
Entity,
|
||||
stringifyEntityRef,
|
||||
} from '@backstage/catalog-model';
|
||||
import {
|
||||
defaultEntityPresentation,
|
||||
entityPresentationSnapshot,
|
||||
getEntityRelations,
|
||||
type EntityPresentationApi,
|
||||
} from '@backstage/plugin-catalog-react';
|
||||
@@ -54,15 +50,14 @@ export function entitiesToDocsMapper(
|
||||
}),
|
||||
ownedByRelations,
|
||||
ownedByRelationsTitle: ownedByRelations
|
||||
.map(r => {
|
||||
if (entityPresentationApi) {
|
||||
return entityPresentationApi.forEntity(stringifyEntityRef(r), {
|
||||
defaultKind: 'group',
|
||||
}).snapshot.primaryTitle;
|
||||
}
|
||||
return defaultEntityPresentation(r, { defaultKind: 'group' })
|
||||
.primaryTitle;
|
||||
})
|
||||
.map(
|
||||
r =>
|
||||
entityPresentationSnapshot(
|
||||
r,
|
||||
{ defaultKind: 'group' },
|
||||
entityPresentationApi,
|
||||
).primaryTitle,
|
||||
)
|
||||
.join(', '),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user