Merge pull request #33997 from backstage/charlesdedreuille/act-355-header-improvements

feat(ui): add description, tags, and metadata props to Header
This commit is contained in:
Charles de Dreuille
2026-04-24 13:59:56 +01:00
committed by GitHub
25 changed files with 1124 additions and 39 deletions
@@ -1,6 +1,8 @@
'use client';
import { Header } from '../../../../../packages/ui/src/components/Header/Header';
import { HeaderMetadataUsers } from '../../../../../packages/ui/src/components/Header/HeaderMetadataUsers';
import { HeaderMetadataStatus } from '../../../../../packages/ui/src/components/Header/HeaderMetadataStatus';
import { Button } from '../../../../../packages/ui/src/components/Button/Button';
import { ButtonIcon } from '../../../../../packages/ui/src/components/ButtonIcon/ButtonIcon';
import {
@@ -11,6 +13,29 @@ import {
import { MemoryRouter } from 'react-router-dom';
import { RiMore2Line } from '@remixicon/react';
const users = {
giles: {
name: 'Giles Peyton-Nicoll',
src: 'https://i.pravatar.cc/150?u=giles',
href: '/users/giles',
},
alice: {
name: 'Alice Johnson',
src: 'https://i.pravatar.cc/150?u=alice42',
href: '/users/alice',
},
bob: {
name: 'Bob Smith',
src: 'https://i.pravatar.cc/150?u=bob',
href: '/users/bob',
},
carol: {
name: 'Carol Williams',
src: 'https://i.pravatar.cc/150?u=carol',
href: '/users/carol',
},
};
const tabs = [
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'checks', label: 'Checks', href: '/checks' },
@@ -29,12 +54,37 @@ const breadcrumbs = [
},
];
const tags = [
{ label: 'TypeScript' },
{ label: 'Platform', href: '/platform' },
];
const metadataUsers = [
{ label: 'Type', value: 'website' },
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Owner',
value: <HeaderMetadataUsers users={[users.giles]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers users={[users.alice, users.bob, users.carol]} />
),
},
];
export const WithEverything = () => (
<MemoryRouter initialEntries={['/overview']}>
<Header
title="Page Title"
tags={tags}
description="A short description of this page. Supports [inline links](https://backstage.io)."
metadata={metadataUsers}
tabs={tabs.slice(0, 2)}
breadcrumbs={breadcrumbs.slice(0, 2)}
customActions={
<>
<Button variant="secondary">Secondary</Button>
@@ -45,6 +95,84 @@ export const WithEverything = () => (
</MemoryRouter>
);
export const WithMetadataUsers = () => (
<MemoryRouter>
<Header
title="Page Title"
metadata={[
{ label: 'Type', value: 'website' },
{
label: 'Owner',
value: <HeaderMetadataUsers users={[users.giles]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers
users={[users.alice, users.bob, users.carol]}
/>
),
},
]}
/>
</MemoryRouter>
);
export const WithTags = () => (
<MemoryRouter>
<Header title="Page Title" tags={tags} />
</MemoryRouter>
);
export const WithDescription = () => (
<MemoryRouter>
<Header
title="Page Title"
description="A short description of this page. Supports [inline links](https://backstage.io)."
/>
</MemoryRouter>
);
export const WithMetadata = () => (
<MemoryRouter>
<Header
title="Page Title"
metadata={[
{ label: 'Owner', value: 'platform-team' },
{ label: 'Type', value: 'website' },
]}
/>
</MemoryRouter>
);
export const WithMetadataStatus = () => (
<MemoryRouter>
<Header
title="Page Title"
metadata={[
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Build',
value: (
<HeaderMetadataStatus
label="Failed"
color="danger"
href="/builds/123"
/>
),
},
{
label: 'Coverage',
value: <HeaderMetadataStatus label="Warning" color="warning" />,
},
]}
/>
</MemoryRouter>
);
export const WithLongBreadcrumbs = () => (
<MemoryRouter>
<Header title="Page Title" breadcrumbs={breadcrumbs.slice(0, 2)} />
+44 -7
View File
@@ -3,17 +3,28 @@ import { CodeBlock } from '@/components/CodeBlock';
import { Snippet } from '@/components/Snippet';
import {
WithEverything,
WithLongBreadcrumbs,
WithTabs,
WithTags,
WithDescription,
WithMetadata,
WithMetadataUsers,
WithMetadataStatus,
WithCustomActions,
WithMenu,
} from './components';
import { headerPagePropDefs } from './props-definition';
import {
headerPagePropDefs,
headerMetadataUsersPropDefs,
} from './props-definition';
import {
usage,
defaultSnippet,
withTabs,
withBreadcrumbs,
withTags,
withDescription,
withMetadata,
withMetadataUsers,
withMetadataStatus,
withCustomActions,
withMenu,
} from './snippets';
@@ -24,7 +35,7 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
<PageTitle
title="Header"
description="A secondary header with title, breadcrumbs, tabs, and actions."
description="A secondary header with title, tags, description, metadata, tabs, and actions."
/>
<Snippet py={4} preview={<WithEverything />} code={defaultSnippet} />
@@ -39,11 +50,37 @@ import { ChangelogComponent } from '@/components/ChangelogComponent';
## Examples
### Breadcrumbs
### Tags
Labels are truncated at 240px.
Tags are rendered above the title. Each tag with an `href` renders as a link; tags without `href` render as plain text. Tags are separated by a small circle divider.
<Snippet open preview={<WithLongBreadcrumbs />} code={withBreadcrumbs} />
<Snippet open preview={<WithTags />} code={withTags} />
### Description
The description accepts a markdown string with support for inline links. Bold, italic, and block-level markdown are not rendered.
<Snippet open preview={<WithDescription />} code={withDescription} />
### Metadata
Key-value pairs displayed below the description.
<Snippet open preview={<WithMetadata />} code={withMetadata} />
### Metadata with users
Use `HeaderMetadataUsers` as the metadata value to display users as avatars. A single user shows the avatar with their name beside it. Multiple users show a row of avatars — hover to reveal each name via tooltip. When a user has an `href`, the avatar and name become links.
<Snippet open preview={<WithMetadataUsers />} code={withMetadataUsers} />
<PropsTable data={headerMetadataUsersPropDefs} />
### Metadata with status
Use `HeaderMetadataStatus` as the metadata value to display a status indicator. The dot colour is driven by the `color` prop which maps to BUI status tokens. Pass an `href` to make the label a link.
<Snippet open preview={<WithMetadataStatus />} code={withMetadataStatus} />
### Tabs
@@ -5,6 +5,51 @@ export const headerPagePropDefs: Record<string, PropDef> = {
type: 'string',
description: 'Page heading displayed in the header.',
},
tags: {
type: 'complex',
description:
'Items displayed above the title. Each tag renders as a link when href is provided, or as plain text otherwise. Tags are separated by a small circle divider.',
complexType: {
name: 'HeaderTag[]',
properties: {
label: {
type: 'string',
required: true,
description: 'Display text for the tag.',
},
href: {
type: 'string',
required: false,
description: 'URL to navigate to when the tag is clicked.',
},
},
},
},
description: {
type: 'string',
description:
'Markdown string rendered below the title. Only inline links are supported. Bold, italic, and block-level markdown are not rendered.',
},
metadata: {
type: 'complex',
description: 'Key-value pairs displayed below the description.',
complexType: {
name: 'HeaderMetadataItem[]',
properties: {
label: {
type: 'string',
required: true,
description: 'The key label, displayed in secondary color.',
},
value: {
type: 'string | ReactNode',
required: true,
description:
'The value to display alongside the label. Pass a string for plain text or a ReactNode for custom content such as HeaderMetadataUsers.',
},
},
},
},
customActions: {
type: 'enum',
values: ['ReactNode'],
@@ -49,6 +94,7 @@ export const headerPagePropDefs: Record<string, PropDef> = {
},
breadcrumbs: {
type: 'complex',
deprecated: true,
description: 'Breadcrumb trail displayed above the title.',
complexType: {
name: 'HeaderBreadcrumb[]',
@@ -68,3 +114,33 @@ export const headerPagePropDefs: Record<string, PropDef> = {
},
...classNamePropDefs,
};
export const headerMetadataUsersPropDefs: Record<string, PropDef> = {
users: {
type: 'complex',
description:
'List of users to display. A single user shows the avatar with their name beside it. Multiple users show a row of avatars with names revealed on hover via tooltip.',
complexType: {
name: 'HeaderMetadataUser[]',
properties: {
name: {
type: 'string',
required: true,
description:
'Display name shown beside the avatar (single) or in the tooltip (multiple).',
},
src: {
type: 'string',
required: false,
description: 'URL for the avatar image.',
},
href: {
type: 'string',
required: false,
description:
'When provided, the avatar becomes a link and the name is rendered as a Link component.',
},
},
},
},
};
+98 -5
View File
@@ -2,15 +2,41 @@ export const usage = `import { Header } from '@backstage/ui';
<Header title="Page Title" />`;
export const defaultSnippet = `<Header
export const defaultSnippet = `import { Header, HeaderMetadataUsers, HeaderMetadataStatus } from '@backstage/ui';
<Header
title="Page Title"
breadcrumbs={[
{ label: 'Home', href: '/' },
{ label: 'Dashboard', href: '/dashboard' },
tags={[
{ label: 'TypeScript' },
{ label: 'Platform', href: '/platform' },
]}
description="A short description. Supports [inline links](https://backstage.io)."
metadata={[
{ label: 'Type', value: 'website' },
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Owner',
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers
users={[
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
]}
/>
),
},
]}
tabs={[
{ id: 'overview', label: 'Overview', href: '/overview' },
{ id: 'settings', label: 'Settings', href: '/settings' },
{ id: 'checks', label: 'Checks', href: '/checks' },
]}
customActions={
<>
@@ -54,3 +80,70 @@ export const withMenu = `<Header
</MenuTrigger>
}
/>`;
export const withTags = `<Header
title="Page Title"
tags={[
{ label: 'TypeScript' },
{ label: 'Platform', href: '/platform' },
{ label: 'Gold' },
]}
/>`;
export const withDescription = `<Header
title="Page Title"
description="A short description. Supports [inline links](https://backstage.io)."
/>`;
export const withMetadata = `<Header
title="Page Title"
metadata={[
{ label: 'Owner', value: 'platform-team' },
{ label: 'Type', value: 'website' },
]}
/>`;
export const withMetadataStatus = `import { Header, HeaderMetadataStatus } from '@backstage/ui';
<Header
title="Page Title"
metadata={[
{
label: 'Status',
value: <HeaderMetadataStatus label="Passing" color="success" />,
},
{
label: 'Build',
value: <HeaderMetadataStatus label="Failed" color="danger" href="/builds/123" />,
},
{
label: 'Coverage',
value: <HeaderMetadataStatus label="Warning" color="warning" />,
},
]}
/>`;
export const withMetadataUsers = `import { Header, HeaderMetadataUsers } from '@backstage/ui';
<Header
title="Page Title"
metadata={[
{ label: 'Type', value: 'website' },
{
label: 'Owner',
value: <HeaderMetadataUsers users={[{ name: 'Giles Peyton-Nicoll', src: '...', href: '/users/giles' }]} />,
},
{
label: 'Contributors',
value: (
<HeaderMetadataUsers
users={[
{ name: 'Alice Johnson', src: '...', href: '/users/alice' },
{ name: 'Bob Smith', src: '...', href: '/users/bob' },
{ name: 'Carol Williams', src: '...', href: '/users/carol' },
]}
/>
),
},
]}
/>`;
+7 -1
View File
@@ -4,12 +4,18 @@ import styles from './styles.module.css';
export const Chip = ({
children,
head = false,
deprecated = false,
}: {
children: ReactNode;
head?: boolean;
deprecated?: boolean;
}) => {
return (
<span className={`${styles.chip} ${head ? styles.head : ''}`}>
<span
className={`${styles.chip} ${head ? styles.head : ''} ${
deprecated ? styles.deprecated : ''
}`}
>
{children}
</span>
);
@@ -14,6 +14,11 @@
color: #2563eb;
}
.deprecated {
background-color: #fff4e5;
color: #b45309;
}
[data-theme-mode='dark'] .chip {
background-color: #2c2c2c;
color: #fff;
@@ -22,3 +27,8 @@
[data-theme-mode='dark'] .chip.head {
background-color: #33405b;
}
[data-theme-mode='dark'] .chip.deprecated {
background-color: #3d2a10;
color: #fbbf24;
}
@@ -52,7 +52,12 @@ export const PropsTable = <T extends Record<string, PropData>>({
switch (column) {
case 'prop':
return <Chip head>{propName}</Chip>;
return (
<div style={{ display: 'flex', flexWrap: 'wrap', gap: '0.375rem' }}>
<Chip head>{propName}</Chip>
{propData.deprecated && <Chip deprecated>deprecated</Chip>}
</div>
);
case 'type':
return (
+1
View File
@@ -44,6 +44,7 @@ export type PropDef = {
required?: boolean;
responsive?: boolean;
description?: ReactNode;
deprecated?: boolean;
};
export { breakpoints };