Merge pull request #33173 from drodil/ai_bep
docs(bep): define shared AI assets repo and cli support
This commit is contained in:
@@ -0,0 +1,236 @@
|
||||
---
|
||||
title: Backstage AI Skills Publishing and Discovery
|
||||
status: implementable
|
||||
authors:
|
||||
- '@drodil'
|
||||
owners:
|
||||
- '@backstage/maintainers'
|
||||
project-areas:
|
||||
- core
|
||||
creation-date: 2026-03-06
|
||||
---
|
||||
|
||||
# BEP: AI skills for Backstage development
|
||||
|
||||
- [Summary](#summary)
|
||||
- [Motivation](#motivation)
|
||||
- [Goals](#goals)
|
||||
- [Non-Goals](#non-goals)
|
||||
- [Proposal](#proposal)
|
||||
- [Design Details](#design-details)
|
||||
- [Release Plan](#release-plan)
|
||||
- [Dependencies](#dependencies)
|
||||
- [Alternatives](#alternatives)
|
||||
|
||||
## Summary
|
||||
|
||||
This BEP proposes that curated Backstage AI skills are authored in the main Backstage
|
||||
repository and published separately to a well-known endpoint on `backstage.io`. Instead
|
||||
of creating a new Backstage-owned repository and custom discovery protocol, Backstage
|
||||
will publish skills using the well-known layout already supported by [`skills.sh`](https://skills.sh/).
|
||||
|
||||
The published output can be released on its own publishing cadence even though the source
|
||||
content lives in the main monorepo.
|
||||
|
||||
The primary consumer workflow becomes:
|
||||
|
||||
- `npx skills add https://backstage.io` to discover and install all published Backstage skills.
|
||||
- `npx skills add https://backstage.io/.well-known/skills/<skill-name>` to install a specific skill.
|
||||
|
||||
This keeps Backstage aligned with an existing ecosystem convention, reduces maintenance
|
||||
surface area, and still gives adopters a clear, official source for reusable AI guidance.
|
||||
Prompt templates, instructions, and supporting files can still be shipped, but they are
|
||||
packaged within skill directories instead of through a separate Backstage-specific asset system.
|
||||
|
||||
## Motivation
|
||||
|
||||
Backstage adopters are increasingly using AI-assisted workflows for plugin development,
|
||||
migration, maintenance, and documentation. Today, prompt and skill quality varies
|
||||
significantly across adopters, and there is no common, well-known channel for sharing proven
|
||||
guidance. A lot of adopters already have assets that help AI-assisted development and
|
||||
could be shared with the community.
|
||||
|
||||
As a result:
|
||||
|
||||
- Teams duplicate effort by recreating similar prompts and instructions.
|
||||
- Effective practices are hard to discover and spread.
|
||||
- Shared guidance is often published in tool-specific or organization-specific layouts.
|
||||
- Security and governance concerns are often handled ad hoc.
|
||||
|
||||
Backstage should solve this in the simplest interoperable way. Creating a dedicated asset
|
||||
repository, a Backstage-specific discovery index, and custom install/update tooling would
|
||||
increase maintenance cost while duplicating functionality that `skills.sh` already provides.
|
||||
|
||||
By publishing official Backstage skills through a well-known endpoint on `backstage.io`,
|
||||
Backstage can provide a reliable baseline for AI-enabled development while preserving local
|
||||
extension and customization and staying aligned with an existing ecosystem workflow.
|
||||
|
||||
### Goals
|
||||
|
||||
- Author official Backstage AI skills in the main Backstage repository.
|
||||
- Publish those skills separately to a well-known endpoint on `backstage.io`.
|
||||
- Reuse `skills.sh` discovery and installation behavior instead of inventing a Backstage-specific protocol.
|
||||
- Define a stable source layout and review process for Backstage-authored skills.
|
||||
- Allow skill directories to include prompts, instructions, and supporting files needed by each skill.
|
||||
- Publish contributor and review guidelines so assets remain high quality and safe.
|
||||
- Provide clear docs for adopters on how to install and customize the published skills.
|
||||
|
||||
### Non-Goals
|
||||
|
||||
- Building or hosting a new LLM inference platform.
|
||||
- Standardizing model provider APIs across all Backstage AI integrations.
|
||||
- Automatically executing AI-generated code in adopter repositories.
|
||||
- Enforcing centralized telemetry collection from adopter environments.
|
||||
- Replacing existing organization-specific prompt repositories. Local assets remain supported.
|
||||
- Solving all possible AI governance policy differences between organizations.
|
||||
- Creating a separate Backstage-owned repository just for AI skills.
|
||||
- Building a Backstage-specific discovery index, manifest, or asset registry.
|
||||
- Supporting bundles, lockfiles, version pinning, or custom update flows in this first iteration.
|
||||
|
||||
## Proposal
|
||||
|
||||
Introduce three deliverables:
|
||||
|
||||
1. A Backstage monorepo location that serves as the source of truth for Backstage-authored AI skills.
|
||||
1. A publishing workflow that exposes approved skills at `https://backstage.io/.well-known/skills/`.
|
||||
1. Documentation for consuming those skills through `skills.sh`
|
||||
|
||||
The Backstage source content will contain:
|
||||
|
||||
- Reusable skills for common Backstage engineering tasks.
|
||||
- Skill definitions, instructions, prompt fragments, and supporting files packaged per skill.
|
||||
- Lightweight metadata needed for publication and review.
|
||||
- Validation and quality checks executed in CI.
|
||||
|
||||
The primary consumer workflow will use `skills.sh` directly:
|
||||
|
||||
- `npx skills add https://backstage.io`
|
||||
- `npx skills add https://backstage.io/.well-known/skills/<skill-name>`
|
||||
|
||||
If Backstage later adds CLI support, it should remain intentionally thin:
|
||||
|
||||
- It should default to the Backstage well-known endpoint.
|
||||
- It should delegate to `skills.sh` semantics rather than defining its own registry or manifest.
|
||||
- It should not introduce separate bundle, version, or update concepts.
|
||||
|
||||
This approach keeps discovery and installation interoperable with the broader ecosystem,
|
||||
while still letting Backstage publish curated guidance from an official source.
|
||||
|
||||
## Design Details
|
||||
|
||||
### Source and publishing design
|
||||
|
||||
The source of truth for Backstage skills will live in the Backstage monorepo. The exact
|
||||
directory can be finalized during implementation, but it should provide a predictable layout
|
||||
for authoring, review, and publication.
|
||||
|
||||
Each skill directory should be self-contained and may include:
|
||||
|
||||
- `SKILL.md` as the primary skill entry point.
|
||||
- Supporting prompt or instruction files referenced by the skill.
|
||||
- Optional metadata files used by Backstage publishing workflows.
|
||||
|
||||
Published output on `backstage.io` will follow the `skills.sh` well-known layout:
|
||||
|
||||
- `https://backstage.io/.well-known/skills/index.json`
|
||||
- `https://backstage.io/.well-known/skills/<skill-name>/SKILL.md`
|
||||
- Additional files under `https://backstage.io/.well-known/skills/<skill-name>/...`
|
||||
|
||||
The published index should contain entries with the current `skills.sh` well-known shape:
|
||||
|
||||
- `name`: skill identifier, matching the published directory name.
|
||||
- `description`: short description of the skill.
|
||||
- `files`: all published files in the skill directory.
|
||||
|
||||
Example published layout:
|
||||
|
||||
```text
|
||||
.well-known/skills/index.json
|
||||
.well-known/skills/plugin-authoring/SKILL.md
|
||||
.well-known/skills/plugin-authoring/prompts/review.md
|
||||
```
|
||||
|
||||
Example index structure:
|
||||
|
||||
```json
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"name": "plugin-authoring",
|
||||
"description": "Guidance for creating and reviewing Backstage plugins.",
|
||||
"files": ["SKILL.md", "prompts/review.md"]
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Backstage CI should validate the source skill layout, ensure published file references are
|
||||
correct, and generate the well-known endpoint contents as part of the publishing workflow.
|
||||
|
||||
### Consumer workflow
|
||||
|
||||
The main adoption path is documentation, not new tooling:
|
||||
|
||||
- Users install Backstage skills with `npx skills add https://backstage.io`.
|
||||
- Users can install a specific published skill by pointing at its well-known skill URL.
|
||||
- Any install behavior, target path decisions, or local merge semantics remain the responsibility of `skills.sh`.
|
||||
|
||||
### Extensibility
|
||||
|
||||
Adopters can:
|
||||
|
||||
- Keep local-only skills alongside installed Backstage skills.
|
||||
- Customize installed files after adding them to their repository.
|
||||
- Publish their own well-known skills endpoints independently of Backstage.
|
||||
|
||||
Backstage should not block these local workflows by requiring a proprietary asset format.
|
||||
|
||||
## Release Plan
|
||||
|
||||
Phase 1: Repository bootstrap
|
||||
|
||||
- Define the Backstage monorepo location and review guidelines for skills.
|
||||
- Seed the source tree with a small set of high-value Backstage skills.
|
||||
- Validate contributor workflow and CI checks for source content.
|
||||
|
||||
Phase 2: Publishing rollout
|
||||
|
||||
- Implement a publishing workflow that generates `/.well-known/skills/` content for `backstage.io`.
|
||||
- Publish `index.json` and the initial skill directories.
|
||||
- Validate the published endpoint against `skills.sh` consumption behavior.
|
||||
|
||||
Phase 3: General availability
|
||||
|
||||
- Publish adoption documentation centered on `npx skills add https://backstage.io`.
|
||||
- Define support and ownership model for ongoing skill curation and publishing.
|
||||
- Evaluate whether an optional thin `backstage-cli` wrapper adds enough value to justify maintenance.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- `skills.sh` continuing to support the current well-known endpoint format.
|
||||
- A Backstage publishing workflow that can emit static files to `backstage.io/.well-known/skills/`.
|
||||
- Agreement on long-term ownership and governance model for Backstage-authored skills.
|
||||
- Documentation updates describing authoring, publishing, and adopter usage patterns.
|
||||
|
||||
## Alternatives
|
||||
|
||||
1. Keep AI assets fully decentralized in individual adopter repositories.
|
||||
|
||||
This preserves autonomy but does not solve discovery, reuse, or consistency. It also increases duplicated maintenance
|
||||
effort across adopters.
|
||||
|
||||
2. Create a separate Backstage-owned repository for AI assets.
|
||||
|
||||
This was the original direction, but it adds repository sprawl and still requires a distribution mechanism. Keeping the
|
||||
source in the main monorepo simplifies governance and keeps the content closer to the Backstage code and docs it
|
||||
describes.
|
||||
|
||||
3. Build a Backstage-specific discovery and installation protocol.
|
||||
|
||||
This would duplicate functionality already available through `skills.sh` and create a long-term maintenance burden for
|
||||
limited short-term benefit.
|
||||
|
||||
4. Publish assets only as documentation in the Backstage docs site.
|
||||
|
||||
This improves discoverability, but without the well-known `skills.sh` layout it does not provide a direct installation
|
||||
path for users.
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"skills": [
|
||||
{
|
||||
"name": "mui-to-bui-migration",
|
||||
"description": "Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (BUI). Use this skill when migrating components, updating imports, replacing styling patterns, or converting MUI components to their BUI equivalents.",
|
||||
"files": ["SKILL.md"]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,781 @@
|
||||
---
|
||||
name: mui-to-bui-migration
|
||||
description: Migrate Backstage plugins from Material-UI (MUI) to Backstage UI (BUI). Use this skill when migrating components, updating imports, replacing styling patterns, or converting MUI components to their BUI equivalents.
|
||||
---
|
||||
|
||||
# MUI to BUI Migration Skill
|
||||
|
||||
This skill helps migrate Backstage plugins from Material-UI (@material-ui/core, @material-ui/icons) to Backstage UI (
|
||||
@backstage/ui).
|
||||
|
||||
## Prerequisites
|
||||
|
||||
Before starting migration:
|
||||
|
||||
1. Install the BUI package:
|
||||
|
||||
```bash
|
||||
yarn add @backstage/ui
|
||||
```
|
||||
|
||||
2. Add the CSS import to your root file (typically `src/index.ts` or app entry point):
|
||||
```typescript
|
||||
import '@backstage/ui/css/styles.css';
|
||||
```
|
||||
|
||||
## Available BUI Components
|
||||
|
||||
### Layout Components
|
||||
|
||||
- `Box` - Basic layout container with CSS properties
|
||||
- `Container` - Centered content container with max-width
|
||||
- `Grid` - CSS Grid-based layout (`Grid.Root`, `Grid.Item`)
|
||||
- `Flex` - `Flexbox` layout component
|
||||
|
||||
### UI Components
|
||||
|
||||
- `Accordion` - Collapsible content panels
|
||||
- `Avatar` - User/entity avatars
|
||||
- `Button` - Primary action buttons (`variant="primary"`, `variant="secondary"`, `isDisabled`)
|
||||
- `ButtonIcon` - Icon-only buttons (`icon`, `onPress`, `variant`)
|
||||
- `ButtonLink` - Link styled as button
|
||||
- `Card` - Content cards (`Card`, `CardHeader`, `CardBody`, `CardFooter`)
|
||||
- `Checkbox` - Checkbox input
|
||||
- `Dialog` - Modal dialogs (`DialogTrigger`, `Dialog`, `DialogHeader`, `DialogBody`, `DialogFooter`)
|
||||
- `Header` - Page headers
|
||||
- `HeaderPage` - Full page header component
|
||||
- `Link` - Navigation links
|
||||
- `Menu` - Dropdown menus (`MenuTrigger`, `Menu`, `MenuItem`)
|
||||
- `PasswordField` - Password input field
|
||||
- `Popover` - Popover overlays
|
||||
- `RadioGroup` - Radio button groups
|
||||
- `SearchField` - Search input
|
||||
- `Select` - Dropdown select
|
||||
- `Skeleton` - Loading skeleton
|
||||
- `Switch` - Toggle switch
|
||||
- `Table` - Data tables
|
||||
- `Tabs` - Tab navigation (`Tabs`, `TabList`, `Tab`, `TabPanel`)
|
||||
- `Tag` - Tag/chip component (replaces MUI Chip)
|
||||
- `TagGroup` - Tag/chip groups
|
||||
- `Text` - Typography component (`variant`, `color`)
|
||||
- `TextField` - Text input (`isRequired`, `onChange` receives string directly)
|
||||
- `ToggleButton` - Toggle buttons
|
||||
- `ToggleButtonGroup` - Grouped toggle buttons
|
||||
- `Tooltip` - Tooltip overlays (used with TooltipTrigger from react-aria-components)
|
||||
- `VisuallyHidden` - Accessibility helper
|
||||
|
||||
### Hooks
|
||||
|
||||
- `useBreakpoint` - Responsive breakpoint hook
|
||||
|
||||
## Migration Patterns
|
||||
|
||||
### 1. Import Changes
|
||||
|
||||
**Remove MUI imports:**
|
||||
|
||||
```typescript
|
||||
// REMOVE these imports
|
||||
import { Box, Typography, Tooltip, Paper } from '@material-ui/core';
|
||||
import { makeStyles, Theme } from '@material-ui/core/styles';
|
||||
import SomeIcon from '@material-ui/icons/SomeIcon';
|
||||
```
|
||||
|
||||
**Add BUI imports:**
|
||||
|
||||
```typescript
|
||||
// ADD these imports
|
||||
import { Box, Flex, Text, Tooltip, Card } from '@backstage/ui';
|
||||
import { RiSomeIcon } from '@remixicon/react';
|
||||
import styles from './MyComponent.module.css';
|
||||
```
|
||||
|
||||
### 2. Styling: `makeStyles` to CSS Modules
|
||||
|
||||
Create a `.module.css` file alongside your component using BUI CSS variables.
|
||||
|
||||
**Before (MUI `makeStyles`):**
|
||||
|
||||
```typescript
|
||||
// MyComponent.tsx
|
||||
import {makeStyles, Theme} from '@material-ui/core/styles';
|
||||
|
||||
const useStyles = makeStyles((theme: Theme) => ({
|
||||
container: {
|
||||
padding: theme.spacing(2),
|
||||
backgroundColor: theme.palette.background.paper,
|
||||
borderRadius: theme.shape.borderRadius,
|
||||
},
|
||||
title: {
|
||||
marginBottom: theme.spacing(1),
|
||||
color: theme.palette.text.primary,
|
||||
},
|
||||
listItem: {
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
},
|
||||
icon: {
|
||||
minWidth: 56,
|
||||
color: theme.palette.text.secondary,
|
||||
},
|
||||
}));
|
||||
|
||||
function MyComponent() {
|
||||
const classes = useStyles();
|
||||
return (
|
||||
<div className = {classes.container} >
|
||||
<Typography className = {classes.title} > Title < /Typography>
|
||||
< div
|
||||
className = {classes.listItem} >
|
||||
<div className = {classes.icon} >
|
||||
<SomeIcon / >
|
||||
</div>
|
||||
< span > Content < /span>
|
||||
< /div>
|
||||
< /div>
|
||||
)
|
||||
;
|
||||
}
|
||||
```
|
||||
|
||||
**After (CSS Modules with BUI variables):**
|
||||
|
||||
```css
|
||||
/* MyComponent.module.css */
|
||||
@layer components {
|
||||
.container {
|
||||
padding: var(--bui-space-4);
|
||||
background-color: var(--bui-bg-surface-1);
|
||||
border-radius: var(--bui-radius-2);
|
||||
}
|
||||
|
||||
.title {
|
||||
margin-bottom: var(--bui-space-2);
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: var(--bui-space-2) 0;
|
||||
}
|
||||
|
||||
.icon {
|
||||
min-width: 56px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: var(--bui-fg-secondary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
// MyComponent.tsx
|
||||
import {Box, Text} from '@backstage/ui';
|
||||
import {RiSomeIcon} from '@remixicon/react';
|
||||
import styles from './MyComponent.module.css';
|
||||
|
||||
function MyComponent() {
|
||||
return (
|
||||
<Box className = {styles.container} >
|
||||
<Text className = {styles.title} > Title < /Text>
|
||||
< div
|
||||
className = {styles.listItem} >
|
||||
<div className = {styles.icon} >
|
||||
<RiSomeIcon size = {24}
|
||||
/>
|
||||
< /div>
|
||||
< span > Content < /span>
|
||||
< /div>
|
||||
< /Box>
|
||||
)
|
||||
;
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Layout: Box with display to `Flex`
|
||||
|
||||
**Before (MUI Box with display prop):**
|
||||
|
||||
```typescript
|
||||
<Box
|
||||
display = "flex"
|
||||
flexDirection = "column"
|
||||
alignItems = "center"
|
||||
justifyContent = "space-between"
|
||||
>
|
||||
<Box display = "flex"
|
||||
flexDirection = "row"
|
||||
gap = {2} >
|
||||
{children}
|
||||
< /Box>
|
||||
< /Box>
|
||||
```
|
||||
|
||||
**After (BUI `Flex` component):**
|
||||
|
||||
```typescript
|
||||
<Flex direction = "column"
|
||||
align = "center"
|
||||
justify = "between" >
|
||||
<Flex direction = "row"
|
||||
style = {
|
||||
{
|
||||
gap: 'var(--bui-space-4)'
|
||||
}
|
||||
}>
|
||||
{
|
||||
children
|
||||
}
|
||||
</Flex>
|
||||
< /Flex>
|
||||
```
|
||||
|
||||
Note: BUI `Flex` uses `justify="between"` not `justify="space-between"`.
|
||||
|
||||
### 4. Grid Layout
|
||||
|
||||
**Before (MUI Grid):**
|
||||
|
||||
```typescript
|
||||
<Grid container
|
||||
spacing = {3} >
|
||||
<Grid item
|
||||
xs = {12}
|
||||
md = {6} >
|
||||
{content}
|
||||
< /Grid>
|
||||
< /Grid>
|
||||
```
|
||||
|
||||
**After (BUI Grid):**
|
||||
|
||||
```typescript
|
||||
<Grid.Root columns = {
|
||||
{
|
||||
sm: '12'
|
||||
}
|
||||
}
|
||||
gap = "6" >
|
||||
<Grid.Item colSpan = {
|
||||
{
|
||||
sm: '12', md
|
||||
:
|
||||
'6'
|
||||
}
|
||||
}>
|
||||
{
|
||||
content
|
||||
}
|
||||
</Grid.Item>
|
||||
< /Grid.Root>
|
||||
```
|
||||
|
||||
### 5. Typography to Text
|
||||
|
||||
**Before (MUI Typography):**
|
||||
|
||||
```typescript
|
||||
<Typography variant = "h1" > Heading < /Typography>
|
||||
< Typography
|
||||
variant = "h6" > Subheading < /Typography>
|
||||
< Typography
|
||||
variant = "body1" > Body
|
||||
text < /Typography>
|
||||
< Typography
|
||||
variant = "body2"
|
||||
color = "textSecondary" > Secondary
|
||||
text < /Typography>
|
||||
```
|
||||
|
||||
**After (BUI Text):**
|
||||
|
||||
```typescript
|
||||
<Text variant = "title-large" > Heading < /Text>
|
||||
< Text
|
||||
variant = "title-small" > Subheading < /Text>
|
||||
< Text
|
||||
variant = "body-medium" > Body
|
||||
text < /Text>
|
||||
< Text
|
||||
variant = "body-small"
|
||||
color = "secondary" > Secondary
|
||||
text < /Text>
|
||||
```
|
||||
|
||||
Valid Text variants: `title-large`, `title-medium`, `title-small`, `title-x-small`, `body-large`, `body-medium`,
|
||||
`body-small`, `body-x-small`
|
||||
|
||||
### 6. Tooltip Pattern
|
||||
|
||||
**Before (MUI Tooltip):**
|
||||
|
||||
```typescript
|
||||
import {Tooltip, Typography} from '@material-ui/core';
|
||||
|
||||
<Tooltip title = { < Typography > Tooltip
|
||||
content < /Typography>}>
|
||||
< span > Hover
|
||||
me < /span>
|
||||
< /Tooltip>;
|
||||
```
|
||||
|
||||
**After (BUI TooltipTrigger pattern):**
|
||||
|
||||
```typescript
|
||||
import {Tooltip, Text} from '@backstage/ui';
|
||||
import {TooltipTrigger} from 'react-aria-components';
|
||||
|
||||
<TooltipTrigger>
|
||||
<Text>Hover
|
||||
me < /Text>
|
||||
< Tooltip > Tooltip
|
||||
content < /Tooltip>
|
||||
< /TooltipTrigger>;
|
||||
```
|
||||
|
||||
Note: Add `react-aria-components` to your dependencies.
|
||||
|
||||
### 7. Dialog Pattern
|
||||
|
||||
**Before (MUI Dialog):**
|
||||
|
||||
```typescript
|
||||
import {Dialog, DialogTitle, DialogActions, Button} from '@material-ui/core';
|
||||
|
||||
<Dialog open = {isOpen}
|
||||
onClose = {onClose} >
|
||||
<DialogTitle>Title < /DialogTitle>
|
||||
< DialogActions >
|
||||
<Button onClick = {onClose} > Cancel < /Button>
|
||||
< Button
|
||||
onClick = {onConfirm}
|
||||
color = "primary" >
|
||||
Confirm
|
||||
< /Button>
|
||||
< /DialogActions>
|
||||
< /Dialog>;
|
||||
```
|
||||
|
||||
**After (BUI Dialog):**
|
||||
|
||||
```typescript
|
||||
import {
|
||||
Dialog,
|
||||
DialogTrigger,
|
||||
DialogHeader,
|
||||
DialogFooter,
|
||||
Button,
|
||||
} from '@backstage/ui';
|
||||
|
||||
<DialogTrigger>
|
||||
<Dialog
|
||||
isOpen = {isOpen}
|
||||
isDismissable
|
||||
onOpenChange = {open
|
||||
=>
|
||||
{
|
||||
if (!open) onClose();
|
||||
}
|
||||
}
|
||||
>
|
||||
<DialogHeader>Title < /DialogHeader>
|
||||
< DialogFooter >
|
||||
<Button onClick = {onConfirm}
|
||||
variant = "primary" >
|
||||
Confirm
|
||||
< /Button>
|
||||
< Button
|
||||
onClick = {onClose}
|
||||
variant = "secondary"
|
||||
slot = "close" >
|
||||
Cancel
|
||||
< /Button>
|
||||
< /DialogFooter>
|
||||
< /Dialog>
|
||||
< /DialogTrigger>;
|
||||
```
|
||||
|
||||
### 8. Button Changes
|
||||
|
||||
**Before (MUI Button):**
|
||||
|
||||
```typescript
|
||||
<Button variant = "contained"
|
||||
color = "primary"
|
||||
disabled = {loading}
|
||||
onClick = {handleClick} >
|
||||
Submit
|
||||
< /Button>
|
||||
< IconButton
|
||||
onClick = {handleDelete}
|
||||
disabled = {!
|
||||
canDelete
|
||||
}>
|
||||
<DeleteIcon / >
|
||||
</IconButton>
|
||||
```
|
||||
|
||||
**After (BUI Button):**
|
||||
|
||||
```typescript
|
||||
<Button variant = "primary"
|
||||
isDisabled = {loading}
|
||||
onClick = {handleClick} >
|
||||
Submit
|
||||
< /Button>
|
||||
< ButtonIcon
|
||||
aria - label = "delete"
|
||||
isDisabled = {!
|
||||
canDelete
|
||||
}
|
||||
onPress = {handleDelete}
|
||||
icon = { < RiDeleteBinLine
|
||||
size = {16}
|
||||
/>}
|
||||
variant = "secondary"
|
||||
/ >
|
||||
```
|
||||
|
||||
### 9. TextField Changes
|
||||
|
||||
**Before (MUI TextField):**
|
||||
|
||||
```typescript
|
||||
<TextField
|
||||
required
|
||||
name="title"
|
||||
label="Title"
|
||||
value={value}
|
||||
onChange={e => setValue(e.target.value)}
|
||||
fullWidth
|
||||
/>
|
||||
```
|
||||
|
||||
**After (BUI TextField):**
|
||||
|
||||
```typescript
|
||||
<TextField
|
||||
isRequired
|
||||
id="title"
|
||||
label="Title"
|
||||
value={value}
|
||||
onChange={newValue => setValue(newValue)} // receives string directly!
|
||||
/>
|
||||
```
|
||||
|
||||
Note: BUI TextField `onChange` receives the string value directly, not an event object.
|
||||
|
||||
### 10. Tabs Pattern
|
||||
|
||||
**Before (MUI Tabs):**
|
||||
|
||||
```typescript
|
||||
import {Tab} from '@material-ui/core';
|
||||
import {TabContext, TabList, TabPanel} from '@material-ui/lab';
|
||||
|
||||
<TabContext value = {tab} >
|
||||
<TabList onChange = {handleChange} >
|
||||
<Tab label = "Tab 1"
|
||||
value = "tab1" / >
|
||||
<Tab label = "Tab 2"
|
||||
value = "tab2" / >
|
||||
</TabList>
|
||||
< TabPanel
|
||||
value = "tab1" > Content
|
||||
1 < /TabPanel>
|
||||
< TabPanel
|
||||
value = "tab2" > Content
|
||||
2 < /TabPanel>
|
||||
< /TabContext>;
|
||||
```
|
||||
|
||||
**After (BUI Tabs):**
|
||||
|
||||
```typescript
|
||||
import {Tabs, TabList, Tab, TabPanel} from '@backstage/ui';
|
||||
|
||||
<Tabs defaultSelectedKey = "tab1" >
|
||||
<TabList>
|
||||
<Tab id = "tab1" > Tab
|
||||
1 < /Tab>
|
||||
< Tab
|
||||
id = "tab2" > Tab
|
||||
2 < /Tab>
|
||||
< /TabList>
|
||||
< TabPanel
|
||||
id = "tab1" > Content
|
||||
1 < /TabPanel>
|
||||
< TabPanel
|
||||
id = "tab2" > Content
|
||||
2 < /TabPanel>
|
||||
< /Tabs>;
|
||||
```
|
||||
|
||||
### 11. Menu Pattern
|
||||
|
||||
**Before (MUI Menu):**
|
||||
|
||||
```typescript
|
||||
import {IconButton, Popover, MenuList, MenuItem} from '@material-ui/core';
|
||||
import MoreVertIcon from '@material-ui/icons/MoreVert';
|
||||
|
||||
<IconButton onClick = {handleOpen} > <MoreVertIcon / > </IconButton>
|
||||
< Popover
|
||||
open = {open}
|
||||
anchorEl = {anchorEl}
|
||||
onClose = {handleClose} >
|
||||
<MenuList>
|
||||
<MenuItem onClick = {handleAction} > Action < /MenuItem>
|
||||
< /MenuList>
|
||||
< /Popover>
|
||||
```
|
||||
|
||||
**After (BUI Menu):**
|
||||
|
||||
```typescript
|
||||
import {ButtonIcon, Menu, MenuItem, MenuTrigger} from '@backstage/ui';
|
||||
import {RiMore2Line} from '@remixicon/react';
|
||||
|
||||
<MenuTrigger>
|
||||
<ButtonIcon aria - label = "more"
|
||||
icon = { < RiMore2Line / >
|
||||
}
|
||||
variant = "secondary" / >
|
||||
<Menu>
|
||||
<MenuItem onAction = {handleAction} > Action < /MenuItem>
|
||||
< /Menu>
|
||||
< /MenuTrigger>;
|
||||
```
|
||||
|
||||
### 12. List to HTML with CSS Modules
|
||||
|
||||
**Before (MUI List):**
|
||||
|
||||
```typescript
|
||||
import {List, ListItem, ListItemIcon, ListItemText} from '@material-ui/core';
|
||||
|
||||
<List>
|
||||
<ListItem>
|
||||
<ListItemIcon>
|
||||
<SomeIcon / >
|
||||
</ListItemIcon>
|
||||
< ListItemText
|
||||
primary = "Title"
|
||||
secondary = "Description" / >
|
||||
</ListItem>
|
||||
< /List>;
|
||||
```
|
||||
|
||||
**After (HTML list with BUI and CSS Modules):**
|
||||
|
||||
```css
|
||||
/* MyList.module.css */
|
||||
@layer components {
|
||||
.list {
|
||||
list-style: none;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.listItem {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
padding: var(--bui-space-2) 0;
|
||||
}
|
||||
|
||||
.listItemIcon {
|
||||
min-width: 36px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
color: var(--bui-fg-primary);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
```typescript
|
||||
import {Flex, Text} from '@backstage/ui';
|
||||
import {RiSomeIcon} from '@remixicon/react';
|
||||
import styles from './MyList.module.css';
|
||||
|
||||
<ul className = {styles.list} >
|
||||
<li className = {styles.listItem} >
|
||||
<div className = {styles.listItemIcon} >
|
||||
<RiSomeIcon size = {20}
|
||||
/>
|
||||
< /div>
|
||||
< Flex
|
||||
direction = "column" >
|
||||
<Text>Title < /Text>
|
||||
< Text
|
||||
variant = "body-small"
|
||||
color = "secondary" >
|
||||
Description
|
||||
< /Text>
|
||||
< /Flex>
|
||||
< /li>
|
||||
< /ul>;
|
||||
```
|
||||
|
||||
### 13. Chip to Tag
|
||||
|
||||
**Before (MUI Chip):**
|
||||
|
||||
```typescript
|
||||
import { Chip } from '@material-ui/core';
|
||||
|
||||
<Chip label="Category" size="small" />;
|
||||
```
|
||||
|
||||
**After (BUI Tag):**
|
||||
|
||||
```typescript
|
||||
import {Tag} from '@backstage/ui';
|
||||
|
||||
<Tag size = "small" > Category < /Tag>;
|
||||
```
|
||||
|
||||
### 14. Icons: MUI Icons to Remix Icons
|
||||
|
||||
**Before (MUI Icons):**
|
||||
|
||||
```typescript
|
||||
import CloseIcon from '@material-ui/icons/Close';
|
||||
import SearchIcon from '@material-ui/icons/Search';
|
||||
|
||||
<CloseIcon / >
|
||||
<SearchIcon fontSize = "small" / >
|
||||
```
|
||||
|
||||
**After (Remix Icons):**
|
||||
|
||||
```typescript
|
||||
import {RiCloseLine, RiSearchLine} from '@remixicon/react';
|
||||
|
||||
<RiCloseLine / >
|
||||
<RiSearchLine size = {16}
|
||||
/>
|
||||
```
|
||||
|
||||
Common icon mappings:
|
||||
|
||||
| MUI Icon | Remix Icon |
|
||||
| ----------------- | -------------------- |
|
||||
| `Close` | `RiCloseLine` |
|
||||
| `Search` | `RiSearchLine` |
|
||||
| `Settings` | `RiSettingsLine` |
|
||||
| `Add` | `RiAddLine` |
|
||||
| `Delete` | `RiDeleteBinLine` |
|
||||
| `Edit` | `RiEditLine` |
|
||||
| `Check` | `RiCheckLine` |
|
||||
| `Error` | `RiErrorWarningLine` |
|
||||
| `Warning` | `RiAlertLine` |
|
||||
| `Info` | `RiInformationLine` |
|
||||
| `ExpandMore` | `RiArrowDownSLine` |
|
||||
| `ExpandLess` | `RiArrowUpSLine` |
|
||||
| `ChevronRight` | `RiArrowRightSLine` |
|
||||
| `ChevronLeft` | `RiArrowLeftSLine` |
|
||||
| `Menu` | `RiMenuLine` |
|
||||
| `MoreVert` | `RiMore2Line` |
|
||||
| `Visibility` | `RiEyeLine` |
|
||||
| `VisibilityOff` | `RiEyeOffLine` |
|
||||
| `NewReleases` | `RiMegaphoneLine` |
|
||||
| `RecordVoiceOver` | `RiMegaphoneLine` |
|
||||
| `Description` | `RiFileTextLine` |
|
||||
|
||||
Find more icons at: https://remixicon.com/
|
||||
|
||||
## CSS Variable Reference
|
||||
|
||||
### Spacing
|
||||
|
||||
| MUI theme.spacing() | BUI CSS Variable |
|
||||
| -------------------- | -------------------- |
|
||||
| `theme.spacing(0.5)` | `var(--bui-space-1)` |
|
||||
| `theme.spacing(1)` | `var(--bui-space-2)` |
|
||||
| `theme.spacing(1.5)` | `var(--bui-space-3)` |
|
||||
| `theme.spacing(2)` | `var(--bui-space-4)` |
|
||||
| `theme.spacing(3)` | `var(--bui-space-6)` |
|
||||
| `theme.spacing(4)` | `var(--bui-space-8)` |
|
||||
|
||||
### Colors
|
||||
|
||||
| MUI theme.palette | BUI CSS Variable |
|
||||
| -------------------- | ------------------------------------------ |
|
||||
| `text.primary` | `var(--bui-fg-primary)` |
|
||||
| `text.secondary` | `var(--bui-fg-secondary)` |
|
||||
| `background.paper` | `var(--bui-bg-surface-1)` |
|
||||
| `background.default` | `var(--bui-bg-surface-0)` |
|
||||
| `primary.main` | `var(--bui-bg-solid)` or `var(--bui-ring)` |
|
||||
| `error.main` | `var(--bui-fg-danger)` |
|
||||
| `action.hover` | `var(--bui-bg-hover)` |
|
||||
| `divider` | `var(--bui-border)` |
|
||||
|
||||
### Typography
|
||||
|
||||
| Property | BUI CSS Variable |
|
||||
| ------------------- | -------------------------------- |
|
||||
| Font family | `var(--bui-font-regular)` |
|
||||
| Font size small | `var(--bui-font-size-1)` |
|
||||
| Font size medium | `var(--bui-font-size-2)` |
|
||||
| Font size large | `var(--bui-font-size-3)` |
|
||||
| Font weight regular | `var(--bui-font-weight-regular)` |
|
||||
| Font weight bold | `var(--bui-font-weight-bold)` |
|
||||
|
||||
### Other
|
||||
|
||||
| Property | BUI CSS Variable |
|
||||
| -------------------- | ------------------------ |
|
||||
| Border radius small | `var(--bui-radius-2)` |
|
||||
| Border radius medium | `var(--bui-radius-3)` |
|
||||
| Border radius full | `var(--bui-radius-full)` |
|
||||
| Link color | `var(--bui-fg-link)` |
|
||||
|
||||
## Known Limitations
|
||||
|
||||
Some Backstage APIs still require MUI-compatible icon types:
|
||||
|
||||
- **NavItemBlueprint** (`@backstage/frontend-plugin-api`): The `icon` prop expects MUI `IconComponent` type. Remix icons
|
||||
are not type-compatible.
|
||||
- **Timeline** (`@material-ui/lab`): No BUI equivalent exists.
|
||||
- **Pagination** (`@material-ui/lab`): No BUI equivalent exists.
|
||||
- **Alert** (`@material-ui/lab`): No BUI equivalent exists.
|
||||
|
||||
For these cases, keep using MUI components.
|
||||
|
||||
## Migration Checklist
|
||||
|
||||
When migrating a plugin:
|
||||
|
||||
1. [ ] Add `@backstage/ui` dependency
|
||||
2. [ ] Add `@remixicon/react` dependency (if using icons)
|
||||
3. [ ] Add `react-aria-components` dependency (if using Tooltip)
|
||||
4. [ ] Add CSS import to root file
|
||||
5. [ ] Remove `@material-ui/core` imports (except components with no BUI equivalent)
|
||||
6. [ ] Remove `@material-ui/icons` imports
|
||||
7. [ ] Remove `makeStyles` and related imports
|
||||
8. [ ] Create `.module.css` files for component styles
|
||||
9. [ ] Replace `Typography` with `Text`
|
||||
10. [ ] Replace `Box display="flex"` with `Flex`
|
||||
11. [ ] Replace `Grid container/item` with `Grid.Root/Grid.Item`
|
||||
12. [ ] Replace `Paper` with `Card`
|
||||
13. [ ] Replace MUI `Dialog` with BUI `DialogTrigger` pattern
|
||||
14. [ ] Replace MUI `Tooltip` with BUI `TooltipTrigger` pattern
|
||||
15. [ ] Replace MUI `Tabs` with BUI `Tabs`
|
||||
16. [ ] Replace MUI `Menu` with BUI `MenuTrigger` pattern
|
||||
17. [ ] Replace `Chip` with `Tag`
|
||||
18. [ ] Replace `IconButton` with `ButtonIcon`
|
||||
19. [ ] Update `Button` props (`disabled` → `isDisabled`, `variant="contained"` → `variant="primary"`)
|
||||
20. [ ] Update `TextField` props (`required` → `isRequired`, `onChange` signature)
|
||||
21. [ ] Replace MUI icons with Remix icons
|
||||
22. [ ] Run `yarn tsc` to check for type errors
|
||||
23. [ ] Run `yarn build` to verify build
|
||||
24. [ ] Run `yarn lint` to check for missing dependencies
|
||||
25. [ ] Test component rendering and functionality
|
||||
|
||||
## Reference
|
||||
|
||||
- BUI Documentation: https://ui.backstage.io
|
||||
- Remix Icons: https://remixicon.com/
|
||||
- Example Migration PR: https://github.com/backstage/backstage/pull/31631
|
||||
@@ -19,6 +19,8 @@
|
||||
/** @type{import('prism-react-renderer').PrismTheme} **/
|
||||
// @ts-ignore
|
||||
import { themes } from 'prism-react-renderer';
|
||||
import { cpSync, existsSync } from 'node:fs';
|
||||
import { resolve as resolvePath } from 'node:path';
|
||||
import type * as Preset from '@docusaurus/preset-classic';
|
||||
import { Config } from '@docusaurus/types';
|
||||
import RedirectPlugin from '@docusaurus/plugin-client-redirects';
|
||||
@@ -28,7 +30,9 @@ import type * as OpenApiPlugin from 'docusaurus-plugin-openapi-docs';
|
||||
const backstageTheme = themes.vsDark;
|
||||
backstageTheme.plain.backgroundColor = '#232323';
|
||||
|
||||
const useVersionedDocs = require('node:fs').existsSync('versions.json');
|
||||
const useVersionedDocs = existsSync('versions.json');
|
||||
const wellKnownDocsPath = resolvePath(__dirname, '../docs/.well-known');
|
||||
const wellKnownPublicPath = '/.well-known';
|
||||
|
||||
// This patches the redirect plugin to ignore the error when it tries to override existing fields.
|
||||
// This lets us add redirects that only apply to the next docs, while the stable docs still contain the source path.
|
||||
@@ -253,6 +257,40 @@ const config: Config = {
|
||||
};
|
||||
},
|
||||
}),
|
||||
() => ({
|
||||
name: 'publish-well-known-docs',
|
||||
getPathsToWatch() {
|
||||
return [wellKnownDocsPath];
|
||||
},
|
||||
configureWebpack() {
|
||||
if (!existsSync(wellKnownDocsPath)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return {
|
||||
devServer: {
|
||||
static: [
|
||||
{
|
||||
publicPath: wellKnownPublicPath,
|
||||
directory: wellKnownDocsPath,
|
||||
staticOptions: {
|
||||
dotfiles: 'allow',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
},
|
||||
async postBuild({ outDir }: { outDir: string }) {
|
||||
if (!existsSync(wellKnownDocsPath)) {
|
||||
return;
|
||||
}
|
||||
|
||||
cpSync(wellKnownDocsPath, resolvePath(outDir, '.well-known'), {
|
||||
recursive: true,
|
||||
});
|
||||
},
|
||||
}),
|
||||
ctx =>
|
||||
PatchedRedirectPlugin(ctx, {
|
||||
id: '@docusaurus/plugin-client-redirects',
|
||||
|
||||
Reference in New Issue
Block a user