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"