refactor(catalog,api-docs): migrate entity table cards to BUI (#33260)

* feat(catalog-react): add EntityDataTable and EntityRelationCard components

Add EntityDataTable (BUI Table wrapper for entity data) and
EntityRelationCard (card shell + data fetching + table) as shared
building blocks for entity relation table cards. Includes BUI column
factories, column presets for common entity types, and a new
translation key for the empty state help link.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* refactor(catalog): migrate 8 entity table cards to EntityRelationCard

Rewrite HasComponentsCard, HasResourcesCard, HasSubcomponentsCard,
HasSubdomainsCard, HasSystemsCard, DependsOnComponentsCard,
DependsOnResourcesCard, and DependencyOfComponentsCard as thin
wrappers around EntityRelationCard from catalog-react. Remove variant
and tableOptions props.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* refactor(api-docs): migrate 5 entity table cards to EntityRelationCard

Rewrite ConsumedApisCard, ProvidedApisCard, HasApisCard,
ConsumingComponentsCard, and ProvidingComponentsCard to use
EntityRelationCard from catalog-react. Add BUI column presets for
API-specific columns alongside existing MUI presets. Remove variant
and tableOptions props.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* chore: add API reports and changesets for entity table card migration

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* fix(catalog-react,api-docs): refine entity card migration

Use BUI Alert for error states and BUI Link for empty state help
links in EntityRelationCard instead of core-components. Replace MUI
ToggleButton with BUI ButtonIcon for the API definition column.
Add trailing periods to empty state translation messages. Fix tests
asserting on removed external link icon SVG.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* feat(catalog-react): add column sorting to EntityDataTable

Introduce EntityColumnConfig extending ColumnConfig with an optional
sortValue getter. EntityDataTable now uses useTable with a sortFn
that reads sortValue from each column, restoring the per-column
sorting that the old core-components Table provided by default. All
built-in column factories set isSortable and sortValue.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* fix(api-docs): use dynamic entity kind in empty state messages

Restore entity.kind interpolation in ConsumedApisCard, ProvidedApisCard,
and HasApisCard empty state translations instead of hardcoded strings.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* fix(catalog-react): use data length for useTable pageSize

Replace pageSize: Infinity with tableData.length to avoid potential
edge cases in pagination math.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* fix(catalog-react): avoid this binding in column factories

Use columnFactories.createEntityRelationColumn instead of
this.createEntityRelationColumn so the methods work when
destructured.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* chore: update alpha API reports

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* fix(create-app): remove variant prop from migrated entity cards in template

The create-app template EntityPage still passed variant="gridItem"
to cards that no longer accept it.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* test(catalog-react): add tests for EntityDataTable and EntityRelationCard

Cover rendering with data, empty state, error state, and column
sorting for EntityDataTable. Cover title rendering, empty state
with help link, related entity display, and error state for
EntityRelationCard.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* refactor(catalog,api-docs): add backwards compatibility for legacy props

Support both old (variant, columns, tableOptions) and new (columnConfig)
props via a discriminated union type. When legacy props are detected the
old MUI-based implementation is used; otherwise the new BUI-based
EntityRelationCard renders. This avoids immediate breaking changes and
provides a migration path. Changesets downgraded from major/minor to
minor/patch accordingly.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

* refactor(catalog-react,catalog,api-docs): address review feedback

Remove BaseProps types and use inline union in component signatures.
Move EntityDataTable, EntityRelationCard, and related types to alpha
exports. Group column presets and help links into entityColumnPresets
object. Update all consumers and API reports.

Signed-off-by: Johan Persson <johanopersson@gmail.com>

---------

Signed-off-by: Johan Persson <johanopersson@gmail.com>
This commit is contained in:
Johan Persson
2026-03-17 15:33:45 +01:00
committed by GitHub
parent 8e559b427d
commit c548a0ff86
53 changed files with 2135 additions and 287 deletions
@@ -0,0 +1,5 @@
---
'@backstage/plugin-catalog-react': minor
---
Added `EntityDataTable`, `EntityRelationCard`, `entityDataTableColumns`, `entityColumnPresets`, and related types as alpha exports. These replace `EntityTable` and `RelatedEntitiesCard` (from `@backstage/plugin-catalog`) respectively, providing a unified BUI-based pattern for entity table cards.
@@ -0,0 +1,5 @@
---
'@backstage/plugin-api-docs': patch
---
Migrated entity table cards (`ConsumedApisCard`, `ProvidedApisCard`, `HasApisCard`, `ConsumingComponentsCard`, `ProvidingComponentsCard`) to use BUI when no legacy props are passed. The old `variant`, `columns`, and `tableOptions` props are deprecated but still supported — passing any of them triggers the legacy MUI-based rendering. The new `columnConfig` prop accepts `EntityColumnConfig[]` for BUI-based rendering.
@@ -0,0 +1,7 @@
---
'@backstage/plugin-catalog': minor
---
Migrated entity table cards (`HasComponentsCard`, `HasResourcesCard`, `HasSubcomponentsCard`, `HasSubdomainsCard`, `HasSystemsCard`, `DependsOnComponentsCard`, `DependsOnResourcesCard`, `DependencyOfComponentsCard`) to use BUI when no legacy props are passed. The old `variant`, `columns`, and `tableOptions` props are deprecated but still supported — passing any of them triggers the legacy MUI-based rendering. The new `columnConfig` prop accepts `EntityColumnConfig[]` for BUI-based rendering.
`RelatedEntitiesCard` is deprecated — use `EntityRelationCard` from `@backstage/plugin-catalog-react/alpha` instead.
@@ -186,7 +186,7 @@ const overviewContent = (
</EntitySwitch>
<Grid item md={8} xs={12}>
<EntityHasSubcomponentsCard variant="gridItem" />
<EntityHasSubcomponentsCard />
</Grid>
</Grid>
);
@@ -215,10 +215,10 @@ const serviceEntityPage = (
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item xs={12} md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
<EntityDependsOnComponentsCard />
</Grid>
<Grid item xs={12} md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
<EntityDependsOnResourcesCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -246,10 +246,10 @@ const websiteEntityPage = (
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
<EntityDependsOnComponentsCard />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
<EntityDependsOnResourcesCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -374,13 +374,13 @@ const systemPage = (
<EntityCatalogGraphCard height={400} />
</Grid>
<Grid item md={6}>
<EntityHasComponentsCard variant="gridItem" />
<EntityHasComponentsCard />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
<EntityHasApisCard />
</Grid>
<Grid item md={6}>
<EntityHasResourcesCard variant="gridItem" />
<EntityHasResourcesCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -417,7 +417,7 @@ const domainPage = (
<EntityCatalogGraphCard height={400} />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
<EntityHasSystemsCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -436,7 +436,7 @@ const resourcePage = (
<EntityCatalogGraphCard height={400} />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
<EntityHasSystemsCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -138,7 +138,7 @@ const overviewContent = (
<EntityLinksCard />
</Grid>
<Grid item md={8} xs={12}>
<EntityHasSubcomponentsCard variant="gridItem" />
<EntityHasSubcomponentsCard />
</Grid>
</Grid>
);
@@ -175,10 +175,10 @@ const serviceEntityPage = (
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
<EntityDependsOnComponentsCard />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
<EntityDependsOnResourcesCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -210,10 +210,10 @@ const websiteEntityPage = (
<EntityLayout.Route path="/dependencies" title="Dependencies">
<Grid container spacing={3} alignItems="stretch">
<Grid item md={6}>
<EntityDependsOnComponentsCard variant="gridItem" />
<EntityDependsOnComponentsCard />
</Grid>
<Grid item md={6}>
<EntityDependsOnResourcesCard variant="gridItem" />
<EntityDependsOnResourcesCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -345,13 +345,13 @@ const systemPage = (
<EntityLinksCard />
</Grid>
<Grid item md={8}>
<EntityHasComponentsCard variant="gridItem" />
<EntityHasComponentsCard />
</Grid>
<Grid item md={6}>
<EntityHasApisCard variant="gridItem" />
<EntityHasApisCard />
</Grid>
<Grid item md={6}>
<EntityHasResourcesCard variant="gridItem" />
<EntityHasResourcesCard />
</Grid>
</Grid>
</EntityLayout.Route>
@@ -388,7 +388,7 @@ const domainPage = (
<EntityCatalogGraphCard height={400} />
</Grid>
<Grid item md={6}>
<EntityHasSystemsCard variant="gridItem" />
<EntityHasSystemsCard />
</Grid>
</Grid>
</EntityLayout.Route>
+2
View File
@@ -62,10 +62,12 @@
"@backstage/plugin-catalog-common": "workspace:^",
"@backstage/plugin-catalog-react": "workspace:^",
"@backstage/plugin-permission-react": "workspace:^",
"@backstage/ui": "workspace:^",
"@graphiql/react": "0.29.0",
"@material-ui/core": "^4.12.2",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.61",
"@remixicon/react": "^4.6.0",
"graphiql": "^3.9.0",
"graphql": "^16.0.0",
"graphql-ws": "^5.4.1",
+1 -1
View File
@@ -58,7 +58,7 @@ export const apiDocsTranslationRef: TranslationRef<
readonly 'providingComponentsCard.error.title': 'Could not load components';
readonly 'providingComponentsCard.title': 'Providers';
readonly 'providingComponentsCard.emptyContent.title': 'No component provides this API.';
readonly apisCardHelpLinkTitle: 'Learn how to change this';
readonly apisCardHelpLinkTitle: 'Learn how to change this.';
}
>;
+119 -48
View File
@@ -8,6 +8,7 @@ import { ApiRef } from '@backstage/frontend-plugin-api';
import { BackstagePlugin } from '@backstage/core-plugin-api';
import { CatalogTableRow } from '@backstage/plugin-catalog';
import { ComponentEntity } from '@backstage/catalog-model';
import { EntityColumnConfig } from '@backstage/plugin-catalog-react/alpha';
import { EntityListPagination } from '@backstage/plugin-catalog-react';
import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react';
import { ExternalRouteRef } from '@backstage/core-plugin-api';
@@ -94,20 +95,54 @@ export type AsyncApiResolver = {
};
// @public (undocumented)
export const ConsumedApisCard: (props: {
variant?: InfoCardVariants;
title?: string;
export const ConsumedApisCard: (
props: ConsumedApisCardProps | ConsumedApisCardLegacyProps,
) => JSX_2.Element;
// @public @deprecated
export interface ConsumedApisCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ApiEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
}) => JSX_2.Element;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export const ConsumingComponentsCard: (props: {
variant?: InfoCardVariants;
export interface ConsumedApisCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public (undocumented)
export const ConsumingComponentsCard: (
props: ConsumingComponentsCardProps | ConsumingComponentsCardLegacyProps,
) => JSX_2.Element;
// @public @deprecated
export interface ConsumingComponentsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ComponentEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
}) => JSX_2.Element;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface ConsumingComponentsCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public
export const DefaultApiExplorerPage: (
@@ -130,44 +165,29 @@ export function defaultDefinitionWidgets(): ApiDefinitionWidget[];
export const EntityApiDefinitionCard: () => JSX_2.Element;
// @public (undocumented)
export const EntityConsumedApisCard: (props: {
variant?: InfoCardVariants;
title?: string;
columns?: TableColumn<ApiEntity>[];
tableOptions?: TableOptions;
}) => JSX_2.Element;
export const EntityConsumedApisCard: (
props: ConsumedApisCardProps | ConsumedApisCardLegacyProps,
) => JSX_2.Element;
// @public (undocumented)
export const EntityConsumingComponentsCard: (props: {
variant?: InfoCardVariants;
title?: string;
columns?: TableColumn<ComponentEntity>[];
tableOptions?: TableOptions;
}) => JSX_2.Element;
export const EntityConsumingComponentsCard: (
props: ConsumingComponentsCardProps | ConsumingComponentsCardLegacyProps,
) => JSX_2.Element;
// @public (undocumented)
export const EntityHasApisCard: (props: {
variant?: InfoCardVariants;
title?: string;
columns?: TableColumn<ApiEntity>[];
tableOptions?: TableOptions;
}) => JSX_2.Element;
export const EntityHasApisCard: (
props: HasApisCardProps | HasApisCardLegacyProps,
) => JSX_2.Element;
// @public (undocumented)
export const EntityProvidedApisCard: (props: {
variant?: InfoCardVariants;
title?: string;
columns?: TableColumn<ApiEntity>[];
tableOptions?: TableOptions;
}) => JSX_2.Element;
export const EntityProvidedApisCard: (
props: ProvidedApisCardProps | ProvidedApisCardLegacyProps,
) => JSX_2.Element;
// @public (undocumented)
export const EntityProvidingComponentsCard: (props: {
variant?: InfoCardVariants;
title?: string;
columns?: TableColumn<ComponentEntity>[];
tableOptions?: TableOptions;
}) => JSX_2.Element;
export const EntityProvidingComponentsCard: (
props: ProvidingComponentsCardProps | ProvidingComponentsCardLegacyProps,
) => JSX_2.Element;
// @public (undocumented)
export const GraphQlDefinitionWidget: (
@@ -180,12 +200,29 @@ export type GraphQlDefinitionWidgetProps = {
};
// @public (undocumented)
export const HasApisCard: (props: {
variant?: InfoCardVariants;
title?: string;
export const HasApisCard: (
props: HasApisCardProps | HasApisCardLegacyProps,
) => JSX_2.Element;
// @public @deprecated
export interface HasApisCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ApiEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
}) => JSX_2.Element;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface HasApisCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public (undocumented)
export const OpenApiDefinitionWidget: (
@@ -211,20 +248,54 @@ export type PlainApiDefinitionWidgetProps = {
};
// @public (undocumented)
export const ProvidedApisCard: (props: {
variant?: InfoCardVariants;
title?: string;
export const ProvidedApisCard: (
props: ProvidedApisCardProps | ProvidedApisCardLegacyProps,
) => JSX_2.Element;
// @public @deprecated
export interface ProvidedApisCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ApiEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
}) => JSX_2.Element;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export const ProvidingComponentsCard: (props: {
variant?: InfoCardVariants;
export interface ProvidedApisCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public (undocumented)
export const ProvidingComponentsCard: (
props: ProvidingComponentsCardProps | ProvidingComponentsCardLegacyProps,
) => JSX_2.Element;
// @public @deprecated
export interface ProvidingComponentsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ComponentEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
}) => JSX_2.Element;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface ProvidingComponentsCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public (undocumented)
export const TrpcApiDefinitionWidget: (
@@ -60,7 +60,7 @@ describe('<ConsumedApisCard />', () => {
relations: [],
};
const { getByText, getByRole, container } = await renderInTestApp(
const { getByText, getByRole } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<ConsumedApisCard />
@@ -76,16 +76,11 @@ describe('<ConsumedApisCard />', () => {
expect(getByText(/Consumed APIs/i)).toBeInTheDocument();
expect(getByText(/does not consume any APIs/i)).toBeInTheDocument();
// Also render external link icon
const externalLink = getByRole('link');
expect(externalLink).toHaveAttribute(
'href',
'https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional',
);
const externalLinkIcon: HTMLElement | null = container.querySelector(
'svg[class*="externalLink"]',
);
expect(externalLink).toContainElement(externalLinkIcon);
});
it('shows consumed APIs', async () => {
@@ -21,7 +21,11 @@ import {
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import { getApiEntityColumns } from './presets';
import {
EntityRelationCard,
EntityColumnConfig,
} from '@backstage/plugin-catalog-react/alpha';
import { getApiEntityColumns, getApiEntityColumnConfig } from './presets';
import {
CodeSnippet,
InfoCard,
@@ -35,15 +39,34 @@ import {
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { apiDocsTranslationRef } from '../../translation';
/** @public */
export interface ConsumedApisCardProps {
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link ConsumedApisCardProps} instead.
* @public
*/
export const ConsumedApisCard = (props: {
variant?: InfoCardVariants;
export interface ConsumedApisCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ApiEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}) => {
}
function isLegacyProps(
props: ConsumedApisCardProps | ConsumedApisCardLegacyProps,
): props is ConsumedApisCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
function ConsumedApisCardLegacy(props: ConsumedApisCardLegacyProps) {
const { t } = useTranslationRef(apiDocsTranslationRef);
const {
variant = 'gridItem',
@@ -102,4 +125,38 @@ export const ConsumedApisCard = (props: {
entities={entities as ApiEntity[]}
/>
);
}
/**
* @public
*/
export const ConsumedApisCard = (
props: ConsumedApisCardProps | ConsumedApisCardLegacyProps,
) => {
const { t } = useTranslationRef(apiDocsTranslationRef);
const { entity } = useEntity();
if (isLegacyProps(props)) {
return <ConsumedApisCardLegacy {...props} />;
}
const {
title = t('consumedApisCard.title'),
columnConfig = getApiEntityColumnConfig(t),
} = props;
return (
<EntityRelationCard
title={title}
relationType={RELATION_CONSUMES_API}
columnConfig={columnConfig}
emptyState={{
message: t('consumedApisCard.emptyContent.title', {
entity: entity.kind.toLocaleLowerCase('en-US'),
}),
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional',
}}
/>
);
};
@@ -21,8 +21,12 @@ import {
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import {
EntityRelationCard,
EntityColumnConfig,
} from '@backstage/plugin-catalog-react/alpha';
import { useMemo } from 'react';
import { createSpecApiTypeColumn } from './presets';
import { createSpecApiTypeColumn, getHasApisColumnConfig } from './presets';
import {
CodeSnippet,
InfoCard,
@@ -36,15 +40,34 @@ import {
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { apiDocsTranslationRef } from '../../translation';
/** @public */
export interface HasApisCardProps {
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link HasApisCardProps} instead.
* @public
*/
export const HasApisCard = (props: {
variant?: InfoCardVariants;
export interface HasApisCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ApiEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}) => {
}
function isLegacyProps(
props: HasApisCardProps | HasApisCardLegacyProps,
): props is HasApisCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
function HasApisCardLegacy(props: HasApisCardLegacyProps) {
const { t } = useTranslationRef(apiDocsTranslationRef);
const presetColumns: TableColumn<ApiEntity>[] = useMemo(() => {
return [
@@ -110,4 +133,39 @@ export const HasApisCard = (props: {
entities={entities as ApiEntity[]}
/>
);
}
/**
* @public
*/
export const HasApisCard = (
props: HasApisCardProps | HasApisCardLegacyProps,
) => {
const { t } = useTranslationRef(apiDocsTranslationRef);
const { entity } = useEntity();
if (isLegacyProps(props)) {
return <HasApisCardLegacy {...props} />;
}
const {
title = t('hasApisCard.title'),
columnConfig = getHasApisColumnConfig(t),
} = props;
return (
<EntityRelationCard
title={title}
entityKind="API"
relationType={RELATION_HAS_PART}
columnConfig={columnConfig}
emptyState={{
message: t('hasApisCard.emptyContent.title', {
entity: entity.kind.toLocaleLowerCase('en-US'),
}),
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-api',
}}
/>
);
};
@@ -60,7 +60,7 @@ describe('<ProvidedApisCard />', () => {
relations: [],
};
const { getByText, getByRole, container } = await renderInTestApp(
const { getByText, getByRole } = await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<ProvidedApisCard />
@@ -77,16 +77,11 @@ describe('<ProvidedApisCard />', () => {
expect(getByText(/does not provide any APIs/i)).toBeInTheDocument();
expect(getByText(/Learn how to change this/)).toBeInTheDocument();
// Also render external link icon
const externalLink = getByRole('link');
expect(externalLink).toHaveAttribute(
'href',
'https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional',
);
const externalLinkIcon: HTMLElement | null = container.querySelector(
'svg[class*="externalLink"]',
);
expect(externalLink).toContainElement(externalLinkIcon);
});
it('shows consumed APIs', async () => {
@@ -21,7 +21,11 @@ import {
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import { getApiEntityColumns } from './presets';
import {
EntityRelationCard,
EntityColumnConfig,
} from '@backstage/plugin-catalog-react/alpha';
import { getApiEntityColumns, getApiEntityColumnConfig } from './presets';
import {
CodeSnippet,
InfoCard,
@@ -35,15 +39,34 @@ import {
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { apiDocsTranslationRef } from '../../translation';
/** @public */
export interface ProvidedApisCardProps {
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link ProvidedApisCardProps} instead.
* @public
*/
export const ProvidedApisCard = (props: {
variant?: InfoCardVariants;
export interface ProvidedApisCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ApiEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}) => {
}
function isLegacyProps(
props: ProvidedApisCardProps | ProvidedApisCardLegacyProps,
): props is ProvidedApisCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
function ProvidedApisCardLegacy(props: ProvidedApisCardLegacyProps) {
const { t } = useTranslationRef(apiDocsTranslationRef);
const {
variant = 'gridItem',
@@ -102,4 +125,38 @@ export const ProvidedApisCard = (props: {
entities={entities as ApiEntity[]}
/>
);
}
/**
* @public
*/
export const ProvidedApisCard = (
props: ProvidedApisCardProps | ProvidedApisCardLegacyProps,
) => {
const { t } = useTranslationRef(apiDocsTranslationRef);
const { entity } = useEntity();
if (isLegacyProps(props)) {
return <ProvidedApisCardLegacy {...props} />;
}
const {
title = t('providedApisCard.title'),
columnConfig = getApiEntityColumnConfig(t),
} = props;
return (
<EntityRelationCard
title={title}
relationType={RELATION_PROVIDES_API}
columnConfig={columnConfig}
emptyState={{
message: t('providedApisCard.emptyContent.title', {
entity: entity.kind.toLocaleLowerCase('en-US'),
}),
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional',
}}
/>
);
};
@@ -15,5 +15,14 @@
*/
export { ConsumedApisCard } from './ConsumedApisCard';
export type {
ConsumedApisCardLegacyProps,
ConsumedApisCardProps,
} from './ConsumedApisCard';
export { HasApisCard } from './HasApisCard';
export type { HasApisCardLegacyProps, HasApisCardProps } from './HasApisCard';
export { ProvidedApisCard } from './ProvidedApisCard';
export type {
ProvidedApisCardLegacyProps,
ProvidedApisCardProps,
} from './ProvidedApisCard';
@@ -17,8 +17,12 @@
import { ApiEntity } from '@backstage/catalog-model';
import { TableColumn } from '@backstage/core-components';
import { EntityTable } from '@backstage/plugin-catalog-react';
import ExtensionIcon from '@material-ui/icons/Extension';
import ToggleButton from '@material-ui/lab/ToggleButton';
import {
EntityColumnConfig,
entityDataTableColumns,
} from '@backstage/plugin-catalog-react/alpha';
import { ButtonIcon, Cell } from '@backstage/ui';
import { RiPuzzleLine } from '@remixicon/react';
import { useState } from 'react';
import { ApiTypeTitle } from '../ApiDefinitionCard';
import { ApiDefinitionDialog } from '../ApiDefinitionDialog';
@@ -28,6 +32,7 @@ import {
} from '@backstage/core-plugin-api/alpha';
import { apiDocsTranslationRef } from '../../translation';
/** @deprecated Use `getApiEntityColumnConfig` instead. */
export function createSpecApiTypeColumn(
t: TranslationFunction<typeof apiDocsTranslationRef.T>,
): TableColumn<ApiEntity> {
@@ -43,13 +48,13 @@ const ApiDefinitionButton = ({ apiEntity }: { apiEntity: ApiEntity }) => {
const { t } = useTranslationRef(apiDocsTranslationRef);
return (
<>
<ToggleButton
<ButtonIcon
aria-label={t('apiDefinitionDialog.toggleButtonAriaLabel')}
onClick={() => setDialogOpen(!dialogOpen)}
value={dialogOpen}
>
<ExtensionIcon />
</ToggleButton>
onPress={() => setDialogOpen(!dialogOpen)}
variant="tertiary"
size="small"
icon={<RiPuzzleLine />}
/>
<ApiDefinitionDialog
entity={apiEntity}
open={dialogOpen}
@@ -68,6 +73,7 @@ function createApiDefinitionColumn(
};
}
/** @deprecated Use `getApiEntityColumnConfig` instead. */
export const getApiEntityColumns = (
t: TranslationFunction<typeof apiDocsTranslationRef.T>,
): TableColumn<ApiEntity>[] => {
@@ -81,3 +87,62 @@ export const getApiEntityColumns = (
createApiDefinitionColumn(t),
];
};
// Column config presets
function createSpecApiTypeColumnConfig(
t: TranslationFunction<typeof apiDocsTranslationRef.T>,
): EntityColumnConfig {
return {
id: 'apiType',
label: t('apiEntityColumns.typeTitle'),
isSortable: true,
cell: entity => (
<Cell>
<ApiTypeTitle apiEntity={entity as unknown as ApiEntity} />
</Cell>
),
sortValue: entity =>
(entity.spec as Record<string, string> | undefined)?.type ?? '',
};
}
function createApiDefinitionColumnConfig(
t: TranslationFunction<typeof apiDocsTranslationRef.T>,
): EntityColumnConfig {
return {
id: 'apiDefinition',
label: t('apiEntityColumns.apiDefinitionTitle'),
cell: entity => (
<Cell>
<ApiDefinitionButton apiEntity={entity as unknown as ApiEntity} />
</Cell>
),
};
}
export function getApiEntityColumnConfig(
t: TranslationFunction<typeof apiDocsTranslationRef.T>,
): EntityColumnConfig[] {
return [
entityDataTableColumns.createEntityRefColumn({ defaultKind: 'API' }),
entityDataTableColumns.createSystemColumn(),
entityDataTableColumns.createOwnerColumn(),
createSpecApiTypeColumnConfig(t),
entityDataTableColumns.createSpecLifecycleColumn(),
entityDataTableColumns.createMetadataDescriptionColumn(),
createApiDefinitionColumnConfig(t),
];
}
export function getHasApisColumnConfig(
t: TranslationFunction<typeof apiDocsTranslationRef.T>,
): EntityColumnConfig[] {
return [
entityDataTableColumns.createEntityRefColumn({ defaultKind: 'API' }),
entityDataTableColumns.createOwnerColumn(),
createSpecApiTypeColumnConfig(t),
entityDataTableColumns.createSpecLifecycleColumn(),
entityDataTableColumns.createMetadataDescriptionColumn(),
];
}
@@ -24,6 +24,11 @@ import {
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
CodeSnippet,
InfoCard,
@@ -37,15 +42,36 @@ import {
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { apiDocsTranslationRef } from '../../translation';
/** @public */
export interface ConsumingComponentsCardProps {
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link ConsumingComponentsCardProps} instead.
* @public
*/
export const ConsumingComponentsCard = (props: {
variant?: InfoCardVariants;
export interface ConsumingComponentsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ComponentEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}) => {
}
function isLegacyProps(
props: ConsumingComponentsCardProps | ConsumingComponentsCardLegacyProps,
): props is ConsumingComponentsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
function ConsumingComponentsCardLegacy(
props: ConsumingComponentsCardLegacyProps,
) {
const { t } = useTranslationRef(apiDocsTranslationRef);
const {
variant = 'gridItem',
@@ -99,4 +125,35 @@ export const ConsumingComponentsCard = (props: {
entities={entities as ComponentEntity[]}
/>
);
}
/**
* @public
*/
export const ConsumingComponentsCard = (
props: ConsumingComponentsCardProps | ConsumingComponentsCardLegacyProps,
) => {
const { t } = useTranslationRef(apiDocsTranslationRef);
if (isLegacyProps(props)) {
return <ConsumingComponentsCardLegacy {...props} />;
}
const {
title = t('consumingComponentsCard.title'),
columnConfig = entityColumnPresets.component.columns,
} = props;
return (
<EntityRelationCard
title={title}
relationType={RELATION_API_CONSUMED_BY}
columnConfig={columnConfig}
emptyState={{
message: t('consumingComponentsCard.emptyContent.title'),
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#specconsumesapis-optional',
}}
/>
);
};
@@ -24,6 +24,11 @@ import {
useEntity,
useRelatedEntities,
} from '@backstage/plugin-catalog-react';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
CodeSnippet,
InfoCard,
@@ -38,12 +43,35 @@ import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { apiDocsTranslationRef } from '../../translation';
/** @public */
export const ProvidingComponentsCard = (props: {
variant?: InfoCardVariants;
export interface ProvidingComponentsCardProps {
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link ProvidingComponentsCardProps} instead.
* @public
*/
export interface ProvidingComponentsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ComponentEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}) => {
}
function isLegacyProps(
props: ProvidingComponentsCardProps | ProvidingComponentsCardLegacyProps,
): props is ProvidingComponentsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
function ProvidingComponentsCardLegacy(
props: ProvidingComponentsCardLegacyProps,
) {
const { t } = useTranslationRef(apiDocsTranslationRef);
const {
variant = 'gridItem',
@@ -97,4 +125,33 @@ export const ProvidingComponentsCard = (props: {
entities={entities as ComponentEntity[]}
/>
);
}
/** @public */
export const ProvidingComponentsCard = (
props: ProvidingComponentsCardProps | ProvidingComponentsCardLegacyProps,
) => {
const { t } = useTranslationRef(apiDocsTranslationRef);
if (isLegacyProps(props)) {
return <ProvidingComponentsCardLegacy {...props} />;
}
const {
title = t('providingComponentsCard.title'),
columnConfig = entityColumnPresets.component.columns,
} = props;
return (
<EntityRelationCard
title={title}
relationType={RELATION_API_PROVIDED_BY}
columnConfig={columnConfig}
emptyState={{
message: t('providingComponentsCard.emptyContent.title'),
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#specprovidesapis-optional',
}}
/>
);
};
@@ -15,4 +15,12 @@
*/
export { ConsumingComponentsCard } from './ConsumingComponentsCard';
export type {
ConsumingComponentsCardLegacyProps,
ConsumingComponentsCardProps,
} from './ConsumingComponentsCard';
export { ProvidingComponentsCard } from './ProvidingComponentsCard';
export type {
ProvidingComponentsCardLegacyProps,
ProvidingComponentsCardProps,
} from './ProvidingComponentsCard';
+1 -1
View File
@@ -90,6 +90,6 @@ export const apiDocsTranslationRef = createTranslationRef({
title: 'No component provides this API.',
},
},
apisCardHelpLinkTitle: 'Learn how to change this',
apisCardHelpLinkTitle: 'Learn how to change this.',
},
});
+97
View File
@@ -4,6 +4,7 @@
```ts
import { AnyRouteRefParams } from '@backstage/frontend-plugin-api';
import { ColumnConfig } from '@backstage/ui';
import { ComponentType } from 'react';
import { ConfigurableExtensionDataRef } from '@backstage/frontend-plugin-api';
import { Entity } from '@backstage/catalog-model';
@@ -13,11 +14,13 @@ import { ExtensionDefinition } from '@backstage/frontend-plugin-api';
import { FilterPredicate } from '@backstage/filter-predicates';
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 { ReactElement } from 'react';
import { ReactNode } from 'react';
import { ResourcePermission } from '@backstage/plugin-permission-common';
import { RouteRef } from '@backstage/frontend-plugin-api';
import { TableItem } from '@backstage/ui';
import { TranslationRef } from '@backstage/frontend-plugin-api';
// @alpha
@@ -119,6 +122,7 @@ export const catalogReactTranslationRef: TranslationRef<
readonly 'entityTableColumnTitle.owner': 'Owner';
readonly 'entityTableColumnTitle.lifecycle': 'Lifecycle';
readonly 'entityTableColumnTitle.targets': 'Targets';
readonly 'entityRelationCard.emptyHelpLinkTitle': 'Learn how to change this.';
readonly 'missingAnnotationEmptyState.title': 'Missing Annotation';
readonly 'missingAnnotationEmptyState.readMore': 'Read more';
readonly 'missingAnnotationEmptyState.annotationYaml': 'Add the annotation to your {{entityKind}} YAML as shown in the highlighted example below:';
@@ -241,6 +245,32 @@ export const EntityCardBlueprint: ExtensionBlueprint<{
// @alpha (undocumented)
export type EntityCardType = 'info' | 'content';
// @public (undocumented)
export interface EntityColumnConfig extends ColumnConfig<EntityRow> {
// (undocumented)
sortValue?: (entity: EntityRow) => string;
}
// @alpha (undocumented)
export const entityColumnPresets: {
readonly component: {
readonly columns: EntityColumnConfig[];
readonly helpLink: 'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component';
};
readonly resource: {
readonly columns: EntityColumnConfig[];
readonly helpLink: 'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-resource';
};
readonly system: {
readonly columns: EntityColumnConfig[];
readonly helpLink: 'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system';
};
readonly domain: {
readonly columns: EntityColumnConfig[];
readonly helpLink: 'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-domain';
};
};
// @alpha
export const EntityContentBlueprint: ExtensionBlueprint<{
kind: 'entity-content';
@@ -450,6 +480,46 @@ export type EntityContextMenuItemParams = {
filter?: FilterPredicate | ((entity: Entity) => boolean);
};
// @public (undocumented)
export function EntityDataTable(props: EntityDataTableProps): JSX_3.Element;
// @public (undocumented)
export const entityDataTableColumns: Readonly<{
createEntityRefColumn(options: {
defaultKind?: string;
isRowHeader?: boolean;
}): EntityColumnConfig;
createEntityRelationColumn(options: {
id: string;
translationKey: 'owner' | 'system' | 'domain';
relation: string;
defaultKind?: string;
filter?: {
kind: string;
};
}): EntityColumnConfig;
createOwnerColumn(): EntityColumnConfig;
createSystemColumn(): EntityColumnConfig;
createDomainColumn(): EntityColumnConfig;
createMetadataDescriptionColumn(): EntityColumnConfig;
createSpecTypeColumn(): EntityColumnConfig;
createSpecLifecycleColumn(): EntityColumnConfig;
}>;
// @public (undocumented)
export interface EntityDataTableProps {
// (undocumented)
columnConfig: EntityColumnConfig[];
// (undocumented)
data: Entity[];
// (undocumented)
emptyState?: ReactNode;
// (undocumented)
error?: Error;
// (undocumented)
loading?: boolean;
}
// @alpha (undocumented)
export const EntityHeaderBlueprint: ExtensionBlueprint<{
kind: 'entity-header';
@@ -557,6 +627,33 @@ export const EntityIconLinkBlueprint: ExtensionBlueprint<{
};
}>;
// @public (undocumented)
export function EntityRelationCard(
props: EntityRelationCardProps,
): JSX_3.Element;
// @public (undocumented)
export interface EntityRelationCardProps {
// (undocumented)
className?: string;
// (undocumented)
columnConfig: EntityColumnConfig[];
// (undocumented)
emptyState?: {
message: string;
helpLink?: string;
};
// (undocumented)
entityKind?: string;
// (undocumented)
relationType: string;
// (undocumented)
title: string;
}
// @public (undocumented)
export type EntityRow = Entity & TableItem;
// @alpha (undocumented)
export const EntityTableColumnTitle: (
input: EntityTableColumnTitleProps,
+2
View File
@@ -20,3 +20,5 @@ export { catalogReactTranslationRef } from '../translation';
export { isOwnerOf } from '../utils/isOwnerOf';
export { useEntityPermission } from '../hooks/useEntityPermission';
export * from '../components/EntityTable/TitleColumn';
export * from '../components/EntityDataTable';
export * from '../components/EntityRelationCard';
@@ -0,0 +1,160 @@
/*
* 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 { Entity } from '@backstage/catalog-model';
import { renderInTestApp } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { CellText } from '@backstage/ui';
import { EntityDataTable } from './EntityDataTable';
import { EntityColumnConfig } from './columnFactories';
import { entityRouteRef } from '../../routes';
const columns: EntityColumnConfig[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
isSortable: true,
cell: entity => <CellText title={entity.metadata.name} />,
sortValue: entity => entity.metadata.name,
},
{
id: 'description',
label: 'Description',
cell: entity => <CellText title={entity.metadata.description ?? ''} />,
},
];
const entities: Entity[] = [
{
apiVersion: 'v1',
kind: 'Component',
metadata: { name: 'bravo', namespace: 'default', description: 'Second' },
},
{
apiVersion: 'v1',
kind: 'Component',
metadata: { name: 'alpha', namespace: 'default', description: 'First' },
},
];
describe('<EntityDataTable />', () => {
it('renders entity data in a table', async () => {
await renderInTestApp(
<EntityDataTable columnConfig={columns} data={entities} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(screen.getByText('bravo')).toBeInTheDocument();
expect(screen.getByText('alpha')).toBeInTheDocument();
expect(screen.getByText('Second')).toBeInTheDocument();
expect(screen.getByText('First')).toBeInTheDocument();
});
it('renders empty state when data is empty', async () => {
await renderInTestApp(
<EntityDataTable
columnConfig={columns}
data={[]}
emptyState={<div>No entities found</div>}
/>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(screen.getByText('No entities found')).toBeInTheDocument();
});
it('renders error state', async () => {
await renderInTestApp(
<EntityDataTable
columnConfig={columns}
data={[]}
error={new Error('Something went wrong')}
/>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(screen.getByText(/Something went wrong/)).toBeInTheDocument();
});
it('sorts data when clicking a sortable column header', async () => {
await renderInTestApp(
<EntityDataTable columnConfig={columns} data={entities} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
const rows = () => screen.getAllByRole('row').slice(1); // skip header row
// Initial order: bravo, alpha (insertion order)
expect(rows()[0]).toHaveTextContent('bravo');
expect(rows()[1]).toHaveTextContent('alpha');
// Click Name header to sort ascending
await userEvent.click(screen.getByText('Name'));
expect(rows()[0]).toHaveTextContent('alpha');
expect(rows()[1]).toHaveTextContent('bravo');
// Click again to sort descending
await userEvent.click(screen.getByText('Name'));
expect(rows()[0]).toHaveTextContent('bravo');
expect(rows()[1]).toHaveTextContent('alpha');
});
it('does not sort when column has no sortValue', async () => {
const unsortableColumns: EntityColumnConfig[] = [
{
id: 'name',
label: 'Name',
isRowHeader: true,
cell: entity => <CellText title={entity.metadata.name} />,
},
];
await renderInTestApp(
<EntityDataTable columnConfig={unsortableColumns} data={entities} />,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
const rows = () => screen.getAllByRole('row').slice(1);
// Should maintain insertion order
expect(rows()[0]).toHaveTextContent('bravo');
expect(rows()[1]).toHaveTextContent('alpha');
});
});
@@ -0,0 +1,77 @@
/*
* 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 { Entity, stringifyEntityRef } from '@backstage/catalog-model';
import { Table, useTable } from '@backstage/ui';
import { useCallback, useMemo, ReactNode } from 'react';
import { EntityRow, EntityColumnConfig } from './columnFactories';
import { SortDescriptor } from '@backstage/ui';
/** @public */
export interface EntityDataTableProps {
columnConfig: EntityColumnConfig[];
data: Entity[];
loading?: boolean;
error?: Error;
emptyState?: ReactNode;
}
/** @public */
export function EntityDataTable(props: EntityDataTableProps) {
const { columnConfig, data, loading, error, emptyState } = props;
const tableData: EntityRow[] = useMemo(
() =>
data.map(entity => ({
...entity,
id: stringifyEntityRef(entity),
})),
[data],
);
const sortFn = useCallback(
(items: EntityRow[], sort: SortDescriptor) => {
const column = columnConfig.find(c => c.id === sort.column);
if (!column?.sortValue) {
return items;
}
const getValue = column.sortValue;
const direction = sort.direction === 'descending' ? -1 : 1;
return [...items].sort(
(a, b) => getValue(a).localeCompare(getValue(b)) * direction,
);
},
[columnConfig],
);
const { tableProps } = useTable({
mode: 'complete',
data: tableData,
sortFn,
paginationOptions: { pageSize: tableData.length || 1 },
});
return (
<Table
columnConfig={columnConfig}
{...tableProps}
loading={loading}
error={error}
emptyState={emptyState}
pagination={{ type: 'none' }}
/>
);
}
@@ -0,0 +1,193 @@
/*
* 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 {
Entity,
RELATION_OWNED_BY,
RELATION_PART_OF,
} from '@backstage/catalog-model';
import { Cell, CellText, Column, ColumnConfig, TableItem } from '@backstage/ui';
import {
EntityRefLink,
EntityRefLinks,
humanizeEntityRef,
} from '../EntityRefLink';
import { EntityTableColumnTitle } from '../EntityTable/TitleColumn';
import { getEntityRelations } from '../../utils';
/** @public */
export type EntityRow = Entity & TableItem;
/** @public */
export interface EntityColumnConfig extends ColumnConfig<EntityRow> {
sortValue?: (entity: EntityRow) => string;
}
/** @public */
export const columnFactories = Object.freeze({
createEntityRefColumn(options: {
defaultKind?: string;
isRowHeader?: boolean;
}): EntityColumnConfig {
const isRowHeader = options.isRowHeader ?? true;
return {
id: 'name',
label: 'Name',
header: () => (
<Column id="name" isRowHeader={isRowHeader} allowsSorting>
<EntityTableColumnTitle translationKey="name" />
</Column>
),
isRowHeader,
isSortable: true,
cell: entity => (
<Cell>
<EntityRefLink
entityRef={entity}
defaultKind={options.defaultKind}
title={entity.metadata?.title}
/>
</Cell>
),
sortValue: entity =>
entity.metadata?.title ||
humanizeEntityRef(entity, { defaultKind: options.defaultKind }),
};
},
createEntityRelationColumn(options: {
id: string;
translationKey: 'owner' | 'system' | 'domain';
relation: string;
defaultKind?: string;
filter?: { kind: string };
}): EntityColumnConfig {
return {
id: options.id,
label: options.id.charAt(0).toUpperCase() + options.id.slice(1),
header: () => (
<Column id={options.id} allowsSorting>
<EntityTableColumnTitle translationKey={options.translationKey} />
</Column>
),
isSortable: true,
cell: entity => (
<Cell>
<EntityRefLinks
entityRefs={getEntityRelations(
entity,
options.relation,
options.filter,
)}
defaultKind={options.defaultKind}
/>
</Cell>
),
sortValue: entity =>
getEntityRelations(entity, options.relation, options.filter)
.map(r => humanizeEntityRef(r, { defaultKind: options.defaultKind }))
.join(', '),
};
},
createOwnerColumn(): EntityColumnConfig {
return columnFactories.createEntityRelationColumn({
id: 'owner',
translationKey: 'owner',
relation: RELATION_OWNED_BY,
defaultKind: 'group',
});
},
createSystemColumn(): EntityColumnConfig {
return columnFactories.createEntityRelationColumn({
id: 'system',
translationKey: 'system',
relation: RELATION_PART_OF,
defaultKind: 'system',
filter: { kind: 'system' },
});
},
createDomainColumn(): EntityColumnConfig {
return columnFactories.createEntityRelationColumn({
id: 'domain',
translationKey: 'domain',
relation: RELATION_PART_OF,
defaultKind: 'domain',
filter: { kind: 'domain' },
});
},
createMetadataDescriptionColumn(): EntityColumnConfig {
return {
id: 'description',
label: 'Description',
header: () => (
<Column id="description" allowsSorting>
<EntityTableColumnTitle translationKey="description" />
</Column>
),
isSortable: true,
cell: entity => <CellText title={entity.metadata.description ?? ''} />,
sortValue: entity => entity.metadata.description ?? '',
};
},
createSpecTypeColumn(): EntityColumnConfig {
return {
id: 'type',
label: 'Type',
header: () => (
<Column id="type" allowsSorting>
<EntityTableColumnTitle translationKey="type" />
</Column>
),
isSortable: true,
cell: entity => (
<CellText
title={
(entity.spec as Record<string, string> | undefined)?.type ?? ''
}
/>
),
sortValue: entity =>
(entity.spec as Record<string, string> | undefined)?.type ?? '',
};
},
createSpecLifecycleColumn(): EntityColumnConfig {
return {
id: 'lifecycle',
label: 'Lifecycle',
header: () => (
<Column id="lifecycle" allowsSorting>
<EntityTableColumnTitle translationKey="lifecycle" />
</Column>
),
isSortable: true,
cell: entity => (
<CellText
title={
(entity.spec as Record<string, string> | undefined)?.lifecycle ?? ''
}
/>
),
sortValue: entity =>
(entity.spec as Record<string, string> | undefined)?.lifecycle ?? '',
};
},
});
@@ -0,0 +1,21 @@
/*
* 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.
*/
export { EntityDataTable } from './EntityDataTable';
export type { EntityDataTableProps } from './EntityDataTable';
export { columnFactories as entityDataTableColumns } from './columnFactories';
export type { EntityRow, EntityColumnConfig } from './columnFactories';
export { entityColumnPresets } from './presets';
@@ -0,0 +1,61 @@
/*
* 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 { columnFactories, EntityColumnConfig } from './columnFactories';
/** @alpha */
export const entityColumnPresets = {
component: {
columns: [
columnFactories.createEntityRefColumn({ defaultKind: 'component' }),
columnFactories.createOwnerColumn(),
columnFactories.createSpecTypeColumn(),
columnFactories.createSpecLifecycleColumn(),
columnFactories.createMetadataDescriptionColumn(),
] as EntityColumnConfig[],
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-component',
},
resource: {
columns: [
columnFactories.createEntityRefColumn({ defaultKind: 'resource' }),
columnFactories.createOwnerColumn(),
columnFactories.createSpecTypeColumn(),
columnFactories.createSpecLifecycleColumn(),
columnFactories.createMetadataDescriptionColumn(),
] as EntityColumnConfig[],
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-resource',
},
system: {
columns: [
columnFactories.createEntityRefColumn({ defaultKind: 'system' }),
columnFactories.createOwnerColumn(),
columnFactories.createMetadataDescriptionColumn(),
] as EntityColumnConfig[],
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-system',
},
domain: {
columns: [
columnFactories.createEntityRefColumn({ defaultKind: 'domain' }),
columnFactories.createOwnerColumn(),
columnFactories.createMetadataDescriptionColumn(),
] as EntityColumnConfig[],
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#kind-domain',
},
} as const;
@@ -0,0 +1,194 @@
/*
* 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 { Entity, RELATION_HAS_PART } from '@backstage/catalog-model';
import {
catalogApiRef,
EntityProvider,
entityRouteRef,
} from '@backstage/plugin-catalog-react';
import { renderInTestApp, TestApiProvider } from '@backstage/test-utils';
import { screen } from '@testing-library/react';
import { ComponentType, PropsWithChildren, ReactNode } from 'react';
import { EntityRelationCard } from './EntityRelationCard';
import { entityColumnPresets } from '../EntityDataTable/presets';
import { catalogApiMock } from '../../testUtils/catalogApiMock';
describe('<EntityRelationCard />', () => {
const catalogApi = catalogApiMock.mock();
let Wrapper: ComponentType<PropsWithChildren<{}>>;
beforeEach(() => {
Wrapper = ({ children }: { children?: ReactNode }) => (
<TestApiProvider apis={[[catalogApiRef, catalogApi]]}>
{children}
</TestApiProvider>
);
});
afterEach(() => jest.resetAllMocks());
it('renders the card title', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'System',
metadata: { name: 'my-system', namespace: 'my-namespace' },
relations: [],
};
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<EntityRelationCard
title="Has components"
relationType={RELATION_HAS_PART}
entityKind="Component"
columnConfig={entityColumnPresets.component.columns}
/>
</EntityProvider>
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(screen.getByText('Has components')).toBeInTheDocument();
});
it('shows empty state message and help link when no related entities', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'System',
metadata: { name: 'my-system', namespace: 'my-namespace' },
relations: [],
};
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<EntityRelationCard
title="Has components"
relationType={RELATION_HAS_PART}
entityKind="Component"
columnConfig={entityColumnPresets.component.columns}
emptyState={{
message: 'No component is part of this system.',
helpLink: 'https://backstage.io/docs',
}}
/>
</EntityProvider>
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(
screen.getByText(/No component is part of this system/),
).toBeInTheDocument();
const link = screen.getByRole('link');
expect(link).toHaveAttribute('href', 'https://backstage.io/docs');
});
it('shows related entities in the table', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'System',
metadata: { name: 'my-system', namespace: 'my-namespace' },
relations: [
{
targetRef: 'component:my-namespace/my-component',
type: RELATION_HAS_PART,
},
],
};
catalogApi.getEntitiesByRefs.mockResolvedValue({
items: [
{
apiVersion: 'v1',
kind: 'Component',
metadata: { name: 'my-component', namespace: 'my-namespace' },
spec: { type: 'service', lifecycle: 'production' },
},
],
});
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<EntityRelationCard
title="Has components"
relationType={RELATION_HAS_PART}
entityKind="Component"
columnConfig={entityColumnPresets.component.columns}
/>
</EntityProvider>
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(screen.getByText('Has components')).toBeInTheDocument();
expect(await screen.findByText(/my-component/)).toBeInTheDocument();
});
it('shows error state when loading fails', async () => {
const entity: Entity = {
apiVersion: 'v1',
kind: 'System',
metadata: { name: 'my-system', namespace: 'my-namespace' },
relations: [
{
targetRef: 'component:my-namespace/my-component',
type: RELATION_HAS_PART,
},
],
};
catalogApi.getEntitiesByRefs.mockRejectedValue(
new Error('Request failed with 500'),
);
await renderInTestApp(
<Wrapper>
<EntityProvider entity={entity}>
<EntityRelationCard
title="Has components"
relationType={RELATION_HAS_PART}
entityKind="Component"
columnConfig={entityColumnPresets.component.columns}
/>
</EntityProvider>
</Wrapper>,
{
mountedRoutes: {
'/catalog/:namespace/:kind/:name': entityRouteRef,
},
},
);
expect(screen.getByText('Has components')).toBeInTheDocument();
expect(
await screen.findByText(/Request failed with 500/),
).toBeInTheDocument();
});
});
@@ -0,0 +1,85 @@
/*
* 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 { Alert, Link, Text } from '@backstage/ui';
import { useTranslationRef } from '@backstage/frontend-plugin-api';
import { EntityInfoCard } from '../EntityInfoCard';
import { EntityDataTable } from '../EntityDataTable';
import { EntityColumnConfig } from '../EntityDataTable/columnFactories';
import { useEntity, useRelatedEntities } from '../../hooks';
import { catalogReactTranslationRef } from '../../translation';
/** @public */
export interface EntityRelationCardProps {
title: string;
relationType: string;
entityKind?: string;
columnConfig: EntityColumnConfig[];
emptyState?: {
message: string;
helpLink?: string;
};
className?: string;
}
/** @public */
export function EntityRelationCard(props: EntityRelationCardProps) {
const {
title,
relationType,
entityKind,
columnConfig,
emptyState,
className,
} = props;
const { t } = useTranslationRef(catalogReactTranslationRef);
const { entity } = useEntity();
const { entities, loading, error } = useRelatedEntities(entity, {
type: relationType,
kind: entityKind,
});
return (
<EntityInfoCard title={title} className={className}>
{error ? (
<Alert status="warning" icon title={error.message} role="status" />
) : (
<EntityDataTable
columnConfig={columnConfig}
data={entities ?? []}
loading={loading}
emptyState={
emptyState && (
<Text as="p">
{emptyState.message}{' '}
{emptyState.helpLink && (
<Link
href={emptyState.helpLink}
target="_blank"
rel="noopener noreferrer"
variant="body-medium"
>
{t('entityRelationCard.emptyHelpLinkTitle')}
</Link>
)}
</Text>
)
}
/>
)}
</EntityInfoCard>
);
}
@@ -0,0 +1,18 @@
/*
* 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.
*/
export { EntityRelationCard } from './EntityRelationCard';
export type { EntityRelationCardProps } from './EntityRelationCard';
+3
View File
@@ -168,6 +168,9 @@ export const catalogReactTranslationRef = createTranslationRef({
label: 'Label',
domain: 'Domain',
},
entityRelationCard: {
emptyHelpLinkTitle: 'Learn how to change this.',
},
missingAnnotationEmptyState: {
title: 'Missing Annotation',
readMore: 'Read more',
+9 -9
View File
@@ -83,11 +83,11 @@ export const catalogTranslationRef: TranslationRef<
readonly 'catalogTable.starActionTitle': 'Add to favorites';
readonly 'catalogTable.unStarActionTitle': 'Remove from favorites';
readonly 'dependencyOfComponentsCard.title': 'Dependency of components';
readonly 'dependencyOfComponentsCard.emptyMessage': 'No component depends on this component';
readonly 'dependencyOfComponentsCard.emptyMessage': 'No component depends on this component.';
readonly 'dependsOnComponentsCard.title': 'Depends on components';
readonly 'dependsOnComponentsCard.emptyMessage': 'No component is a dependency of this component';
readonly 'dependsOnComponentsCard.emptyMessage': 'No component is a dependency of this component.';
readonly 'dependsOnResourcesCard.title': 'Depends on resources';
readonly 'dependsOnResourcesCard.emptyMessage': 'No resource is a dependency of this component';
readonly 'dependsOnResourcesCard.emptyMessage': 'No resource is a dependency of this component.';
readonly 'entityContextMenu.copiedMessage': 'Copied!';
readonly 'entityContextMenu.moreButtonTitle': 'More';
readonly 'entityContextMenu.inspectMenuTitle': 'Inspect entity';
@@ -112,16 +112,16 @@ export const catalogTranslationRef: TranslationRef<
readonly entityProcessingErrorsDescription: 'The error below originates from';
readonly entityRelationWarningDescription: "This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: ";
readonly 'hasComponentsCard.title': 'Has components';
readonly 'hasComponentsCard.emptyMessage': 'No component is part of this system';
readonly 'hasComponentsCard.emptyMessage': 'No component is part of this system.';
readonly 'hasResourcesCard.title': 'Has resources';
readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system';
readonly 'hasResourcesCard.emptyMessage': 'No resource is part of this system.';
readonly 'hasSubcomponentsCard.title': 'Has subcomponents';
readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component';
readonly 'hasSubcomponentsCard.emptyMessage': 'No subcomponent is part of this component.';
readonly 'hasSubdomainsCard.title': 'Has subdomains';
readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain';
readonly 'hasSubdomainsCard.emptyMessage': 'No subdomain is part of this domain.';
readonly 'hasSystemsCard.title': 'Has systems';
readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain';
readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this';
readonly 'hasSystemsCard.emptyMessage': 'No system is part of this domain.';
readonly 'relatedEntitiesCard.emptyHelpLinkTitle': 'Learn how to change this.';
readonly 'systemDiagramCard.title': 'System Diagram';
readonly 'systemDiagramCard.description': 'Use pinch & zoom to move around the diagram.';
readonly 'systemDiagramCard.edgeLabels.dependsOn': 'depends on';
+96 -29
View File
@@ -11,6 +11,7 @@ import { CompoundEntityRef } from '@backstage/catalog-model';
import { DomainEntity } from '@backstage/catalog-model';
import { ElementType } from 'react';
import { Entity } from '@backstage/catalog-model';
import { EntityColumnConfig } from '@backstage/plugin-catalog-react/alpha';
import { EntityListContextProps } from '@backstage/plugin-catalog-react';
import { EntityListPagination } from '@backstage/plugin-catalog-react';
import { EntityOwnerPickerProps } from '@backstage/plugin-catalog-react';
@@ -290,40 +291,64 @@ export class DefaultStarredEntitiesApi implements StarredEntitiesApi {
toggleStarred(entityRef: string): Promise<void>;
}
// @public (undocumented)
export interface DependencyOfComponentsCardProps {
// (undocumented)
// @public @deprecated
export interface DependencyOfComponentsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ComponentEntity>[];
// (undocumented)
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface DependencyOfComponentsCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public @deprecated
export interface DependsOnComponentsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ComponentEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface DependsOnComponentsCardProps {
// (undocumented)
columns?: TableColumn<ComponentEntity>[];
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public @deprecated
export interface DependsOnResourcesCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ResourceEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// (undocumented)
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface DependsOnResourcesCardProps {
// (undocumented)
columns?: TableColumn<ResourceEntity>[];
// (undocumented)
tableOptions?: TableOptions;
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
// (undocumented)
variant?: InfoCardVariants;
}
// @public
@@ -502,18 +527,26 @@ export function hasCatalogProcessingErrors(
},
): Promise<boolean>;
// @public (undocumented)
export interface HasComponentsCardProps {
// (undocumented)
// @public @deprecated
export interface HasComponentsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ComponentEntity>[];
// (undocumented)
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// (undocumented)
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface HasComponentsCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public
export function hasLabels(entity: Entity): boolean;
@@ -525,54 +558,88 @@ export function hasRelationWarnings(
},
): Promise<boolean>;
// @public (undocumented)
export interface HasResourcesCardProps {
// (undocumented)
// @public @deprecated
export interface HasResourcesCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ResourceEntity>[];
// (undocumented)
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface HasResourcesCardProps {
// (undocumented)
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public @deprecated
export interface HasSubcomponentsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<ComponentEntity>[];
// (undocumented)
kind?: string;
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface HasSubcomponentsCardProps {
// (undocumented)
columns?: TableColumn<ComponentEntity>[];
columnConfig?: EntityColumnConfig[];
// (undocumented)
kind?: string;
// (undocumented)
title?: string;
}
// @public @deprecated
export interface HasSubdomainsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<DomainEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// (undocumented)
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface HasSubdomainsCardProps {
// (undocumented)
columns?: TableColumn<DomainEntity>[];
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
}
// @public @deprecated
export interface HasSystemsCardLegacyProps {
// @deprecated (undocumented)
columns?: TableColumn<SystemEntity>[];
// @deprecated (undocumented)
tableOptions?: TableOptions;
// (undocumented)
title?: string;
// (undocumented)
// @deprecated (undocumented)
variant?: InfoCardVariants;
}
// @public (undocumented)
export interface HasSystemsCardProps {
// (undocumented)
columns?: TableColumn<SystemEntity>[];
// (undocumented)
tableOptions?: TableOptions;
columnConfig?: EntityColumnConfig[];
// (undocumented)
title?: string;
// (undocumented)
variant?: InfoCardVariants;
}
// @public
+7 -9
View File
@@ -100,7 +100,7 @@ export const catalogDependsOnComponentsEntityCard = EntityCardBlueprint.make({
filter: { kind: 'component' },
loader: async () =>
import('../components/DependsOnComponentsCard').then(m => (
<m.DependsOnComponentsCard variant="gridItem" />
<m.DependsOnComponentsCard />
)),
},
});
@@ -111,7 +111,7 @@ export const catalogDependsOnResourcesEntityCard = EntityCardBlueprint.make({
filter: { kind: 'component' },
loader: async () =>
import('../components/DependsOnResourcesCard').then(m => (
<m.DependsOnResourcesCard variant="gridItem" />
<m.DependsOnResourcesCard />
)),
},
});
@@ -122,7 +122,7 @@ export const catalogHasComponentsEntityCard = EntityCardBlueprint.make({
filter: { kind: 'system' },
loader: async () =>
import('../components/HasComponentsCard').then(m => (
<m.HasComponentsCard variant="gridItem" />
<m.HasComponentsCard />
)),
},
});
@@ -133,7 +133,7 @@ export const catalogHasResourcesEntityCard = EntityCardBlueprint.make({
filter: { kind: 'system' },
loader: async () =>
import('../components/HasResourcesCard').then(m => (
<m.HasResourcesCard variant="gridItem" />
<m.HasResourcesCard />
)),
},
});
@@ -144,7 +144,7 @@ export const catalogHasSubcomponentsEntityCard = EntityCardBlueprint.make({
filter: { kind: 'component' },
loader: async () =>
import('../components/HasSubcomponentsCard').then(m => (
<m.HasSubcomponentsCard variant="gridItem" />
<m.HasSubcomponentsCard />
)),
},
});
@@ -155,7 +155,7 @@ export const catalogHasSubdomainsEntityCard = EntityCardBlueprint.make({
filter: { kind: 'domain' },
loader: async () =>
import('../components/HasSubdomainsCard').then(m => (
<m.HasSubdomainsCard variant="gridItem" />
<m.HasSubdomainsCard />
)),
},
});
@@ -165,9 +165,7 @@ export const catalogHasSystemsEntityCard = EntityCardBlueprint.make({
params: {
filter: { kind: 'domain' },
loader: async () =>
import('../components/HasSystemsCard').then(m => (
<m.HasSystemsCard variant="gridItem" />
)),
import('../components/HasSystemsCard').then(m => <m.HasSystemsCard />),
},
});
+9 -9
View File
@@ -91,15 +91,15 @@ export const catalogTranslationRef = createTranslationRef({
},
dependencyOfComponentsCard: {
title: 'Dependency of components',
emptyMessage: 'No component depends on this component',
emptyMessage: 'No component depends on this component.',
},
dependsOnComponentsCard: {
title: 'Depends on components',
emptyMessage: 'No component is a dependency of this component',
emptyMessage: 'No component is a dependency of this component.',
},
dependsOnResourcesCard: {
title: 'Depends on resources',
emptyMessage: 'No resource is a dependency of this component',
emptyMessage: 'No resource is a dependency of this component.',
},
entityContextMenu: {
copiedMessage: 'Copied!',
@@ -150,26 +150,26 @@ export const catalogTranslationRef = createTranslationRef({
"This entity has relations to other entities, which can't be found in the catalog.\n Entities not found are: ",
hasComponentsCard: {
title: 'Has components',
emptyMessage: 'No component is part of this system',
emptyMessage: 'No component is part of this system.',
},
hasResourcesCard: {
title: 'Has resources',
emptyMessage: 'No resource is part of this system',
emptyMessage: 'No resource is part of this system.',
},
hasSubcomponentsCard: {
title: 'Has subcomponents',
emptyMessage: 'No subcomponent is part of this component',
emptyMessage: 'No subcomponent is part of this component.',
},
hasSubdomainsCard: {
title: 'Has subdomains',
emptyMessage: 'No subdomain is part of this domain',
emptyMessage: 'No subdomain is part of this domain.',
},
hasSystemsCard: {
title: 'Has systems',
emptyMessage: 'No system is part of this domain',
emptyMessage: 'No system is part of this domain.',
},
relatedEntitiesCard: {
emptyHelpLinkTitle: 'Learn how to change this',
emptyHelpLinkTitle: 'Learn how to change this.',
},
systemDiagramCard: {
title: 'System Diagram',
@@ -23,10 +23,15 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asComponentEntities,
componentEntityColumns,
componentEntityHelpLink,
componentEntityHelpLink as legacyHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
import { catalogTranslationRef } from '../../alpha/translation';
@@ -34,33 +39,76 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface DependencyOfComponentsCardProps {
variant?: InfoCardVariants;
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link DependencyOfComponentsCardProps} instead.
* @public
*/
export interface DependencyOfComponentsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ComponentEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}
function isLegacyProps(
props:
| DependencyOfComponentsCardProps
| DependencyOfComponentsCardLegacyProps,
): props is DependencyOfComponentsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function DependencyOfComponentsCard(
props: DependencyOfComponentsCardProps,
props:
| DependencyOfComponentsCardProps
| DependencyOfComponentsCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('dependencyOfComponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind="Component"
relationType={RELATION_DEPENDENCY_OF}
columns={columns}
emptyMessage={t('dependencyOfComponentsCard.emptyMessage')}
emptyHelpLink={legacyHelpLink}
asRenderableEntities={asComponentEntities}
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('dependencyOfComponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.component.columns,
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind="Component"
relationType={RELATION_DEPENDENCY_OF}
columns={columns}
emptyMessage={t('dependencyOfComponentsCard.emptyMessage')}
emptyHelpLink={componentEntityHelpLink}
asRenderableEntities={asComponentEntities}
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('dependencyOfComponentsCard.emptyMessage'),
helpLink: entityColumnPresets.component.helpLink,
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { DependencyOfComponentsCard } from './DependencyOfComponentsCard';
export type { DependencyOfComponentsCardProps } from './DependencyOfComponentsCard';
export type {
DependencyOfComponentsCardLegacyProps,
DependencyOfComponentsCardProps,
} from './DependencyOfComponentsCard';
@@ -20,10 +20,15 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asComponentEntities,
componentEntityColumns,
componentEntityHelpLink,
componentEntityHelpLink as legacyHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
import { catalogTranslationRef } from '../../alpha/translation';
@@ -31,31 +36,72 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface DependsOnComponentsCardProps {
variant?: InfoCardVariants;
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link DependsOnComponentsCardProps} instead.
* @public
*/
export interface DependsOnComponentsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ComponentEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}
export function DependsOnComponentsCard(props: DependsOnComponentsCardProps) {
function isLegacyProps(
props: DependsOnComponentsCardProps | DependsOnComponentsCardLegacyProps,
): props is DependsOnComponentsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function DependsOnComponentsCard(
props: DependsOnComponentsCardProps | DependsOnComponentsCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('dependsOnComponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind="Component"
relationType={RELATION_DEPENDS_ON}
columns={columns}
emptyMessage={t('dependsOnComponentsCard.emptyMessage')}
emptyHelpLink={legacyHelpLink}
asRenderableEntities={asComponentEntities}
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('dependsOnComponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.component.columns,
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind="Component"
relationType={RELATION_DEPENDS_ON}
columns={columns}
emptyMessage={t('dependsOnComponentsCard.emptyMessage')}
emptyHelpLink={componentEntityHelpLink}
asRenderableEntities={asComponentEntities}
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('dependsOnComponentsCard.emptyMessage'),
helpLink: entityColumnPresets.component.helpLink,
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { DependsOnComponentsCard } from './DependsOnComponentsCard';
export type { DependsOnComponentsCardProps } from './DependsOnComponentsCard';
export type {
DependsOnComponentsCardLegacyProps,
DependsOnComponentsCardProps,
} from './DependsOnComponentsCard';
@@ -20,42 +20,88 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asResourceEntities,
componentEntityHelpLink,
RelatedEntitiesCard,
componentEntityHelpLink as legacyHelpLink,
resourceEntityColumns,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
import { catalogTranslationRef } from '../../alpha/translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface DependsOnResourcesCardProps {
variant?: InfoCardVariants;
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link DependsOnResourcesCardProps} instead.
* @public
*/
export interface DependsOnResourcesCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ResourceEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}
export function DependsOnResourcesCard(props: DependsOnResourcesCardProps) {
function isLegacyProps(
props: DependsOnResourcesCardProps | DependsOnResourcesCardLegacyProps,
): props is DependsOnResourcesCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function DependsOnResourcesCard(
props: DependsOnResourcesCardProps | DependsOnResourcesCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('dependsOnResourcesCard.title'),
columns = resourceEntityColumns,
tableOptions = {},
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind="Resource"
relationType={RELATION_DEPENDS_ON}
columns={columns}
emptyMessage={t('dependsOnResourcesCard.emptyMessage')}
emptyHelpLink={legacyHelpLink}
asRenderableEntities={asResourceEntities}
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('dependsOnResourcesCard.title'),
columns = resourceEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.resource.columns,
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind="Resource"
relationType={RELATION_DEPENDS_ON}
columns={columns}
emptyMessage={t('dependsOnResourcesCard.emptyMessage')}
emptyHelpLink={componentEntityHelpLink}
asRenderableEntities={asResourceEntities}
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('dependsOnResourcesCard.emptyMessage'),
helpLink: entityColumnPresets.component.helpLink,
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { DependsOnResourcesCard } from './DependsOnResourcesCard';
export type { DependsOnResourcesCardProps } from './DependsOnResourcesCard';
export type {
DependsOnResourcesCardLegacyProps,
DependsOnResourcesCardProps,
} from './DependsOnResourcesCard';
@@ -20,10 +20,15 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asComponentEntities,
componentEntityColumns,
componentEntityHelpLink,
componentEntityHelpLink as legacyHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
import { catalogTranslationRef } from '../../alpha/translation';
@@ -31,31 +36,72 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface HasComponentsCardProps {
variant?: InfoCardVariants;
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link HasComponentsCardProps} instead.
* @public
*/
export interface HasComponentsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ComponentEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}
export function HasComponentsCard(props: HasComponentsCardProps) {
function isLegacyProps(
props: HasComponentsCardProps | HasComponentsCardLegacyProps,
): props is HasComponentsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function HasComponentsCard(
props: HasComponentsCardProps | HasComponentsCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('hasComponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind="Component"
relationType={RELATION_HAS_PART}
columns={columns}
emptyMessage={t('hasComponentsCard.emptyMessage')}
emptyHelpLink={legacyHelpLink}
asRenderableEntities={asComponentEntities}
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('hasComponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.component.columns,
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind="Component"
relationType={RELATION_HAS_PART}
columns={columns}
emptyMessage={t('hasComponentsCard.emptyMessage')}
emptyHelpLink={componentEntityHelpLink}
asRenderableEntities={asComponentEntities}
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('hasComponentsCard.emptyMessage'),
helpLink: entityColumnPresets.component.helpLink,
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { HasComponentsCard } from './HasComponentsCard';
export type { HasComponentsCardProps } from './HasComponentsCard';
export type {
HasComponentsCardLegacyProps,
HasComponentsCardProps,
} from './HasComponentsCard';
@@ -20,42 +20,88 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asResourceEntities,
RelatedEntitiesCard,
resourceEntityColumns,
resourceEntityHelpLink,
resourceEntityHelpLink as legacyHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
import { catalogTranslationRef } from '../../alpha/translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface HasResourcesCardProps {
variant?: InfoCardVariants;
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link HasResourcesCardProps} instead.
* @public
*/
export interface HasResourcesCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ResourceEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}
export function HasResourcesCard(props: HasResourcesCardProps) {
function isLegacyProps(
props: HasResourcesCardProps | HasResourcesCardLegacyProps,
): props is HasResourcesCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function HasResourcesCard(
props: HasResourcesCardProps | HasResourcesCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('hasResourcesCard.title'),
columns = resourceEntityColumns,
tableOptions = {},
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind="Resource"
relationType={RELATION_HAS_PART}
columns={columns}
emptyMessage={t('hasResourcesCard.emptyMessage')}
emptyHelpLink={legacyHelpLink}
asRenderableEntities={asResourceEntities}
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('hasResourcesCard.title'),
columns = resourceEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.resource.columns,
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind="Resource"
relationType={RELATION_HAS_PART}
columns={columns}
asRenderableEntities={asResourceEntities}
emptyMessage={t('hasResourcesCard.emptyMessage')}
emptyHelpLink={resourceEntityHelpLink}
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('hasResourcesCard.emptyMessage'),
helpLink: entityColumnPresets.resource.helpLink,
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { HasResourcesCard } from './HasResourcesCard';
export type { HasResourcesCardProps } from './HasResourcesCard';
export type {
HasResourcesCardLegacyProps,
HasResourcesCardProps,
} from './HasResourcesCard';
@@ -20,6 +20,11 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asComponentEntities,
componentEntityColumns,
@@ -30,33 +35,77 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface HasSubcomponentsCardProps {
variant?: InfoCardVariants;
title?: string;
columnConfig?: EntityColumnConfig[];
kind?: string;
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link HasSubcomponentsCardProps} instead.
* @public
*/
export interface HasSubcomponentsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<ComponentEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
kind?: string;
}
export function HasSubcomponentsCard(props: HasSubcomponentsCardProps) {
function isLegacyProps(
props: HasSubcomponentsCardProps | HasSubcomponentsCardLegacyProps,
): props is HasSubcomponentsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function HasSubcomponentsCard(
props: HasSubcomponentsCardProps | HasSubcomponentsCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('hasSubcomponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
kind = 'Component',
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind={kind}
relationType={RELATION_HAS_PART}
columns={columns}
asRenderableEntities={asComponentEntities}
emptyMessage={t('hasSubcomponentsCard.emptyMessage')}
emptyHelpLink="https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional"
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('hasSubcomponentsCard.title'),
columns = componentEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.component.columns,
kind = 'Component',
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind={kind}
relationType={RELATION_HAS_PART}
columns={columns}
asRenderableEntities={asComponentEntities}
emptyMessage={t('hasSubcomponentsCard.emptyMessage')}
emptyHelpLink="https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional"
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('hasSubcomponentsCard.emptyMessage'),
helpLink:
'https://backstage.io/docs/features/software-catalog/descriptor-format#specsubcomponentof-optional',
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { HasSubcomponentsCard } from './HasSubcomponentsCard';
export type { HasSubcomponentsCardProps } from './HasSubcomponentsCard';
export type {
HasSubcomponentsCardLegacyProps,
HasSubcomponentsCardProps,
} from './HasSubcomponentsCard';
@@ -20,10 +20,15 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asDomainEntities,
domainEntityColumns,
domainEntityHelpLink,
domainEntityHelpLink as legacyHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
import { catalogTranslationRef } from '../../alpha/translation';
@@ -31,31 +36,72 @@ import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface HasSubdomainsCardProps {
variant?: InfoCardVariants;
tableOptions?: TableOptions;
columns?: TableColumn<DomainEntity>[];
title?: string;
columnConfig?: EntityColumnConfig[];
}
export function HasSubdomainsCard(props: HasSubdomainsCardProps) {
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link HasSubdomainsCardProps} instead.
* @public
*/
export interface HasSubdomainsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<DomainEntity>[];
}
function isLegacyProps(
props: HasSubdomainsCardProps | HasSubdomainsCardLegacyProps,
): props is HasSubdomainsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function HasSubdomainsCard(
props: HasSubdomainsCardProps | HasSubdomainsCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('hasSubdomainsCard.title'),
columns = domainEntityColumns,
tableOptions = {},
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind="Domain"
relationType={RELATION_HAS_PART}
columns={columns}
asRenderableEntities={asDomainEntities}
emptyMessage={t('hasSubdomainsCard.emptyMessage')}
emptyHelpLink={legacyHelpLink}
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('hasSubdomainsCard.title'),
columns = domainEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.domain.columns,
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind="Domain"
relationType={RELATION_HAS_PART}
columns={columns}
asRenderableEntities={asDomainEntities}
emptyMessage={t('hasSubdomainsCard.emptyMessage')}
emptyHelpLink={domainEntityHelpLink}
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('hasSubdomainsCard.emptyMessage'),
helpLink: entityColumnPresets.domain.helpLink,
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { HasSubdomainsCard } from './HasSubdomainsCard';
export type { HasSubdomainsCardProps } from './HasSubdomainsCard';
export type {
HasSubdomainsCardLegacyProps,
HasSubdomainsCardProps,
} from './HasSubdomainsCard';
@@ -20,42 +20,88 @@ import {
TableColumn,
TableOptions,
} from '@backstage/core-components';
import {
EntityRelationCard,
EntityColumnConfig,
entityColumnPresets,
} from '@backstage/plugin-catalog-react/alpha';
import {
asSystemEntities,
RelatedEntitiesCard,
systemEntityColumns,
systemEntityHelpLink,
systemEntityHelpLink as legacyHelpLink,
RelatedEntitiesCard,
} from '../RelatedEntitiesCard';
import { catalogTranslationRef } from '../../alpha/translation';
import { useTranslationRef } from '@backstage/core-plugin-api/alpha';
/** @public */
export interface HasSystemsCardProps {
variant?: InfoCardVariants;
title?: string;
columnConfig?: EntityColumnConfig[];
}
/**
* Props for the legacy MUI-based rendering.
* @deprecated Use {@link HasSystemsCardProps} instead.
* @public
*/
export interface HasSystemsCardLegacyProps {
title?: string;
/** @deprecated Use `columnConfig` instead. */
variant?: InfoCardVariants;
/** @deprecated Use `columnConfig` instead. */
columns?: TableColumn<SystemEntity>[];
/** @deprecated Use `columnConfig` instead. */
tableOptions?: TableOptions;
}
export function HasSystemsCard(props: HasSystemsCardProps) {
function isLegacyProps(
props: HasSystemsCardProps | HasSystemsCardLegacyProps,
): props is HasSystemsCardLegacyProps {
return 'variant' in props || 'columns' in props || 'tableOptions' in props;
}
export function HasSystemsCard(
props: HasSystemsCardProps | HasSystemsCardLegacyProps,
) {
const { t } = useTranslationRef(catalogTranslationRef);
if (isLegacyProps(props)) {
const {
variant = 'gridItem',
title = t('hasSystemsCard.title'),
columns = systemEntityColumns,
tableOptions = {},
} = props;
return (
<RelatedEntitiesCard
variant={variant}
title={title}
entityKind="System"
relationType={RELATION_HAS_PART}
columns={columns}
asRenderableEntities={asSystemEntities}
emptyMessage={t('hasSystemsCard.emptyMessage')}
emptyHelpLink={legacyHelpLink}
tableOptions={tableOptions}
/>
);
}
const {
variant = 'gridItem',
title = t('hasSystemsCard.title'),
columns = systemEntityColumns,
tableOptions = {},
columnConfig = entityColumnPresets.system.columns,
} = props;
return (
<RelatedEntitiesCard
variant={variant}
<EntityRelationCard
title={title}
entityKind="System"
relationType={RELATION_HAS_PART}
columns={columns}
asRenderableEntities={asSystemEntities}
emptyMessage={t('hasSystemsCard.emptyMessage')}
emptyHelpLink={systemEntityHelpLink}
tableOptions={tableOptions}
columnConfig={columnConfig}
emptyState={{
message: t('hasSystemsCard.emptyMessage'),
helpLink: entityColumnPresets.system.helpLink,
}}
/>
);
}
@@ -15,4 +15,7 @@
*/
export { HasSystemsCard } from './HasSystemsCard';
export type { HasSystemsCardProps } from './HasSystemsCard';
export type {
HasSystemsCardLegacyProps,
HasSystemsCardProps,
} from './HasSystemsCard';
@@ -70,6 +70,7 @@ export type RelatedEntitiesCardProps<T extends Entity> = {
* this card as its implementation with some of the props set to the appropriate
* values.
*
* @deprecated Use {@link @backstage/plugin-catalog-react/alpha#EntityRelationCard} instead.
* @public
*/
export const RelatedEntitiesCard = <T extends Entity>(
+32 -8
View File
@@ -57,9 +57,18 @@ export {
CatalogSearchResultListItem,
} from './plugin';
export type { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard';
export type { DependsOnComponentsCardProps } from './components/DependsOnComponentsCard';
export type { DependsOnResourcesCardProps } from './components/DependsOnResourcesCard';
export type {
DependencyOfComponentsCardLegacyProps,
DependencyOfComponentsCardProps,
} from './components/DependencyOfComponentsCard';
export type {
DependsOnComponentsCardLegacyProps,
DependsOnComponentsCardProps,
} from './components/DependsOnComponentsCard';
export type {
DependsOnResourcesCardLegacyProps,
DependsOnResourcesCardProps,
} from './components/DependsOnResourcesCard';
export type {
EntityLinksEmptyStateClassKey,
EntityLinksCardProps,
@@ -69,10 +78,25 @@ export type {
export type { SystemDiagramCardClassKey } from './components/SystemDiagramCard';
export type { DefaultCatalogPageProps } from './components/CatalogPage';
export type { EntityContextMenuClassKey } from './components/EntityContextMenu';
export type { HasComponentsCardProps } from './components/HasComponentsCard';
export type { HasResourcesCardProps } from './components/HasResourcesCard';
export type { HasSubcomponentsCardProps } from './components/HasSubcomponentsCard';
export type { HasSubdomainsCardProps } from './components/HasSubdomainsCard';
export type { HasSystemsCardProps } from './components/HasSystemsCard';
export type {
HasComponentsCardLegacyProps,
HasComponentsCardProps,
} from './components/HasComponentsCard';
export type {
HasResourcesCardLegacyProps,
HasResourcesCardProps,
} from './components/HasResourcesCard';
export type {
HasSubcomponentsCardLegacyProps,
HasSubcomponentsCardProps,
} from './components/HasSubcomponentsCard';
export type {
HasSubdomainsCardLegacyProps,
HasSubdomainsCardProps,
} from './components/HasSubdomainsCard';
export type {
HasSystemsCardLegacyProps,
HasSystemsCardProps,
} from './components/HasSystemsCard';
export type { RelatedEntitiesCardProps } from './components/RelatedEntitiesCard';
export type { CatalogSearchResultListItemProps } from './components/CatalogSearchResultListItem';
+2
View File
@@ -3968,10 +3968,12 @@ __metadata:
"@backstage/plugin-catalog-react": "workspace:^"
"@backstage/plugin-permission-react": "workspace:^"
"@backstage/test-utils": "workspace:^"
"@backstage/ui": "workspace:^"
"@graphiql/react": "npm:0.29.0"
"@material-ui/core": "npm:^4.12.2"
"@material-ui/icons": "npm:^4.9.1"
"@material-ui/lab": "npm:4.0.0-alpha.61"
"@remixicon/react": "npm:^4.6.0"
"@testing-library/dom": "npm:^10.0.0"
"@testing-library/jest-dom": "npm:^6.0.0"
"@testing-library/react": "npm:^16.0.0"