From c548a0ff8651e8e8d36561728ea1a4ce58e73f35 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Tue, 17 Mar 2026 15:33:45 +0100 Subject: [PATCH] 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 * 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 * 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 * chore: add API reports and changesets for entity table card migration Signed-off-by: Johan Persson * 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 * 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 * 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 * 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 * 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 * chore: update alpha API reports Signed-off-by: Johan Persson * 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 * 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 * 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 * 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 --------- Signed-off-by: Johan Persson --- ...add-entity-data-table-and-relation-card.md | 5 + .../migrate-api-docs-entity-cards-to-bui.md | 5 + .../migrate-catalog-entity-cards-to-bui.md | 7 + .../src/components/catalog/EntityPage.tsx | 20 +- .../app/src/components/catalog/EntityPage.tsx | 18 +- plugins/api-docs/package.json | 2 + plugins/api-docs/report-alpha.api.md | 2 +- plugins/api-docs/report.api.md | 167 ++++++++++----- .../ApisCards/ConsumedApisCard.test.tsx | 7 +- .../components/ApisCards/ConsumedApisCard.tsx | 65 +++++- .../src/components/ApisCards/HasApisCard.tsx | 66 +++++- .../ApisCards/ProvidedApisCard.test.tsx | 7 +- .../components/ApisCards/ProvidedApisCard.tsx | 65 +++++- .../src/components/ApisCards/index.ts | 9 + .../src/components/ApisCards/presets.tsx | 81 +++++++- .../ConsumingComponentsCard.tsx | 63 +++++- .../ProvidingComponentsCard.tsx | 63 +++++- .../src/components/ComponentsCards/index.ts | 8 + plugins/api-docs/src/translation.ts | 2 +- plugins/catalog-react/report-alpha.api.md | 97 +++++++++ plugins/catalog-react/src/alpha/index.ts | 2 + .../EntityDataTable/EntityDataTable.test.tsx | 160 +++++++++++++++ .../EntityDataTable/EntityDataTable.tsx | 77 +++++++ .../EntityDataTable/columnFactories.tsx | 193 +++++++++++++++++ .../src/components/EntityDataTable/index.ts | 21 ++ .../components/EntityDataTable/presets.tsx | 61 ++++++ .../EntityRelationCard.test.tsx | 194 ++++++++++++++++++ .../EntityRelationCard/EntityRelationCard.tsx | 85 ++++++++ .../components/EntityRelationCard/index.ts | 18 ++ plugins/catalog-react/src/translation.ts | 3 + plugins/catalog/report-alpha.api.md | 18 +- plugins/catalog/report.api.md | 125 ++++++++--- plugins/catalog/src/alpha/entityCards.tsx | 16 +- plugins/catalog/src/alpha/translation.ts | 18 +- .../DependencyOfComponentsCard.tsx | 74 +++++-- .../DependencyOfComponentsCard/index.ts | 5 +- .../DependsOnComponentsCard.tsx | 72 +++++-- .../DependsOnComponentsCard/index.ts | 5 +- .../DependsOnResourcesCard.tsx | 74 +++++-- .../DependsOnResourcesCard/index.ts | 5 +- .../HasComponentsCard/HasComponentsCard.tsx | 72 +++++-- .../src/components/HasComponentsCard/index.ts | 5 +- .../HasResourcesCard/HasResourcesCard.tsx | 74 +++++-- .../src/components/HasResourcesCard/index.ts | 5 +- .../HasSubcomponentsCard.tsx | 73 +++++-- .../components/HasSubcomponentsCard/index.ts | 5 +- .../HasSubdomainsCard/HasSubdomainsCard.tsx | 76 +++++-- .../src/components/HasSubdomainsCard/index.ts | 5 +- .../HasSystemsCard/HasSystemsCard.tsx | 74 +++++-- .../src/components/HasSystemsCard/index.ts | 5 +- .../RelatedEntitiesCard.tsx | 1 + plugins/catalog/src/index.ts | 40 +++- yarn.lock | 2 + 53 files changed, 2135 insertions(+), 287 deletions(-) create mode 100644 .changeset/add-entity-data-table-and-relation-card.md create mode 100644 .changeset/migrate-api-docs-entity-cards-to-bui.md create mode 100644 .changeset/migrate-catalog-entity-cards-to-bui.md create mode 100644 plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.test.tsx create mode 100644 plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.tsx create mode 100644 plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx create mode 100644 plugins/catalog-react/src/components/EntityDataTable/index.ts create mode 100644 plugins/catalog-react/src/components/EntityDataTable/presets.tsx create mode 100644 plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.test.tsx create mode 100644 plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.tsx create mode 100644 plugins/catalog-react/src/components/EntityRelationCard/index.ts diff --git a/.changeset/add-entity-data-table-and-relation-card.md b/.changeset/add-entity-data-table-and-relation-card.md new file mode 100644 index 0000000000..98c8cd9081 --- /dev/null +++ b/.changeset/add-entity-data-table-and-relation-card.md @@ -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. diff --git a/.changeset/migrate-api-docs-entity-cards-to-bui.md b/.changeset/migrate-api-docs-entity-cards-to-bui.md new file mode 100644 index 0000000000..afc44a832d --- /dev/null +++ b/.changeset/migrate-api-docs-entity-cards-to-bui.md @@ -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. diff --git a/.changeset/migrate-catalog-entity-cards-to-bui.md b/.changeset/migrate-catalog-entity-cards-to-bui.md new file mode 100644 index 0000000000..84b91549a6 --- /dev/null +++ b/.changeset/migrate-catalog-entity-cards-to-bui.md @@ -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. diff --git a/packages/app-legacy/src/components/catalog/EntityPage.tsx b/packages/app-legacy/src/components/catalog/EntityPage.tsx index 7c42b62542..6a8daffad6 100644 --- a/packages/app-legacy/src/components/catalog/EntityPage.tsx +++ b/packages/app-legacy/src/components/catalog/EntityPage.tsx @@ -186,7 +186,7 @@ const overviewContent = ( - + ); @@ -215,10 +215,10 @@ const serviceEntityPage = ( - + - + @@ -246,10 +246,10 @@ const websiteEntityPage = ( - + - + @@ -374,13 +374,13 @@ const systemPage = ( - + - + - + @@ -417,7 +417,7 @@ const domainPage = ( - + @@ -436,7 +436,7 @@ const resourcePage = ( - + diff --git a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx index 367652b70d..d229b536cb 100644 --- a/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx +++ b/packages/create-app/templates/default-app/packages/app/src/components/catalog/EntityPage.tsx @@ -138,7 +138,7 @@ const overviewContent = ( - + ); @@ -175,10 +175,10 @@ const serviceEntityPage = ( - + - + @@ -210,10 +210,10 @@ const websiteEntityPage = ( - + - + @@ -345,13 +345,13 @@ const systemPage = ( - + - + - + @@ -388,7 +388,7 @@ const domainPage = ( - + diff --git a/plugins/api-docs/package.json b/plugins/api-docs/package.json index a0d50e78ab..7dcc9fd958 100644 --- a/plugins/api-docs/package.json +++ b/plugins/api-docs/package.json @@ -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", diff --git a/plugins/api-docs/report-alpha.api.md b/plugins/api-docs/report-alpha.api.md index 0ee7ac5460..4afa55ce77 100644 --- a/plugins/api-docs/report-alpha.api.md +++ b/plugins/api-docs/report-alpha.api.md @@ -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.'; } >; diff --git a/plugins/api-docs/report.api.md b/plugins/api-docs/report.api.md index 693502fad9..370cbb3e0e 100644 --- a/plugins/api-docs/report.api.md +++ b/plugins/api-docs/report.api.md @@ -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[]; + // @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[]; + // @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[]; - 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[]; - 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[]; - 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[]; - 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[]; - 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[]; + // @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[]; + // @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[]; + // @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: ( diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx index e8ed6bedb5..b62616e98c 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.test.tsx @@ -60,7 +60,7 @@ describe('', () => { relations: [], }; - const { getByText, getByRole, container } = await renderInTestApp( + const { getByText, getByRole } = await renderInTestApp( @@ -76,16 +76,11 @@ describe('', () => { 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 () => { diff --git a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx index 5dc6e4a473..2e289a7009 100644 --- a/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ConsumedApisCard.tsx @@ -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[]; + /** @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 ; + } + + const { + title = t('consumedApisCard.title'), + columnConfig = getApiEntityColumnConfig(t), + } = props; + + return ( + + ); }; diff --git a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx index f0777e87a8..66f6d0b4f4 100644 --- a/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/HasApisCard.tsx @@ -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[]; + /** @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[] = 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 ; + } + + const { + title = t('hasApisCard.title'), + columnConfig = getHasApisColumnConfig(t), + } = props; + + return ( + + ); }; diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx index df478566e9..b49da7a9a1 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.test.tsx @@ -60,7 +60,7 @@ describe('', () => { relations: [], }; - const { getByText, getByRole, container } = await renderInTestApp( + const { getByText, getByRole } = await renderInTestApp( @@ -77,16 +77,11 @@ describe('', () => { 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 () => { diff --git a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx index 784bfe2a2a..f99c10e176 100644 --- a/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx +++ b/plugins/api-docs/src/components/ApisCards/ProvidedApisCard.tsx @@ -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[]; + /** @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 ; + } + + const { + title = t('providedApisCard.title'), + columnConfig = getApiEntityColumnConfig(t), + } = props; + + return ( + + ); }; diff --git a/plugins/api-docs/src/components/ApisCards/index.ts b/plugins/api-docs/src/components/ApisCards/index.ts index 4a74549755..d78454bf28 100644 --- a/plugins/api-docs/src/components/ApisCards/index.ts +++ b/plugins/api-docs/src/components/ApisCards/index.ts @@ -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'; diff --git a/plugins/api-docs/src/components/ApisCards/presets.tsx b/plugins/api-docs/src/components/ApisCards/presets.tsx index 8341ce2c2b..3489797eb6 100644 --- a/plugins/api-docs/src/components/ApisCards/presets.tsx +++ b/plugins/api-docs/src/components/ApisCards/presets.tsx @@ -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, ): TableColumn { @@ -43,13 +48,13 @@ const ApiDefinitionButton = ({ apiEntity }: { apiEntity: ApiEntity }) => { const { t } = useTranslationRef(apiDocsTranslationRef); return ( <> - setDialogOpen(!dialogOpen)} - value={dialogOpen} - > - - + onPress={() => setDialogOpen(!dialogOpen)} + variant="tertiary" + size="small" + icon={} + /> , ): TableColumn[] => { @@ -81,3 +87,62 @@ export const getApiEntityColumns = ( createApiDefinitionColumn(t), ]; }; + +// Column config presets + +function createSpecApiTypeColumnConfig( + t: TranslationFunction, +): EntityColumnConfig { + return { + id: 'apiType', + label: t('apiEntityColumns.typeTitle'), + isSortable: true, + cell: entity => ( + + + + ), + sortValue: entity => + (entity.spec as Record | undefined)?.type ?? '', + }; +} + +function createApiDefinitionColumnConfig( + t: TranslationFunction, +): EntityColumnConfig { + return { + id: 'apiDefinition', + label: t('apiEntityColumns.apiDefinitionTitle'), + cell: entity => ( + + + + ), + }; +} + +export function getApiEntityColumnConfig( + t: TranslationFunction, +): EntityColumnConfig[] { + return [ + entityDataTableColumns.createEntityRefColumn({ defaultKind: 'API' }), + entityDataTableColumns.createSystemColumn(), + entityDataTableColumns.createOwnerColumn(), + createSpecApiTypeColumnConfig(t), + entityDataTableColumns.createSpecLifecycleColumn(), + entityDataTableColumns.createMetadataDescriptionColumn(), + createApiDefinitionColumnConfig(t), + ]; +} + +export function getHasApisColumnConfig( + t: TranslationFunction, +): EntityColumnConfig[] { + return [ + entityDataTableColumns.createEntityRefColumn({ defaultKind: 'API' }), + entityDataTableColumns.createOwnerColumn(), + createSpecApiTypeColumnConfig(t), + entityDataTableColumns.createSpecLifecycleColumn(), + entityDataTableColumns.createMetadataDescriptionColumn(), + ]; +} diff --git a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx index 1fda0d7e05..1006c069f6 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ConsumingComponentsCard.tsx @@ -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[]; + /** @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 ; + } + + const { + title = t('consumingComponentsCard.title'), + columnConfig = entityColumnPresets.component.columns, + } = props; + + return ( + + ); }; diff --git a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx index efdcd83247..e1be0c777d 100644 --- a/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx +++ b/plugins/api-docs/src/components/ComponentsCards/ProvidingComponentsCard.tsx @@ -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[]; + /** @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 ; + } + + const { + title = t('providingComponentsCard.title'), + columnConfig = entityColumnPresets.component.columns, + } = props; + + return ( + + ); }; diff --git a/plugins/api-docs/src/components/ComponentsCards/index.ts b/plugins/api-docs/src/components/ComponentsCards/index.ts index 84dec071fb..33def6180c 100644 --- a/plugins/api-docs/src/components/ComponentsCards/index.ts +++ b/plugins/api-docs/src/components/ComponentsCards/index.ts @@ -15,4 +15,12 @@ */ export { ConsumingComponentsCard } from './ConsumingComponentsCard'; +export type { + ConsumingComponentsCardLegacyProps, + ConsumingComponentsCardProps, +} from './ConsumingComponentsCard'; export { ProvidingComponentsCard } from './ProvidingComponentsCard'; +export type { + ProvidingComponentsCardLegacyProps, + ProvidingComponentsCardProps, +} from './ProvidingComponentsCard'; diff --git a/plugins/api-docs/src/translation.ts b/plugins/api-docs/src/translation.ts index 385f5ecbaf..a6a43952bc 100644 --- a/plugins/api-docs/src/translation.ts +++ b/plugins/api-docs/src/translation.ts @@ -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.', }, }); diff --git a/plugins/catalog-react/report-alpha.api.md b/plugins/catalog-react/report-alpha.api.md index 9c76a221dc..3cf5bc652c 100644 --- a/plugins/catalog-react/report-alpha.api.md +++ b/plugins/catalog-react/report-alpha.api.md @@ -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 { + // (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, diff --git a/plugins/catalog-react/src/alpha/index.ts b/plugins/catalog-react/src/alpha/index.ts index 6d2d76fd9c..a4c8daa83b 100644 --- a/plugins/catalog-react/src/alpha/index.ts +++ b/plugins/catalog-react/src/alpha/index.ts @@ -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'; diff --git a/plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.test.tsx b/plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.test.tsx new file mode 100644 index 0000000000..77b0e26a02 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.test.tsx @@ -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 => , + sortValue: entity => entity.metadata.name, + }, + { + id: 'description', + label: 'Description', + cell: entity => , + }, +]; + +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('', () => { + it('renders entity data in a table', async () => { + await renderInTestApp( + , + { + 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( + No entities found} + />, + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('No entities found')).toBeInTheDocument(); + }); + + it('renders error state', async () => { + await renderInTestApp( + , + { + 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( + , + { + 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 => , + }, + ]; + + await renderInTestApp( + , + { + 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'); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.tsx b/plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.tsx new file mode 100644 index 0000000000..32234b1bc0 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityDataTable/EntityDataTable.tsx @@ -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 ( + + ); +} diff --git a/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx b/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx new file mode 100644 index 0000000000..49fb64caa8 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityDataTable/columnFactories.tsx @@ -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 { + 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: () => ( + + + + ), + isRowHeader, + isSortable: true, + cell: entity => ( + + + + ), + 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: () => ( + + + + ), + isSortable: true, + cell: entity => ( + + + + ), + 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: () => ( + + + + ), + isSortable: true, + cell: entity => , + sortValue: entity => entity.metadata.description ?? '', + }; + }, + + createSpecTypeColumn(): EntityColumnConfig { + return { + id: 'type', + label: 'Type', + header: () => ( + + + + ), + isSortable: true, + cell: entity => ( + | undefined)?.type ?? '' + } + /> + ), + sortValue: entity => + (entity.spec as Record | undefined)?.type ?? '', + }; + }, + + createSpecLifecycleColumn(): EntityColumnConfig { + return { + id: 'lifecycle', + label: 'Lifecycle', + header: () => ( + + + + ), + isSortable: true, + cell: entity => ( + | undefined)?.lifecycle ?? '' + } + /> + ), + sortValue: entity => + (entity.spec as Record | undefined)?.lifecycle ?? '', + }; + }, +}); diff --git a/plugins/catalog-react/src/components/EntityDataTable/index.ts b/plugins/catalog-react/src/components/EntityDataTable/index.ts new file mode 100644 index 0000000000..a313d3401e --- /dev/null +++ b/plugins/catalog-react/src/components/EntityDataTable/index.ts @@ -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'; diff --git a/plugins/catalog-react/src/components/EntityDataTable/presets.tsx b/plugins/catalog-react/src/components/EntityDataTable/presets.tsx new file mode 100644 index 0000000000..7bbd94e7d7 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityDataTable/presets.tsx @@ -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; diff --git a/plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.test.tsx b/plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.test.tsx new file mode 100644 index 0000000000..06e9059ee3 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.test.tsx @@ -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('', () => { + const catalogApi = catalogApiMock.mock(); + let Wrapper: ComponentType>; + + beforeEach(() => { + Wrapper = ({ children }: { children?: ReactNode }) => ( + + {children} + + ); + }); + + 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( + + + + + , + { + 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( + + + + + , + { + 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( + + + + + , + { + 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( + + + + + , + { + mountedRoutes: { + '/catalog/:namespace/:kind/:name': entityRouteRef, + }, + }, + ); + + expect(screen.getByText('Has components')).toBeInTheDocument(); + expect( + await screen.findByText(/Request failed with 500/), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.tsx b/plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.tsx new file mode 100644 index 0000000000..6554881fd5 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityRelationCard/EntityRelationCard.tsx @@ -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 ( + + {error ? ( + + ) : ( + + {emptyState.message}{' '} + {emptyState.helpLink && ( + + {t('entityRelationCard.emptyHelpLinkTitle')} + + )} + + ) + } + /> + )} + + ); +} diff --git a/plugins/catalog-react/src/components/EntityRelationCard/index.ts b/plugins/catalog-react/src/components/EntityRelationCard/index.ts new file mode 100644 index 0000000000..0f42b61fb7 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityRelationCard/index.ts @@ -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'; diff --git a/plugins/catalog-react/src/translation.ts b/plugins/catalog-react/src/translation.ts index 4411f7e004..77e0162e8b 100644 --- a/plugins/catalog-react/src/translation.ts +++ b/plugins/catalog-react/src/translation.ts @@ -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', diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index a0b092a453..a26143a1f6 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -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'; diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index 48e3f53de2..cebf1fd503 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -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; } -// @public (undocumented) -export interface DependencyOfComponentsCardProps { - // (undocumented) +// @public @deprecated +export interface DependencyOfComponentsCardLegacyProps { + // @deprecated (undocumented) columns?: TableColumn[]; - // (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[]; + // @deprecated (undocumented) + tableOptions?: TableOptions; + // (undocumented) + title?: string; + // @deprecated (undocumented) variant?: InfoCardVariants; } // @public (undocumented) export interface DependsOnComponentsCardProps { // (undocumented) - columns?: TableColumn[]; + columnConfig?: EntityColumnConfig[]; // (undocumented) + title?: string; +} + +// @public @deprecated +export interface DependsOnResourcesCardLegacyProps { + // @deprecated (undocumented) + columns?: TableColumn[]; + // @deprecated (undocumented) tableOptions?: TableOptions; // (undocumented) title?: string; - // (undocumented) + // @deprecated (undocumented) variant?: InfoCardVariants; } // @public (undocumented) export interface DependsOnResourcesCardProps { // (undocumented) - columns?: TableColumn[]; - // (undocumented) - tableOptions?: TableOptions; + columnConfig?: EntityColumnConfig[]; // (undocumented) title?: string; - // (undocumented) - variant?: InfoCardVariants; } // @public @@ -502,18 +527,26 @@ export function hasCatalogProcessingErrors( }, ): Promise; -// @public (undocumented) -export interface HasComponentsCardProps { - // (undocumented) +// @public @deprecated +export interface HasComponentsCardLegacyProps { + // @deprecated (undocumented) columns?: TableColumn[]; - // (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; -// @public (undocumented) -export interface HasResourcesCardProps { - // (undocumented) +// @public @deprecated +export interface HasResourcesCardLegacyProps { + // @deprecated (undocumented) columns?: TableColumn[]; - // (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[]; + // (undocumented) + kind?: string; + // @deprecated (undocumented) + tableOptions?: TableOptions; + // (undocumented) + title?: string; + // @deprecated (undocumented) variant?: InfoCardVariants; } // @public (undocumented) export interface HasSubcomponentsCardProps { // (undocumented) - columns?: TableColumn[]; + columnConfig?: EntityColumnConfig[]; // (undocumented) kind?: string; // (undocumented) + title?: string; +} + +// @public @deprecated +export interface HasSubdomainsCardLegacyProps { + // @deprecated (undocumented) + columns?: TableColumn[]; + // @deprecated (undocumented) tableOptions?: TableOptions; // (undocumented) title?: string; - // (undocumented) + // @deprecated (undocumented) variant?: InfoCardVariants; } // @public (undocumented) export interface HasSubdomainsCardProps { // (undocumented) - columns?: TableColumn[]; + columnConfig?: EntityColumnConfig[]; // (undocumented) + title?: string; +} + +// @public @deprecated +export interface HasSystemsCardLegacyProps { + // @deprecated (undocumented) + columns?: TableColumn[]; + // @deprecated (undocumented) tableOptions?: TableOptions; // (undocumented) title?: string; - // (undocumented) + // @deprecated (undocumented) variant?: InfoCardVariants; } // @public (undocumented) export interface HasSystemsCardProps { // (undocumented) - columns?: TableColumn[]; - // (undocumented) - tableOptions?: TableOptions; + columnConfig?: EntityColumnConfig[]; // (undocumented) title?: string; - // (undocumented) - variant?: InfoCardVariants; } // @public diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 7be14eb445..bd48eebaaf 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -100,7 +100,7 @@ export const catalogDependsOnComponentsEntityCard = EntityCardBlueprint.make({ filter: { kind: 'component' }, loader: async () => import('../components/DependsOnComponentsCard').then(m => ( - + )), }, }); @@ -111,7 +111,7 @@ export const catalogDependsOnResourcesEntityCard = EntityCardBlueprint.make({ filter: { kind: 'component' }, loader: async () => import('../components/DependsOnResourcesCard').then(m => ( - + )), }, }); @@ -122,7 +122,7 @@ export const catalogHasComponentsEntityCard = EntityCardBlueprint.make({ filter: { kind: 'system' }, loader: async () => import('../components/HasComponentsCard').then(m => ( - + )), }, }); @@ -133,7 +133,7 @@ export const catalogHasResourcesEntityCard = EntityCardBlueprint.make({ filter: { kind: 'system' }, loader: async () => import('../components/HasResourcesCard').then(m => ( - + )), }, }); @@ -144,7 +144,7 @@ export const catalogHasSubcomponentsEntityCard = EntityCardBlueprint.make({ filter: { kind: 'component' }, loader: async () => import('../components/HasSubcomponentsCard').then(m => ( - + )), }, }); @@ -155,7 +155,7 @@ export const catalogHasSubdomainsEntityCard = EntityCardBlueprint.make({ filter: { kind: 'domain' }, loader: async () => import('../components/HasSubdomainsCard').then(m => ( - + )), }, }); @@ -165,9 +165,7 @@ export const catalogHasSystemsEntityCard = EntityCardBlueprint.make({ params: { filter: { kind: 'domain' }, loader: async () => - import('../components/HasSystemsCard').then(m => ( - - )), + import('../components/HasSystemsCard').then(m => ), }, }); diff --git a/plugins/catalog/src/alpha/translation.ts b/plugins/catalog/src/alpha/translation.ts index 3d48c0915a..240dea6407 100644 --- a/plugins/catalog/src/alpha/translation.ts +++ b/plugins/catalog/src/alpha/translation.ts @@ -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', diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx index 72f6d5b85b..0bd2e9e094 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/DependencyOfComponentsCard.tsx @@ -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[]; + /** @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 ( + + ); + } + const { - variant = 'gridItem', title = t('dependencyOfComponentsCard.title'), - columns = componentEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.component.columns, } = props; return ( - ); } diff --git a/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts b/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts index 3d324e865d..a7067237ff 100644 --- a/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts +++ b/plugins/catalog/src/components/DependencyOfComponentsCard/index.ts @@ -15,4 +15,7 @@ */ export { DependencyOfComponentsCard } from './DependencyOfComponentsCard'; -export type { DependencyOfComponentsCardProps } from './DependencyOfComponentsCard'; +export type { + DependencyOfComponentsCardLegacyProps, + DependencyOfComponentsCardProps, +} from './DependencyOfComponentsCard'; diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx index 7276e3bf08..f92e917d62 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx +++ b/plugins/catalog/src/components/DependsOnComponentsCard/DependsOnComponentsCard.tsx @@ -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[]; + /** @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 ( + + ); + } + const { - variant = 'gridItem', title = t('dependsOnComponentsCard.title'), - columns = componentEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.component.columns, } = props; return ( - ); } diff --git a/plugins/catalog/src/components/DependsOnComponentsCard/index.ts b/plugins/catalog/src/components/DependsOnComponentsCard/index.ts index 8292526270..f62d7995aa 100644 --- a/plugins/catalog/src/components/DependsOnComponentsCard/index.ts +++ b/plugins/catalog/src/components/DependsOnComponentsCard/index.ts @@ -15,4 +15,7 @@ */ export { DependsOnComponentsCard } from './DependsOnComponentsCard'; -export type { DependsOnComponentsCardProps } from './DependsOnComponentsCard'; +export type { + DependsOnComponentsCardLegacyProps, + DependsOnComponentsCardProps, +} from './DependsOnComponentsCard'; diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx index 64153e98af..7d8ff6a650 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx +++ b/plugins/catalog/src/components/DependsOnResourcesCard/DependsOnResourcesCard.tsx @@ -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[]; + /** @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 ( + + ); + } + const { - variant = 'gridItem', title = t('dependsOnResourcesCard.title'), - columns = resourceEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.resource.columns, } = props; return ( - ); } diff --git a/plugins/catalog/src/components/DependsOnResourcesCard/index.ts b/plugins/catalog/src/components/DependsOnResourcesCard/index.ts index 8d638e16a2..1384df4da3 100644 --- a/plugins/catalog/src/components/DependsOnResourcesCard/index.ts +++ b/plugins/catalog/src/components/DependsOnResourcesCard/index.ts @@ -15,4 +15,7 @@ */ export { DependsOnResourcesCard } from './DependsOnResourcesCard'; -export type { DependsOnResourcesCardProps } from './DependsOnResourcesCard'; +export type { + DependsOnResourcesCardLegacyProps, + DependsOnResourcesCardProps, +} from './DependsOnResourcesCard'; diff --git a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx index 2917cdf1e1..c41eaf865d 100644 --- a/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx +++ b/plugins/catalog/src/components/HasComponentsCard/HasComponentsCard.tsx @@ -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[]; + /** @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 ( + + ); + } + const { - variant = 'gridItem', title = t('hasComponentsCard.title'), - columns = componentEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.component.columns, } = props; return ( - ); } diff --git a/plugins/catalog/src/components/HasComponentsCard/index.ts b/plugins/catalog/src/components/HasComponentsCard/index.ts index 86038b6030..96278c4513 100644 --- a/plugins/catalog/src/components/HasComponentsCard/index.ts +++ b/plugins/catalog/src/components/HasComponentsCard/index.ts @@ -15,4 +15,7 @@ */ export { HasComponentsCard } from './HasComponentsCard'; -export type { HasComponentsCardProps } from './HasComponentsCard'; +export type { + HasComponentsCardLegacyProps, + HasComponentsCardProps, +} from './HasComponentsCard'; diff --git a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx index 81cd432290..34659c4930 100644 --- a/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx +++ b/plugins/catalog/src/components/HasResourcesCard/HasResourcesCard.tsx @@ -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[]; + /** @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 ( + + ); + } + const { - variant = 'gridItem', title = t('hasResourcesCard.title'), - columns = resourceEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.resource.columns, } = props; return ( - ); } diff --git a/plugins/catalog/src/components/HasResourcesCard/index.ts b/plugins/catalog/src/components/HasResourcesCard/index.ts index cdbbef680b..171808d75c 100644 --- a/plugins/catalog/src/components/HasResourcesCard/index.ts +++ b/plugins/catalog/src/components/HasResourcesCard/index.ts @@ -15,4 +15,7 @@ */ export { HasResourcesCard } from './HasResourcesCard'; -export type { HasResourcesCardProps } from './HasResourcesCard'; +export type { + HasResourcesCardLegacyProps, + HasResourcesCardProps, +} from './HasResourcesCard'; diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx index 84087ed082..7307379d5d 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx +++ b/plugins/catalog/src/components/HasSubcomponentsCard/HasSubcomponentsCard.tsx @@ -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[]; + /** @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 ( + + ); + } + const { - variant = 'gridItem', title = t('hasSubcomponentsCard.title'), - columns = componentEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.component.columns, kind = 'Component', } = props; return ( - ); } diff --git a/plugins/catalog/src/components/HasSubcomponentsCard/index.ts b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts index 6f43967d50..44a6faf125 100644 --- a/plugins/catalog/src/components/HasSubcomponentsCard/index.ts +++ b/plugins/catalog/src/components/HasSubcomponentsCard/index.ts @@ -15,4 +15,7 @@ */ export { HasSubcomponentsCard } from './HasSubcomponentsCard'; -export type { HasSubcomponentsCardProps } from './HasSubcomponentsCard'; +export type { + HasSubcomponentsCardLegacyProps, + HasSubcomponentsCardProps, +} from './HasSubcomponentsCard'; diff --git a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx index 367dbe6c94..63c349ec60 100644 --- a/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx +++ b/plugins/catalog/src/components/HasSubdomainsCard/HasSubdomainsCard.tsx @@ -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[]; 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[]; +} + +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 ( + + ); + } + const { - variant = 'gridItem', title = t('hasSubdomainsCard.title'), - columns = domainEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.domain.columns, } = props; return ( - ); } diff --git a/plugins/catalog/src/components/HasSubdomainsCard/index.ts b/plugins/catalog/src/components/HasSubdomainsCard/index.ts index a740abbcc2..57353f4226 100644 --- a/plugins/catalog/src/components/HasSubdomainsCard/index.ts +++ b/plugins/catalog/src/components/HasSubdomainsCard/index.ts @@ -15,4 +15,7 @@ */ export { HasSubdomainsCard } from './HasSubdomainsCard'; -export type { HasSubdomainsCardProps } from './HasSubdomainsCard'; +export type { + HasSubdomainsCardLegacyProps, + HasSubdomainsCardProps, +} from './HasSubdomainsCard'; diff --git a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx index fab9cf4769..1234c45d6f 100644 --- a/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx +++ b/plugins/catalog/src/components/HasSystemsCard/HasSystemsCard.tsx @@ -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[]; + /** @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 ( + + ); + } + const { - variant = 'gridItem', title = t('hasSystemsCard.title'), - columns = systemEntityColumns, - tableOptions = {}, + columnConfig = entityColumnPresets.system.columns, } = props; return ( - ); } diff --git a/plugins/catalog/src/components/HasSystemsCard/index.ts b/plugins/catalog/src/components/HasSystemsCard/index.ts index 71fb7539b1..fd5c204cdd 100644 --- a/plugins/catalog/src/components/HasSystemsCard/index.ts +++ b/plugins/catalog/src/components/HasSystemsCard/index.ts @@ -15,4 +15,7 @@ */ export { HasSystemsCard } from './HasSystemsCard'; -export type { HasSystemsCardProps } from './HasSystemsCard'; +export type { + HasSystemsCardLegacyProps, + HasSystemsCardProps, +} from './HasSystemsCard'; diff --git a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx index 1337a6dc12..e07a949965 100644 --- a/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx +++ b/plugins/catalog/src/components/RelatedEntitiesCard/RelatedEntitiesCard.tsx @@ -70,6 +70,7 @@ export type RelatedEntitiesCardProps = { * 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 = ( diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index a59283c677..303677b68a 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -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'; diff --git a/yarn.lock b/yarn.lock index 072fb23cee..82b80a61cd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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"