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"