diff --git a/.changeset/social-worlds-report.md b/.changeset/social-worlds-report.md new file mode 100644 index 0000000000..58acda40a8 --- /dev/null +++ b/.changeset/social-worlds-report.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-catalog-react': minor +--- + +Added `EntityInfoCard` component to `@backstage/plugin-catalog-react` as a BUI-based card wrapper for entity page cards. diff --git a/.changeset/thirty-kiwis-trade.md b/.changeset/thirty-kiwis-trade.md new file mode 100644 index 0000000000..d88d804dac --- /dev/null +++ b/.changeset/thirty-kiwis-trade.md @@ -0,0 +1,22 @@ +--- +'@backstage/plugin-catalog': major +'@backstage/plugin-org': minor +--- + +Migrated `EntityAboutCard`, `EntityLinksCard`, `EntityLabelsCard`, `GroupProfileCard`, and `UserProfileCard` from MUI/InfoCard to use the new BUI card layout and BUI components where possible. + +**BREAKING**: Removed `variant` prop from EntityAboutCard, EntityUserProfileCard, EntityGroupProfileCard, EntityLabelsCard, EntityLinksCard. Removed `gridSizes` prop from `AboutField`. + +**Migration:** + +Simply delete the obsolete `variant` and `gridSizes` props, e.g: + +```diff +- ++ +``` + +```diff +- ++ +``` diff --git a/docs/plugins/integrating-plugin-into-software-catalog.md b/docs/plugins/integrating-plugin-into-software-catalog.md index 911d74b019..417b932451 100644 --- a/docs/plugins/integrating-plugin-into-software-catalog.md +++ b/docs/plugins/integrating-plugin-into-software-catalog.md @@ -94,7 +94,7 @@ const systemPage = ( - + diff --git a/packages/app-legacy/src/components/catalog/EntityPage.tsx b/packages/app-legacy/src/components/catalog/EntityPage.tsx index 08d4d921b5..32249cb84a 100644 --- a/packages/app-legacy/src/components/catalog/EntityPage.tsx +++ b/packages/app-legacy/src/components/catalog/EntityPage.tsx @@ -166,7 +166,7 @@ const overviewContent = ( {entityWarningContent} - + @@ -330,7 +330,7 @@ const userPage = ( {entityWarningContent} - + {entityWarningContent} - + {entityWarningContent} - + @@ -418,7 +418,7 @@ const domainPage = ( {entityWarningContent} - + @@ -437,7 +437,7 @@ const resourcePage = ( {entityWarningContent} - + diff --git a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx index c5138dac3d..f9e8793613 100644 --- a/packages/core-compat-api/src/collectLegacyRoutes.test.tsx +++ b/packages/core-compat-api/src/collectLegacyRoutes.test.tsx @@ -156,7 +156,7 @@ describe('collectLegacyRoutes', () => { if={isKind('component')} children={ - + } /> 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 f75d984366..750dfe6cea 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 @@ -128,7 +128,7 @@ const overviewContent = ( {entityWarningContent} - + @@ -298,7 +298,7 @@ const userPage = ( {entityWarningContent} - + @@ -314,7 +314,7 @@ const groupPage = ( {entityWarningContent} - + @@ -336,7 +336,7 @@ const systemPage = ( {entityWarningContent} - + @@ -383,7 +383,7 @@ const domainPage = ( {entityWarningContent} - + diff --git a/plugins/catalog-react/report.api.md b/plugins/catalog-react/report.api.md index 14c832d545..6a930671c2 100644 --- a/plugins/catalog-react/report.api.md +++ b/plugins/catalog-react/report.api.md @@ -277,6 +277,23 @@ export type EntityFilter = { toQueryValue?: () => string | string[]; }; +// @public (undocumented) +export function EntityInfoCard(props: EntityInfoCardProps): JSX_2.Element; + +// @public (undocumented) +export interface EntityInfoCardProps { + // (undocumented) + children?: ReactNode; + // (undocumented) + className?: string; + // (undocumented) + footerActions?: ReactNode; + // (undocumented) + headerActions?: ReactNode; + // (undocumented) + title?: ReactNode; +} + // @public export class EntityKindFilter implements EntityFilter { constructor(value: string, label: string); diff --git a/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx new file mode 100644 index 0000000000..b3b0efd596 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx @@ -0,0 +1,102 @@ +/* + * 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 { renderInTestApp } from '@backstage/test-utils'; +import { screen } from '@testing-library/react'; +import { EntityInfoCard } from './EntityInfoCard'; + +describe('', () => { + it('renders children in the card body', async () => { + await renderInTestApp( + +
Body content
+
, + ); + + expect(screen.getByText('Body content')).toBeInTheDocument(); + }); + + it('renders the title when provided', async () => { + await renderInTestApp( + +
Body
+
, + ); + + expect( + screen.getByRole('heading', { name: 'My Card Title' }), + ).toBeInTheDocument(); + }); + + it('does not render a heading when title is not provided', async () => { + await renderInTestApp( + +
Body
+
, + ); + + expect(screen.queryByRole('heading')).not.toBeInTheDocument(); + }); + + it('renders header actions next to the title', async () => { + await renderInTestApp( + Edit}> +
Body
+
, + ); + + expect(screen.getByRole('heading', { name: 'Title' })).toBeInTheDocument(); + expect(screen.getByRole('button', { name: 'Edit' })).toBeInTheDocument(); + }); + + it('renders footer actions when provided', async () => { + await renderInTestApp( + Next Page}> +
Body
+
, + ); + + expect( + screen.getByRole('button', { name: 'Next Page' }), + ).toBeInTheDocument(); + }); + + it('does not render footer when footerActions is not provided', async () => { + const { container } = await renderInTestApp( + +
Body
+
, + ); + + expect(container.querySelector('.bui-CardFooter')).not.toBeInTheDocument(); + }); + + it('renders JSX titles (e.g., icon + text)', async () => { + await renderInTestApp( + + 🏠 Home Card + + } + > +
Body
+
, + ); + + expect(screen.getByText('Home Card')).toBeInTheDocument(); + }); +}); diff --git a/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx new file mode 100644 index 0000000000..a0c7c20ffe --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx @@ -0,0 +1,69 @@ +/* + * 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 { ReactNode } from 'react'; +import { + Card, + CardHeader, + CardBody, + CardFooter, + Text, + Flex, +} from '@backstage/ui'; +import { makeStyles } from '@material-ui/core/styles'; +import classNames from 'classnames'; + +const useStyles = makeStyles({ + root: { + height: '100%', + }, +}); + +/** @public */ +export interface EntityInfoCardProps { + title?: ReactNode; + headerActions?: ReactNode; + footerActions?: ReactNode; + children?: ReactNode; + className?: string; +} + +/** @public */ +export function EntityInfoCard(props: EntityInfoCardProps) { + const { title, headerActions, footerActions, children, className } = props; + const classes = useStyles(); + + return ( + + {title && ( + + + + {title} + + {headerActions && ( + + {headerActions} + + )} + + + )} + {children} + {footerActions && {footerActions}} + + ); +} diff --git a/plugins/catalog-react/src/components/EntityInfoCard/index.ts b/plugins/catalog-react/src/components/EntityInfoCard/index.ts new file mode 100644 index 0000000000..1999a228b1 --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/index.ts @@ -0,0 +1,18 @@ +/* + * Copyright 2025 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 { EntityInfoCard } from './EntityInfoCard'; +export type { EntityInfoCardProps } from './EntityInfoCard'; diff --git a/plugins/catalog-react/src/components/index.ts b/plugins/catalog-react/src/components/index.ts index 8a1ebd1835..971d6086f7 100644 --- a/plugins/catalog-react/src/components/index.ts +++ b/plugins/catalog-react/src/components/index.ts @@ -35,3 +35,4 @@ export * from './EntityProcessingStatusPicker'; export * from './EntityNamespacePicker'; export * from './EntityAutocompletePicker'; export * from './MissingAnnotationEmptyState'; +export * from './EntityInfoCard'; diff --git a/plugins/catalog/report-alpha.api.md b/plugins/catalog/report-alpha.api.md index 82b6837cc7..fbe90f461c 100644 --- a/plugins/catalog/report-alpha.api.md +++ b/plugins/catalog/report-alpha.api.md @@ -96,6 +96,8 @@ export const catalogTranslationRef: TranslationRef< readonly 'entityContextMenu.moreButtonAriaLabel': 'more'; readonly 'entityLabelsCard.title': 'Labels'; readonly 'entityLabelsCard.readMoreButtonTitle': 'Read more'; + readonly 'entityLabelsCard.columnKeyLabel': 'Label'; + readonly 'entityLabelsCard.columnValueLabel': 'Value'; readonly 'entityLabelsCard.emptyDescription': 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:'; readonly 'entityLabels.ownerLabel': 'Owner'; readonly 'entityLabels.warningPanelTitle': 'Entity not found'; diff --git a/plugins/catalog/report.api.md b/plugins/catalog/report.api.md index 52b13e1c43..48e3f53de2 100644 --- a/plugins/catalog/report.api.md +++ b/plugins/catalog/report.api.md @@ -40,11 +40,6 @@ import { TableProps } from '@backstage/core-components'; import { TabProps } from '@material-ui/core/Tab'; import { UserListFilterKind } from '@backstage/plugin-catalog-react'; -// @public -export type AboutCardProps = { - variant?: InfoCardVariants; -}; - // @public (undocumented) export function AboutContent(props: AboutContentProps): JSX_2.Element; @@ -64,8 +59,6 @@ export interface AboutFieldProps { // (undocumented) className?: string; // (undocumented) - gridSizes?: Record; - // (undocumented) label: string; // (undocumented) value?: string; @@ -334,7 +327,7 @@ export interface DependsOnResourcesCardProps { } // @public -export const EntityAboutCard: (props: AboutCardProps) => JSX.Element; +export const EntityAboutCard: () => JSX.Element; // @public (undocumented) export type EntityContextMenuClassKey = 'button'; @@ -384,8 +377,6 @@ export const EntityLabelsCard: (props: EntityLabelsCardProps) => JSX_2.Element; export interface EntityLabelsCardProps { // (undocumented) title?: string; - // (undocumented) - variant?: InfoCardVariants; } // @public (undocumented) @@ -435,8 +426,6 @@ export const EntityLinksCard: (props: EntityLinksCardProps) => JSX_2.Element; export interface EntityLinksCardProps { // (undocumented) cols?: ColumnBreakpoints | number; - // (undocumented) - variant?: InfoCardVariants; } // @public (undocumented) diff --git a/plugins/catalog/src/alpha/entityCards.tsx b/plugins/catalog/src/alpha/entityCards.tsx index 556a130dff..7be14eb445 100644 --- a/plugins/catalog/src/alpha/entityCards.tsx +++ b/plugins/catalog/src/alpha/entityCards.tsx @@ -66,9 +66,7 @@ export const catalogAboutEntityCard = EntityCardBlueprint.makeWithOverrides({ const { InternalAboutCard } = await import( '../components/AboutCard/AboutCard' ); - return ( - } /> - ); + return } />; }, }); }, @@ -80,9 +78,7 @@ export const catalogLinksEntityCard = EntityCardBlueprint.make({ type: 'info', filter: { 'metadata.links': { $exists: true } }, loader: async () => - import('../components/EntityLinksCard').then(m => ( - - )), + import('../components/EntityLinksCard').then(m => ), }, }); @@ -93,7 +89,7 @@ export const catalogLabelsEntityCard = EntityCardBlueprint.make({ filter: { 'metadata.labels': { $exists: true } }, loader: async () => import('../components/EntityLabelsCard').then(m => ( - + )), }, }); diff --git a/plugins/catalog/src/alpha/translation.ts b/plugins/catalog/src/alpha/translation.ts index f346c408b9..3d48c0915a 100644 --- a/plugins/catalog/src/alpha/translation.ts +++ b/plugins/catalog/src/alpha/translation.ts @@ -111,6 +111,8 @@ export const catalogTranslationRef = createTranslationRef({ }, entityLabelsCard: { title: 'Labels', + columnKeyLabel: 'Label', + columnValueLabel: 'Value', emptyDescription: 'No labels defined for this entity. You can add labels to your entity YAML as shown in the highlighted example below:', readMoreButtonTitle: 'Read more', diff --git a/plugins/catalog/src/components/AboutCard/AboutCard.tsx b/plugins/catalog/src/components/AboutCard/AboutCard.tsx index 8e90659520..065d786a13 100644 --- a/plugins/catalog/src/components/AboutCard/AboutCard.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutCard.tsx @@ -16,11 +16,6 @@ import { useCallback } from 'react'; -import { makeStyles } from '@material-ui/core/styles'; -import Card from '@material-ui/core/Card'; -import CardContent from '@material-ui/core/CardContent'; -import CardHeader from '@material-ui/core/CardHeader'; -import Divider from '@material-ui/core/Divider'; import IconButton from '@material-ui/core/IconButton'; import CachedIcon from '@material-ui/icons/Cached'; import EditIcon from '@material-ui/icons/Edit'; @@ -31,9 +26,9 @@ import { AppIcon, HeaderIconLinkRow, IconLinkVerticalProps, - InfoCardVariants, Link, } from '@backstage/core-components'; +import { EntityInfoCard } from '@backstage/plugin-catalog-react'; import { alertApiRef, errorApiRef, @@ -77,6 +72,16 @@ import { createFromTemplateRouteRef, viewTechDocRouteRef } from '../../routes'; import { catalogTranslationRef } from '../../alpha/translation'; import { useSourceTemplateCompoundEntityRef } from './hooks'; import { AboutContent } from './AboutContent'; +import { makeStyles } from '@material-ui/core/styles'; + +const useStyles = makeStyles({ + linkContainer: { + border: '1px solid var(--bui-border-1)', + borderLeft: 'none', + borderRight: 'none', + marginBottom: 'var(--bui-space-6)', + }, +}); export function useCatalogSourceIconLinkProps() { const { entity } = useEntity(); @@ -152,41 +157,13 @@ function DefaultAboutCardSubheader() { return ; } -const useStyles = makeStyles({ - gridItemCard: { - display: 'flex', - flexDirection: 'column', - height: 'calc(100% - 10px)', // for pages without content header - marginBottom: '10px', - }, - fullHeightCard: { - display: 'flex', - flexDirection: 'column', - height: '100%', - }, - gridItemCardContent: { - flex: 1, - }, - fullHeightCardContent: { - flex: 1, - }, -}); - -/** - * Props for {@link EntityAboutCard}. - * - * @public - */ -export type AboutCardProps = { - variant?: InfoCardVariants; -}; - -export interface InternalAboutCardProps extends AboutCardProps { - subheader?: JSX.Element; +export interface InternalAboutCardProps { + /** Icon link row rendered at the top of the card body. */ + iconLinks?: JSX.Element; } export function InternalAboutCard(props: InternalAboutCardProps) { - const { variant, subheader } = props; + const { iconLinks } = props; const classes = useStyles(); const { entity } = useEntity(); const catalogApi = useApi(catalogApiRef); @@ -202,20 +179,6 @@ export function InternalAboutCard(props: InternalAboutCardProps) { const entityMetadataEditUrl = entity.metadata.annotations?.[ANNOTATION_EDIT_URL]; - let cardClass = ''; - if (variant === 'gridItem') { - cardClass = classes.gridItemCard; - } else if (variant === 'fullHeight') { - cardClass = classes.fullHeightCard; - } - - let cardContentClass = ''; - if (variant === 'gridItem') { - cardContentClass = classes.gridItemCardContent; - } else if (variant === 'fullHeight') { - cardContentClass = classes.fullHeightCardContent; - } - const entityLocation = entity.metadata.annotations?.[ANNOTATION_LOCATION]; // Limiting the ability to manually refresh to the less expensive locations const allowRefresh = @@ -234,60 +197,58 @@ export function InternalAboutCard(props: InternalAboutCardProps) { }, [catalogApi, entity, alertApi, t, errorApi]); return ( - - - {allowRefresh && canRefresh && ( - - - - )} + + {allowRefresh && canRefresh && ( + + + + )} + + + + {sourceTemplateRef && templateRoute && ( - + - {sourceTemplateRef && templateRoute && ( - - - - )} - - } - subheader={subheader ?? } - /> - - - - - + )} + + } + > +
+ {iconLinks ?? } +
+ +
); } /** * Exported publicly via the EntityAboutCard * - * NOTE: We generally do not accept pull requests to extend this class with more - * props and customizability. If you need to tweak it, consider making a bespoke - * card in your own repository instead, that is perfect for your own needs. + * NOTE: We generally do not accept pull requests to extend this class with props + * and customizability. If you need to tweak it, consider making a bespoke card + * in your own repository instead, that is perfect for your own needs. */ -export function AboutCard(props: AboutCardProps) { - return ; +export function AboutCard() { + return ; } diff --git a/plugins/catalog/src/components/AboutCard/AboutContent.tsx b/plugins/catalog/src/components/AboutCard/AboutContent.tsx index d23a29a7c5..eff9ae7da3 100644 --- a/plugins/catalog/src/components/AboutCard/AboutContent.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutContent.tsx @@ -26,8 +26,8 @@ import { } from '@backstage/plugin-catalog-react'; import { JsonArray } from '@backstage/types'; import Chip from '@material-ui/core/Chip'; -import Grid from '@material-ui/core/Grid'; import { makeStyles } from '@material-ui/core/styles'; +import { Grid } from '@backstage/ui'; import { MarkdownContent } from '@backstage/core-components'; import { AboutField } from './AboutField'; import { LinksGridList } from '../EntityLinksCard/LinksGridList'; @@ -114,25 +114,25 @@ export function AboutContent(props: AboutContentProps) { entitySourceLocation = undefined; } + const columns = { initial: '1', sm: '2', lg: '3' } as const; + return ( - - - - + + + + + + {ownedByRelations.length > 0 && ( @@ -142,7 +142,6 @@ export function AboutContent(props: AboutContentProps) { {partOfDomainRelations.length > 0 && ( {partOfSystemRelations.length > 0 && ( )} {(isAPI || @@ -200,38 +196,37 @@ export function AboutContent(props: AboutContentProps) { )} {(entity?.metadata?.tags || []).map(tag => ( ))} {isLocation && (entity?.spec?.targets || entity?.spec?.target) && ( - - target as string) - .map(target => ({ - text: target, - href: getLocationTargetHref( - target, - (entity?.spec?.type || t('aboutCard.unknown')) as string, - entitySourceLocation!, - ), - }))} - /> - + + + target as string) + .map(target => ({ + text: target, + href: getLocationTargetHref( + target, + (entity?.spec?.type || t('aboutCard.unknown')) as string, + entitySourceLocation!, + ), + }))} + /> + + )} - + ); } diff --git a/plugins/catalog/src/components/AboutCard/AboutField.tsx b/plugins/catalog/src/components/AboutCard/AboutField.tsx index 7ced57e542..725a82612a 100644 --- a/plugins/catalog/src/components/AboutCard/AboutField.tsx +++ b/plugins/catalog/src/components/AboutCard/AboutField.tsx @@ -15,7 +15,6 @@ */ import { useElementFilter } from '@backstage/core-plugin-api'; -import Grid from '@material-ui/core/Grid'; import Typography from '@material-ui/core/Typography'; import { makeStyles } from '@material-ui/core/styles'; import { ReactNode } from 'react'; @@ -48,14 +47,13 @@ const useStyles = makeStyles(theme => ({ export interface AboutFieldProps { label: string; value?: string; - gridSizes?: Record; children?: ReactNode; className?: string; } /** @public */ export function AboutField(props: AboutFieldProps) { - const { label, value, gridSizes, children, className } = props; + const { label, value, children, className } = props; const classes = useStyles(); const { t } = useTranslationRef(catalogTranslationRef); @@ -71,11 +69,11 @@ export function AboutField(props: AboutFieldProps) { ); return ( - +
{label} {content} - +
); } diff --git a/plugins/catalog/src/components/AboutCard/index.ts b/plugins/catalog/src/components/AboutCard/index.ts index 9660a5e2e2..f6a11dfbb7 100644 --- a/plugins/catalog/src/components/AboutCard/index.ts +++ b/plugins/catalog/src/components/AboutCard/index.ts @@ -15,7 +15,6 @@ */ export { AboutCard } from './AboutCard'; -export type { AboutCardProps } from './AboutCard'; export { AboutContent } from './AboutContent'; export type { AboutContentProps } from './AboutContent'; export { AboutField } from './AboutField'; diff --git a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx index 04c182b5be..95985f31c3 100644 --- a/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx +++ b/plugins/catalog/src/components/EntityLabelsCard/EntityLabelsCard.tsx @@ -14,77 +14,77 @@ * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog-react'; -import { - InfoCard, - InfoCardVariants, - Table, - TableColumn, -} from '@backstage/core-components'; +import { useEntity, EntityInfoCard } from '@backstage/plugin-catalog-react'; import { EntityLabelsEmptyState } from './EntityLabelsEmptyState'; -import Typography from '@material-ui/core/Typography'; -import { makeStyles } from '@material-ui/core/styles'; +import { + Table, + CellText, + useTable, + type ColumnConfig, + type TableItem, +} from '@backstage/ui'; import { catalogTranslationRef } from '../../alpha/translation'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; +import { useMemo } from 'react'; /** @public */ export interface EntityLabelsCardProps { - variant?: InfoCardVariants; title?: string; } -const useStyles = makeStyles(_ => ({ - key: { - fontWeight: 'bold', - }, -})); +interface LabelItem extends TableItem { + id: string; + key: string; + value: string; +} export const EntityLabelsCard = (props: EntityLabelsCardProps) => { - const { variant, title } = props; + const { title } = props; const { entity } = useEntity(); - const classes = useStyles(); const { t } = useTranslationRef(catalogTranslationRef); - const columns: TableColumn<{ key: string; value: string }>[] = [ - { - render: row => { - return ( - - {row.key} - - ); - }, - }, - { - field: 'value', - }, - ]; - const labels = entity?.metadata?.labels; + const columnConfig: ColumnConfig[] = useMemo( + () => [ + { + id: 'key', + label: t('entityLabelsCard.columnKeyLabel'), + isRowHeader: true, + cell: item => , + }, + { + id: 'value', + label: t('entityLabelsCard.columnValueLabel'), + cell: item => , + }, + ], + [t], + ); + + const data = useMemo( + () => + Object.keys(labels ?? {}).map(labelKey => ({ + id: labelKey, + key: labelKey, + value: labels![labelKey], + })), + [labels], + ); + + const { tableProps } = useTable({ + mode: 'complete', + data, + paginationOptions: { pageSize: 5 }, + }); + return ( - + {!labels || Object.keys(labels).length === 0 ? ( ) : ( - ({ - key: labelKey, - value: labels[labelKey], - }))} - options={{ - search: false, - showTitle: true, - loadingType: 'linear', - header: false, - padding: 'dense', - pageSize: 5, - toolbar: false, - paging: Object.keys(labels).length > 5, - }} - /> +
)} - + ); }; diff --git a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx index 39088b0a16..e090effb55 100644 --- a/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/EntityLinksCard.tsx @@ -14,24 +14,22 @@ * limitations under the License. */ -import { useEntity } from '@backstage/plugin-catalog-react'; +import { useEntity, EntityInfoCard } from '@backstage/plugin-catalog-react'; import LanguageIcon from '@material-ui/icons/Language'; import { EntityLinksEmptyState } from './EntityLinksEmptyState'; import { LinksGridList } from './LinksGridList'; import { ColumnBreakpoints } from './types'; import { IconComponent, useApp } from '@backstage/core-plugin-api'; -import { InfoCard, InfoCardVariants } from '@backstage/core-components'; import { useTranslationRef } from '@backstage/core-plugin-api/alpha'; import { catalogTranslationRef } from '../../alpha/translation'; /** @public */ export interface EntityLinksCardProps { cols?: ColumnBreakpoints | number; - variant?: InfoCardVariants; } export const EntityLinksCard = (props: EntityLinksCardProps) => { - const { cols = undefined, variant } = props; + const { cols = undefined } = props; const { entity } = useEntity(); const app = useApp(); const { t } = useTranslationRef(catalogTranslationRef); @@ -42,7 +40,7 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { const links = entity?.metadata?.links; return ( - + {!links || links.length === 0 ? ( ) : ( @@ -55,6 +53,6 @@ export const EntityLinksCard = (props: EntityLinksCardProps) => { }))} /> )} - + ); }; diff --git a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx index 797428df33..dfb0b280ff 100644 --- a/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx +++ b/plugins/catalog/src/components/EntityLinksCard/LinksGridList.tsx @@ -14,8 +14,7 @@ * limitations under the License. */ -import ImageList from '@material-ui/core/ImageList'; -import ImageListItem from '@material-ui/core/ImageListItem'; +import { Grid, type Columns } from '@backstage/ui'; import { IconLink } from './IconLink'; import { ColumnBreakpoints } from './types'; import { useDynamicColumns } from './useDynamicColumns'; @@ -37,12 +36,13 @@ export function LinksGridList(props: LinksGridListProps) { const numOfCols = useDynamicColumns(cols); return ( - + {items.map(({ text, href, Icon }, i) => ( - - - + ))} - + ); } diff --git a/plugins/catalog/src/index.ts b/plugins/catalog/src/index.ts index f3cc7d3d0b..a59283c677 100644 --- a/plugins/catalog/src/index.ts +++ b/plugins/catalog/src/index.ts @@ -23,7 +23,6 @@ export * from './apis'; export type { - AboutCardProps, AboutContentProps, AboutFieldProps, } from './components/AboutCard'; diff --git a/plugins/catalog/src/plugin.ts b/plugins/catalog/src/plugin.ts index 50d858a45f..6fe531418b 100644 --- a/plugins/catalog/src/plugin.ts +++ b/plugins/catalog/src/plugin.ts @@ -43,7 +43,6 @@ import { SearchResultListItemExtensionProps, } from '@backstage/plugin-search-react'; import { DefaultStarredEntitiesApi } from './apis'; -import { AboutCardProps } from './components/AboutCard'; import { DefaultCatalogPageProps } from './components/CatalogPage'; import { DependencyOfComponentsCardProps } from './components/DependencyOfComponentsCard'; import { DependsOnComponentsCardProps } from './components/DependsOnComponentsCard'; @@ -128,15 +127,14 @@ export const CatalogEntityPage: () => JSX.Element = catalogPlugin.provide( * not extremely customizable; feel free to make a copy of it as a starting * point if you like. */ -export const EntityAboutCard: (props: AboutCardProps) => JSX.Element = - catalogPlugin.provide( - createComponentExtension({ - name: 'EntityAboutCard', - component: { - lazy: () => import('./components/AboutCard').then(m => m.AboutCard), - }, - }), - ); +export const EntityAboutCard: () => JSX.Element = catalogPlugin.provide( + createComponentExtension({ + name: 'EntityAboutCard', + component: { + lazy: () => import('./components/AboutCard').then(m => m.AboutCard), + }, + }), +); /** @public */ export const EntityLinksCard = catalogPlugin.provide( diff --git a/plugins/org/package.json b/plugins/org/package.json index 8aa539a66d..1615811f2d 100644 --- a/plugins/org/package.json +++ b/plugins/org/package.json @@ -57,6 +57,7 @@ "@backstage/frontend-plugin-api": "workspace:^", "@backstage/plugin-catalog-common": "workspace:^", "@backstage/plugin-catalog-react": "workspace:^", + "@backstage/ui": "workspace:^", "@material-ui/core": "^4.12.2", "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.61", diff --git a/plugins/org/report.api.md b/plugins/org/report.api.md index 13143d0e4c..0d6b15b308 100644 --- a/plugins/org/report.api.md +++ b/plugins/org/report.api.md @@ -18,7 +18,6 @@ export type ComponentsGridClassKey = // @public (undocumented) export const EntityGroupProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; }) => JSX_2.Element; @@ -48,7 +47,6 @@ export type EntityRelationAggregation = 'direct' | 'aggregated'; // @public (undocumented) export const EntityUserProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; maxRelations?: number; hideIcons?: boolean; @@ -56,7 +54,6 @@ export const EntityUserProfileCard: (props: { // @public (undocumented) export const GroupProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; }) => JSX_2.Element; @@ -116,7 +113,6 @@ export type OwnershipCardClassKey = // @public (undocumented) export const UserProfileCard: (props: { - variant?: InfoCardVariants; showLinks?: boolean; maxRelations?: number; hideIcons?: boolean; diff --git a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx index 89eba12b97..5810a0447d 100644 --- a/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx +++ b/plugins/org/src/components/Cards/Group/GroupProfile/GroupProfileCard.tsx @@ -22,13 +22,7 @@ import { RELATION_PARENT_OF, stringifyEntityRef, } from '@backstage/catalog-model'; -import { - Avatar, - InfoCard, - InfoCardVariants, - Link, -} from '@backstage/core-components'; -import Box from '@material-ui/core/Box'; +import { Link } from '@backstage/core-components'; import IconButton from '@material-ui/core/IconButton'; import List from '@material-ui/core/List'; import ListItem from '@material-ui/core/ListItem'; @@ -36,6 +30,7 @@ import ListItemIcon from '@material-ui/core/ListItemIcon'; import ListItemText from '@material-ui/core/ListItemText'; import Tooltip from '@material-ui/core/Tooltip'; import { + EntityInfoCard, EntityRefLinks, catalogApiRef, getEntityRelations, @@ -57,6 +52,7 @@ import { catalogEntityRefreshPermission } from '@backstage/plugin-catalog-common import { useTranslationRef } from '@backstage/frontend-plugin-api'; import { orgTranslationRef } from '../../../../translation'; import { makeStyles } from '@material-ui/core/styles'; +import { Avatar, Flex, Box, Text } from '@backstage/ui'; const useStyles = makeStyles(theme => ({ container: { @@ -71,18 +67,21 @@ const useStyles = makeStyles(theme => ({ }, })); -const CardTitle = (props: { title: string }) => ( - - - {props.title} - +const CardTitle = (props: { title: string; pictureSrc?: string }) => ( + + + {props.title} + ); /** @public */ -export const GroupProfileCard = (props: { - variant?: InfoCardVariants; - showLinks?: boolean; -}) => { +export const GroupProfileCard = (props: { showLinks?: boolean }) => { + const { showLinks } = props; const catalogApi = useApi(catalogApiRef); const alertApi = useApi(alertApiRef); const { entity: group } = useEntity(); @@ -148,11 +147,9 @@ export const GroupProfileCard = (props: { ); return ( - } - subheader={description} - variant={props.variant} - action={ + } + headerActions={ <> {allowRefresh && canRefresh && ( } > - - + {description && {description}} + @@ -234,9 +231,9 @@ export const GroupProfileCard = (props: { secondary={t('groupProfileCard.listItemTitle.childGroups')} /> - {props?.showLinks && } + {showLinks && } - + ); }; diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx index 0463cc78ad..aa1bf978e6 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.stories.tsx @@ -55,7 +55,7 @@ export const Default = () => ( - + @@ -82,7 +82,7 @@ export const NoImage = () => ( - + @@ -134,7 +134,7 @@ export const ExtraDetails = () => ( - + diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx index b5932dfb8e..45936c564f 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.test.tsx @@ -51,7 +51,7 @@ describe('UserSummary Test', () => { it('Display Profile Card', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -61,10 +61,10 @@ describe('UserSummary Test', () => { ); expect(screen.getByText('calum-leavy@example.com')).toBeInTheDocument(); - expect(screen.getByAltText('Calum Leavy')).toHaveAttribute( - 'src', - 'https://example.com/staff/calum.jpeg', - ); + // BUI Avatar is decorative (aria-hidden), because the name is + // present as text right beside it. + expect(screen.getByRole('img', { hidden: true })).toBeInTheDocument(); + expect(screen.getByText('Calum Leavy')).toBeInTheDocument(); expect(screen.getByText('examplegroup').closest('a')).toHaveAttribute( 'href', '/catalog/default/group/examplegroup', @@ -87,7 +87,7 @@ describe('UserSummary Test', () => { it('Should limit the number of displayed groups in the card', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -109,7 +109,7 @@ describe('UserSummary Test', () => { it('Show more groups button when there are more user relations than the maximum', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -128,7 +128,7 @@ describe('UserSummary Test', () => { it('Hide more groups button when limit value is less than or equal to user relations', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -148,7 +148,7 @@ describe('UserSummary Test', () => { it('Hide all groups when max relations is equals to zero', async () => { await renderInTestApp( - + , { mountedRoutes: { @@ -193,7 +193,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { @@ -234,7 +234,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { @@ -285,7 +285,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { @@ -337,7 +337,7 @@ describe('Edit Button', () => { await renderInTestApp( - + , { mountedRoutes: { diff --git a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx index ba3ee9d561..e34adde670 100644 --- a/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx +++ b/plugins/org/src/components/Cards/User/UserProfileCard/UserProfileCard.tsx @@ -19,15 +19,10 @@ import { RELATION_MEMBER_OF, UserEntity, } from '@backstage/catalog-model'; -import { - Avatar, - InfoCard, - InfoCardVariants, - Link, -} from '@backstage/core-components'; +import { Link } from '@backstage/core-components'; +import { EntityInfoCard } from '@backstage/plugin-catalog-react'; +import { Avatar, Box, Flex, Text } from '@backstage/ui'; import { createStyles, makeStyles } from '@material-ui/core/styles'; -import Box from '@material-ui/core/Box'; -import Grid from '@material-ui/core/Grid'; import BaseButton from '@material-ui/core/ButtonBase'; import IconButton from '@material-ui/core/IconButton'; import List from '@material-ui/core/List'; @@ -52,7 +47,6 @@ import EditIcon from '@material-ui/icons/Edit'; import EmailIcon from '@material-ui/icons/Email'; import GroupIcon from '@material-ui/icons/Group'; import { LinksGroup } from '../../Meta'; -import PersonIcon from '@material-ui/icons/Person'; import { useCallback, useState } from 'react'; import { useTranslationRef } from '@backstage/frontend-plugin-api'; @@ -84,17 +78,21 @@ const useStyles = makeStyles( { name: 'PluginOrgUserProfileCard' }, ); -const CardTitle = (props: { title?: string }) => +const CardTitle = (props: { title: string; pictureSrc?: string }) => props.title ? ( - - - {props.title} - + + + {props.title} + ) : null; /** @public */ export const UserProfileCard = (props: { - variant?: InfoCardVariants; showLinks?: boolean; maxRelations?: number; hideIcons?: boolean; @@ -132,11 +130,9 @@ export const UserProfileCard = (props: { }); return ( - } - subheader={description} - variant={props.variant} - action={ + } + headerActions={ <> {entityMetadataEditUrl && ( } > - - - - + {description && {description}} + + + {profile?.email && ( + + + + + + + + {profile.email} + + + )} - - - {profile?.email && ( - - - - - - - - {profile.email} - - - )} - - {maxRelations === undefined || maxRelations > 0 ? ( - - - - - - - - - {maxRelations && memberOfRelations.length > maxRelations ? ( - <> - , - - {t('userProfileCard.moreGroupButtonTitle', { - number: String( - memberOfRelations.length - maxRelations, - ), - })} - - - ) : null} - - - ) : null} - {props?.showLinks && } - - - + {maxRelations === undefined || maxRelations > 0 ? ( + + + + + + + + + {maxRelations && memberOfRelations.length > maxRelations ? ( + <> + , + + {t('userProfileCard.moreGroupButtonTitle', { + number: String(memberOfRelations.length - maxRelations), + })} + + + ) : null} + + + ) : null} + {props?.showLinks && } + + - + ); }; diff --git a/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md b/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md index f41ab1d506..d1172aaceb 100644 --- a/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md +++ b/plugins/techdocs-backend/examples/documented-component-uffizzi/docs/code/code-sample.md @@ -10,7 +10,7 @@ const serviceEntityPage = ( - + diff --git a/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md b/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md index f41ab1d506..d1172aaceb 100644 --- a/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md +++ b/plugins/techdocs-backend/examples/documented-component/docs/code/code-sample.md @@ -10,7 +10,7 @@ const serviceEntityPage = ( - + diff --git a/yarn.lock b/yarn.lock index 634d7ade79..5d2d15c94c 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6525,6 +6525,7 @@ __metadata: "@backstage/plugin-permission-react": "workspace:^" "@backstage/test-utils": "workspace:^" "@backstage/types": "workspace:^" + "@backstage/ui": "workspace:^" "@material-ui/core": "npm:^4.12.2" "@material-ui/icons": "npm:^4.9.1" "@material-ui/lab": "npm:4.0.0-alpha.61"