From a9bfcbe6fd627ebd41fd145b08e63c1d6600d7e9 Mon Sep 17 00:00:00 2001 From: Johan Persson Date: Thu, 26 Feb 2026 16:35:55 +0100 Subject: [PATCH] feat(catalog-react): add EntityInfoCard BUI wrapper component Signed-off-by: Johan Persson --- plugins/catalog-react/report.api.md | 17 +++ .../EntityInfoCard/EntityInfoCard.test.tsx | 102 ++++++++++++++++++ .../EntityInfoCard/EntityInfoCard.tsx | 56 ++++++++++ .../src/components/EntityInfoCard/index.ts | 18 ++++ plugins/catalog-react/src/components/index.ts | 1 + 5 files changed, 194 insertions(+) create mode 100644 plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx create mode 100644 plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx create mode 100644 plugins/catalog-react/src/components/EntityInfoCard/index.ts 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..9fa1e351ff --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.test.tsx @@ -0,0 +1,102 @@ +/* + * 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. + */ + +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..29bcd59fdf --- /dev/null +++ b/plugins/catalog-react/src/components/EntityInfoCard/EntityInfoCard.tsx @@ -0,0 +1,56 @@ +/* + * 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. + */ + +import { ReactNode } from 'react'; +import { + Card, + CardHeader, + CardBody, + CardFooter, + Text, + Flex, +} from '@backstage/ui'; + +/** @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; + + return ( + + {title && ( + + + + {title} + + {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';