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:
@@ -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)} />
|
||||
|
||||
@@ -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.',
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -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' },
|
||||
]}
|
||||
/>
|
||||
),
|
||||
},
|
||||
]}
|
||||
/>`;
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -44,6 +44,7 @@ export type PropDef = {
|
||||
required?: boolean;
|
||||
responsive?: boolean;
|
||||
description?: ReactNode;
|
||||
deprecated?: boolean;
|
||||
};
|
||||
|
||||
export { breakpoints };
|
||||
|
||||
Reference in New Issue
Block a user