Address PR review feedback from freben
- Consolidate changesets: one for catalog-react (deprecation), one combined for the remaining 5 plugins - Add entity-presentation to microsite/sidebars.ts - Update @deprecated tags to point to useEntityPresentation / entityPresentationApiRef only, not defaultEntityPresentation - Use entityPresentationApiRef with .promise in async loaders (StepPrepareCreatePullRequest, TemplateFormPreviewer) - Add optional entityPresentation?: EntityPresentationApi param to sync column factories and use .snapshot when available (columnFactories, EntityTable/columns, EntityOwnerPicker, CatalogTable) - Rewrite entity-presentation.md to recommend .snapshot/.promise instead of defaultEntityPresentation - Update TSDoc on entityPresentationApiRef, EntityPresentationApi, defaultEntityPresentation, useEntityPresentation, EntityDisplayName - Re-export presentation API types from alpha entry point - Update API reports - Fix test mocks for entityPresentationApiRef Made-with: Cursor Signed-off-by: Marat Dyatko <maratd@spotify.com> Made-with: Cursor
This commit is contained in:
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-catalog-import': patch
|
||||
---
|
||||
|
||||
Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `StepPrepareCreatePullRequest`.
|
||||
@@ -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.
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-catalog': patch
|
||||
---
|
||||
|
||||
Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `CatalogTable` and its column factories.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-org-react': patch
|
||||
---
|
||||
|
||||
Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `GroupListPicker`.
|
||||
@@ -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.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder': patch
|
||||
---
|
||||
|
||||
Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in `TemplateFormPreviewer`.
|
||||
@@ -1,5 +0,0 @@
|
||||
---
|
||||
'@backstage/plugin-techdocs': patch
|
||||
---
|
||||
|
||||
Replaced `humanizeEntityRef` with `defaultEntityPresentation` from the Catalog Presentation API in TechDocs table helpers.
|
||||
@@ -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 | `<EntityDisplayName entityRef={entity} />` |
|
||||
| `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 | `<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` |
|
||||
| `humanizeEntity(entity, fallback)` | `useEntityPresentation(entity).primaryTitle` |
|
||||
|
||||
@@ -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',
|
||||
|
||||
+17
-1
@@ -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('<StepPrepareCreatePullRequest />', () => {
|
||||
|
||||
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<typeof errorApiRef.T> = {
|
||||
error$: jest.fn(),
|
||||
post: jest.fn(),
|
||||
@@ -54,6 +69,7 @@ describe('<StepPrepareCreatePullRequest />', () => {
|
||||
apis={[
|
||||
[catalogImportApiRef, catalogImportApi],
|
||||
[catalogApiRef, catalogApi],
|
||||
[entityPresentationApiRef, entityPresentationApi],
|
||||
[errorApiRef, errorApi],
|
||||
[configApiRef, configApi],
|
||||
]}
|
||||
|
||||
+8
-6
@@ -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(
|
||||
|
||||
@@ -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<EntityRefPresentationSnapshot>;
|
||||
snapshot: EntityRefPresentationSnapshot;
|
||||
update$?: Observable<EntityRefPresentationSnapshot>;
|
||||
}
|
||||
|
||||
// @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 = () =>
|
||||
| {
|
||||
|
||||
@@ -266,6 +266,7 @@ export type CatalogReactUserListPickerClassKey =
|
||||
export const columnFactories: Readonly<{
|
||||
createEntityRefColumn<T extends Entity>(options: {
|
||||
defaultKind?: string;
|
||||
entityPresentation?: EntityPresentationApi;
|
||||
}): TableColumn<T>;
|
||||
createEntityRelationColumn<T extends Entity>(options: {
|
||||
title: string | JSX.Element;
|
||||
@@ -274,6 +275,7 @@ export const columnFactories: Readonly<{
|
||||
filter?: {
|
||||
kind: string;
|
||||
};
|
||||
entityPresentation?: EntityPresentationApi;
|
||||
}): TableColumn<T>;
|
||||
createOwnerColumn<T extends Entity>(): TableColumn<T>;
|
||||
createDomainColumn<T extends Entity>(): TableColumn<T>;
|
||||
@@ -687,6 +689,7 @@ export const EntityTable: {
|
||||
columns: Readonly<{
|
||||
createEntityRefColumn<T extends Entity>(options: {
|
||||
defaultKind?: string;
|
||||
entityPresentation?: EntityPresentationApi;
|
||||
}): TableColumn<T>;
|
||||
createEntityRelationColumn<T extends Entity>(options: {
|
||||
title: string | JSX.Element;
|
||||
@@ -695,6 +698,7 @@ export const EntityTable: {
|
||||
filter?: {
|
||||
kind: string;
|
||||
};
|
||||
entityPresentation?: EntityPresentationApi;
|
||||
}): TableColumn<T>;
|
||||
createOwnerColumn<T extends Entity>(): TableColumn<T>;
|
||||
createDomainColumn<T extends Entity>(): TableColumn<T>;
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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<EntityRow> {
|
||||
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({
|
||||
</Cell>
|
||||
),
|
||||
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(', '),
|
||||
};
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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<T extends Entity>(options: {
|
||||
defaultKind?: string;
|
||||
entityPresentation?: EntityPresentationApi;
|
||||
}): TableColumn<T> {
|
||||
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<T> {
|
||||
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(', ');
|
||||
}
|
||||
|
||||
|
||||
@@ -153,6 +153,7 @@ export const CatalogTable: {
|
||||
columns: Readonly<{
|
||||
createNameColumn(options?: {
|
||||
defaultKind?: string;
|
||||
entityPresentation?: EntityPresentationApi;
|
||||
}): TableColumn<CatalogTableRow>;
|
||||
createSystemColumn(): TableColumn<CatalogTableRow>;
|
||||
createOwnerColumn(): TableColumn<CatalogTableRow>;
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -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<CatalogTableRow> {
|
||||
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;
|
||||
|
||||
@@ -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<HTMLElement | null>(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)}
|
||||
|
||||
+28
-3
@@ -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(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApiMock],
|
||||
[entityPresentationApiRef, entityPresentationApi],
|
||||
]}
|
||||
>
|
||||
<TemplateFormPage />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
@@ -41,7 +61,12 @@ describe('TemplateFormPage', () => {
|
||||
|
||||
it('Should have an link back to the edit page', async () => {
|
||||
await renderInTestApp(
|
||||
<TestApiProvider apis={[[catalogApiRef, catalogApiMock]]}>
|
||||
<TestApiProvider
|
||||
apis={[
|
||||
[catalogApiRef, catalogApiMock],
|
||||
[entityPresentationApiRef, entityPresentationApi],
|
||||
]}
|
||||
>
|
||||
<TemplateFormPage />
|
||||
</TestApiProvider>,
|
||||
{
|
||||
|
||||
+13
-9
@@ -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}`,
|
||||
|
||||
@@ -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(', '),
|
||||
},
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user