remove planning docs

Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
Patrik Oldsberg
2026-02-16 21:06:33 +01:00
parent b91e17f622
commit 0f90fda9cc
4 changed files with 0 additions and 1069 deletions
-419
View File
@@ -1,419 +0,0 @@
# Page Header Architecture - Implementation Summary
## Overview
This branch implements a new page header architecture for Backstage's New Frontend System (NFS) that provides:
- Consistent headers across all plugin pages
- Support for sub-pages with tab navigation
- Header actions (buttons/controls in the header)
- Plugin-level display metadata (titles and icons)
## Architectural Changes
### 1. New Blueprint: `HeaderActionBlueprint`
**Location:** `packages/frontend-plugin-api/src/blueprints/HeaderActionBlueprint.tsx`
A new extension blueprint that allows plugins to register actions (buttons, controls) that appear in the page header.
**Key Features:**
- Attaches to `app/routes` with input `headerActions`
- Outputs `coreExtensionData.reactElement`
- Uses lazy loading with `ExtensionBoundary`
**Usage Example** (from api-docs plugin):
```typescript
HeaderActionBlueprint.make({
name: 'register-api',
params: {
loader: async () =>
compatWrapper(
<ButtonLink href="/catalog">Register Existing API</ButtonLink>,
),
},
});
```
### 2. Enhanced `PageBlueprint`
**Location:** `packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx`
The `PageBlueprint` has been significantly enhanced to support sub-pages and tabbed navigation.
**New Features:**
#### Inputs
- New `pages` input that accepts sub-pages with:
- `routePath`
- `routeRef` (optional)
- `reactElement`
- `title` (optional)
#### Outputs
- Added `title` output (optional)
#### Configuration
- New `title` config schema property
#### Parameters
- `title?: string` - Page title displayed in header
- `loader` is now optional (not required when using sub-pages)
- `routeRef` now accepts both `RouteRef` and `SubRouteRef`
#### Behavior Changes
1. **With loader (traditional page):** Renders Header + page content
2. **Without loader (parent page with tabs):** Renders Header with tabs populated from `inputs.pages`
### 3. Central Routing Changes: `AppRoutes`
**Location:** `plugins/app/src/extensions/AppRoutes.tsx`
The core routing extension has been substantially refactored to orchestrate the new header architecture.
**Key Implementation Details:**
#### Header Action Aggregation
```typescript
const headerActionsByPluginId = new Map<string, Array<ReactNode>>();
```
- Collects all header actions from inputs
- Groups them by plugin ID
- Passes them to the Header component via `customActions` prop
#### Page Aggregation by Path
```typescript
const pagesByPath = new Map<
string,
Array<{ element: JSX.Element; title: string; node: AppNode }>
>();
```
- Groups pages by their route path
- Enables detection of pages with sub-pages
- Stores title and node reference for each page
#### Conditional Rendering Logic
1. **Multiple pages at same path (tabbed navigation):**
- Renders `Header` with tabs
- Uses nested `<Routes>` for tab content
- Tab matching uses prefix strategy
2. **Single page (traditional):**
- Renders `Header` with plugin title
- Renders page element directly
### 4. Plugin Display Metadata
**Location:** `plugins/catalog/src/alpha/plugin.tsx`
Plugins can now define display metadata:
```typescript
export default createFrontendPlugin({
pluginId: 'catalog',
info: {
packageJson: () => import('../../package.json'),
},
display: {
icon: 'catalog',
title: 'Software Catalog',
},
// ...
});
```
### 5. Header Component Styling Updates
**Location:** `packages/ui/src/components/Header/Header.module.css`
- Commented out default `margin-bottom` on `.bui-HeaderToolbar`
- Allows tighter integration between header and content
- Maintains conditional margin for tabbed headers
## Implementation Examples
### Example 1: App Visualizer (Multi-page with Tabs)
**Location:** `plugins/app-visualizer/src/plugin.tsx`
Demonstrates the sub-page pattern:
1. **Parent page** (no loader):
```typescript
const appVisualizerPage = PageBlueprint.make({
params: {
path: '/visualizer',
routeRef: rootRouteRef,
title: 'Visualizer',
// No loader - will show tabs
},
});
```
2. **Sub-pages** (using `SubPageBlueprint` - not yet fully implemented):
```typescript
const appVisualizerTreePage = SubPageBlueprint.make({
attachTo: { id: 'page:app-visualizer', input: 'pages' },
name: 'tree',
params: {
path: '/tree',
title: 'Tree',
loader: () => import('./components/...'),
},
});
```
Multiple sub-pages are defined:
- Tree view (`/tree`)
- Detailed view (`/details`)
- Text view (`/text`)
### Example 2: API Docs (Header Actions)
**Location:** `plugins/api-docs/src/alpha.tsx`
Demonstrates header actions:
```typescript
HeaderActionBlueprint.make({
name: 'register-api',
params: {
loader: async () =>
compatWrapper(
<ButtonLink href="/catalog">Register Existing API</ButtonLink>,
),
},
});
```
## Data Flow
```
┌─────────────────────────────────────────────────────────────┐
│ AppRoutes │
│ (Central orchestrator) │
└─────────────────────────────────────────────────────────────┘
┌─────────────┴─────────────┐
│ │
▼ ▼
┌───────────────────┐ ┌────────────────────┐
│ headerActions │ │ routes │
│ input │ │ input │
└───────────────────┘ └────────────────────┘
│ │
│ │
▼ ▼
Group by plugin ID Group by route path
│ │
│ │
└─────────────┬─────────────┘
┌─────────────────┐
│ Route Renderer │
└─────────────────┘
┌─────────────┴──────────────┐
│ │
▼ ▼
┌──────────────────┐ ┌──────────────────────┐
│ Single Page │ │ Multi-page (Tabs) │
│ │ │ │
│ Header + │ │ Header (w/ tabs) + │
│ Page Element │ │ Nested Routes │
└──────────────────┘ └──────────────────────┘
```
## Extension Data Types Used
The implementation relies on these core extension data types:
- `coreExtensionData.routePath` - Route paths for pages
- `coreExtensionData.routeRef` - Route references for navigation
- `coreExtensionData.reactElement` - React components to render
- `coreExtensionData.title` - Page/sub-page titles
## Technical Considerations
### 1. Plugin-Relative Attachment Points
The implementation uses string-based attachment points:
```typescript
attachTo: { id: 'page:app-visualizer', input: 'pages' }
```
**Future consideration:** TypeScript-based attachment points for type safety:
```typescript
attachTo: appVisualizerPage.inputs.pages;
```
### 2. SubPageBlueprint
The code references `SubPageBlueprint` which is not yet fully implemented in the blueprint exports. This suggests it's either:
- Still being developed
- Implemented locally for testing
- Planned for future implementation
### 3. Header Component Integration
The `Header` component from `@backstage/ui` is used directly in:
- `PageBlueprint` rendering
- `AppRoutes` conditional rendering
This component accepts:
- `title` - Page title
- `tabs` - Array of tab configurations
- `customActions` - Array of React nodes for header actions
### 4. Route Matching Strategy
Sub-pages use different route matching:
- Parent pages: Match with trailing `/*` for catch-all
- Tab content: Direct path matching
- Tab component: Uses `matchStrategy: 'prefix'`
## Outstanding Questions & TODOs
From `header.md`:
### Decisions Needed
- [ ] Should we add titles to all plugins?
- [ ] Should we add icons to all plugins?
- [ ] Is `HeaderActionBlueprint` too specific? (Note: "a bit wonky")
- [ ] PageBlueprint additions or ContentBlueprint?
### Implementation Items
- [ ] Ship the Figma design
- [ ] TopBarActionBlueprint? (alternative naming consideration)
- [ ] Add `icon` and `title` to plugin info (partially done)
- [ ] Add support for plugin-relative attachment points
- [ ] Consider TypeScript-based attachment points
- [ ] Add `coreExtensionData.navTarget`
- [ ] Add swappable component for PageBlueprint React element
## Breaking Changes
This implementation introduces breaking changes:
1. **PageBlueprint API changes:**
- New optional `title` parameter
- `loader` is now optional
- `routeRef` accepts `SubRouteRef` in addition to `RouteRef`
2. **AppRoutes behavior:**
- Automatically wraps all pages with Header component
- Changes route structure for multi-page plugins
3. **Header styling:**
- Removes default bottom margin on headers
## Migration Path
For existing plugins to adopt the new header architecture:
1. **Add display metadata to plugin:**
```typescript
display: {
icon: 'plugin-icon',
title: 'Plugin Name',
}
```
2. **Add titles to pages:**
```typescript
PageBlueprint.make({
params: {
title: 'Page Title',
// ... other params
},
});
```
3. **Optional - Add header actions:**
```typescript
HeaderActionBlueprint.make({
params: {
loader: () => <YourButton />,
},
});
```
4. **Optional - Add sub-pages for tabbed navigation:**
Create multiple pages at the same route path with different titles.
## Files Changed Summary
| File | Lines Changed | Type of Change |
| ------------------------------------------ | ------------- | ---------------------- |
| `header.md` | +43 | Documentation |
| `HeaderActionBlueprint.tsx` | +35 | New Blueprint |
| `PageBlueprint.tsx` | +52/-12 | Enhancement |
| `blueprints/index.ts` | +1 | Export |
| `Header.module.css` | +1/-1 | Styling |
| `plugins/api-docs/src/alpha.tsx` | +13/-1 | Example Implementation |
| `plugins/app-visualizer/src/plugin.tsx` | +120/-1 | Example Implementation |
| `plugins/app/package.json` | +1 | Dependency |
| `plugins/app/src/extensions/AppRoutes.tsx` | +77/-2 | Core Logic |
| `plugins/catalog/src/alpha/plugin.tsx` | +9/-1 | Display Metadata |
| `yarn.lock` | +1 | Dependency |
**Total: 11 files, ~354 insertions, ~20 deletions**
## Next Steps
1. **Complete SubPageBlueprint implementation** - Currently referenced but not fully exported
2. **Decide on naming** - HeaderActionBlueprint vs TopBarActionBlueprint
3. **Type-safe attachment points** - Consider TypeScript-based approach
4. **Migration guide** - Document for plugin authors
5. **Testing** - Comprehensive tests for new routing logic
6. **Figma alignment** - Ensure implementation matches design specs
7. **Plugin adoption** - Roll out to all core plugins
8. **API documentation** - Update API docs for new blueprints
## Architecture Benefits
1. **Consistency:** All pages have consistent header treatment
2. **Flexibility:** Supports both simple pages and complex tabbed interfaces
3. **Extensibility:** Header actions allow plugins to add controls
4. **Navigation:** Two-level navigation (page + sub-pages) is now native
5. **Declarative:** Uses extension system patterns for discoverability
6. **Centralized:** AppRoutes orchestrates header rendering for consistency
## Potential Concerns
1. **Complexity:** AppRoutes has increased complexity with conditional rendering
2. **Performance:** Multiple map operations on every render
3. **Breaking:** Changes existing PageBlueprint behavior
4. **Incomplete:** SubPageBlueprint appears incomplete
5. **Debugging:** Console.log statements present (need cleanup)
6. **Migration:** All existing plugins will need updates for full adoption
-43
View File
@@ -1,43 +0,0 @@
## TODO
- [ ] Shipping https://www.figma.com/design/zJxHainw6yO7L9oEsStAh7/Header?node- [ ]id=103- [ ]5709&t=HNkpc2MqdUbaN8Jz- [ ]1
- [ ] TopBarActionBlueprint?
- [ ] PageBlueprint additions or ContentBlueprint?
- [ ] Should we add titles to all plugins?
- [ ] Should we add icons to all plugins?
- [ ] Add `icon` and `title` to the plugin info
## Notes
- HeaderActionBlueprint is a bit wonky, too specific?
## Requirements
- Consistency of headers across all plugins in NFS
- Possibility to put breadcrumbs in the top bar
- Two levels of navigation for plugins - page + sub pages
## Technical Requirements
- Sub pages must be represented in some form as extensions
- Sub pages must have a title and path relative to the parent page
- Plugins must have titles and icons
- Sub pages must be attachments of the parent page
## Potential Solutions
### Make all(\*) pages navigable, get rid of nav items
## Implementation Plan
- Add support for plugin-relative attachment points, i.e. `attachTo: { id: 'page:{pluginId}', input: 'pages' }`
- Alternative: typescript-based attachment points, i.e. `attachTo: appVisualizerPage` or `attachTo: appVisualizerPage.inputs.pages`
- Add a `SubPageBlueprint` that attaches to the `pages` of `PageBlueprint`, using plugin-relative attachment
- Add a swappable component for the `PageBlueprint` React element
- Add `title` param and output to `PageBlueprint`
To consider:
- Add `display` options for plugins
- Add `coreExtensionData.navTarget`, either marker or with actual data
-270
View File
@@ -1,270 +0,0 @@
# SubPageBlueprint Implementation Summary
## What Was Implemented
I've successfully implemented the `SubPageBlueprint` pattern for the header architecture, addressing one of the critical gaps identified in the RFC alignment analysis.
## Files Created/Modified
### New Files
1. **`packages/frontend-plugin-api/src/blueprints/SubPageBlueprint.tsx`**
- New blueprint for creating sub-pages that attach to parent pages
- Outputs: `routePath`, `reactElement`, `title`
- Supports lazy loading of sub-page content
- Requires users to specify `attachTo` to target their parent page
### Modified Files
2. **`packages/frontend-plugin-api/src/blueprints/index.ts`**
- Exported `SubPageBlueprint` for public use
3. **`packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx`**
- Enhanced to handle sub-pages via `inputs.pages`
- Three rendering modes:
- **With loader**: Renders Header + lazy-loaded content
- **With sub-pages**: Renders Header with tabs + nested Routes for sub-pages
- **Empty**: Renders just Header
- Removed duplicate header rendering issues
4. **`plugins/app/src/extensions/AppRoutes.tsx`**
- Simplified to focus on top-level routing only
- Removed duplicate header orchestration logic
- Removed debug console.log statements
- Sub-page routing is now handled by PageBlueprint
5. **`plugins/app-visualizer/src/plugin.tsx`**
- Added import for `SubPageBlueprint`
- Cleaned up unused `SubRouteRef` declarations
- Now properly uses SubPageBlueprint pattern
6. **`plugins/api-docs/src/alpha.tsx`**
- Removed `HeaderActionBlueprint` usage (feature temporarily removed)
- Cleaned up unused imports
7. **`plugins/catalog/src/alpha/plugin.tsx`**
- Removed `display` property (not yet supported in plugin options)
## Architecture Overview
### SubPageBlueprint API
```typescript
const mySubPage = SubPageBlueprint.make({
attachTo: { id: 'page:my-plugin', input: 'pages' },
name: 'overview',
params: {
path: '/overview', // Relative path from parent
title: 'Overview', // Tab title
loader: () => import('./components/Overview').then(m => <m.Overview />),
},
});
```
### How It Works
1. **Parent Page Declaration** (without loader, with sub-pages):
```typescript
const parentPage = PageBlueprint.make({
params: {
path: '/visualizer',
routeRef: rootRouteRef,
title: 'Visualizer',
// No loader - will show tabs
},
});
```
2. **Sub-Page Declarations**:
```typescript
const treePage = SubPageBlueprint.make({
attachTo: { id: 'page:app-visualizer', input: 'pages' },
name: 'tree',
params: {
path: '/tree',
title: 'Tree',
loader: () => import('./TreeView'),
},
});
```
3. **Rendering Flow**:
```
AppRoutes
└─> PageBlueprint (detects sub-pages via inputs.pages)
├─> Header (with tabs)
└─> Routes
├─> /tree -> TreePage content
├─> /details -> DetailsPage content
└─> /text -> TextPage content
```
## Key Design Decisions
### 1. **Simplified AppRoutes**
- Removed header orchestration from AppRoutes
- AppRoutes now only handles top-level routing
- PageBlueprint is responsible for its own header rendering
### 2. **No Header Duplication**
- Pages with loaders: Header rendered by PageBlueprint
- Pages with sub-pages: Header with tabs rendered by PageBlueprint
- No duplicate headers between AppRoutes and PageBlueprint
### 3. **Plugin-Relative Attachment**
- Sub-pages attach using: `{ id: 'page:plugin-id', input: 'pages' }`
- Follows the pattern described in header.md and the RFC
### 4. **Lazy Loading**
- All sub-pages use lazy loading via `ExtensionBoundary.lazy()`
- Improves initial page load performance
### 5. **Removed Features (Temporarily)**
- **HeaderActionBlueprint**: Removed from AppRoutes orchestration (needs redesign)
- **Plugin display metadata**: Removed (not yet supported in plugin options type)
- **SubRouteRef support**: Simplified to only support RouteRef
## Example Usage: App Visualizer
The app-visualizer plugin demonstrates the complete pattern:
```typescript
// Parent page without loader
const appVisualizerPage = PageBlueprint.make({
params: {
path: '/visualizer',
routeRef: rootRouteRef,
title: 'Visualizer',
},
});
// Three sub-pages attached to parent
const appVisualizerTreePage = SubPageBlueprint.make({
attachTo: { id: 'page:app-visualizer', input: 'pages' },
name: 'tree',
params: {
path: '/tree',
title: 'Tree',
loader: () =>
import('./components/AppVisualizerPage/TreeVisualizer').then(m => {
const Component = () => {
const appTreeApi = useApi(appTreeApiRef);
const { tree } = appTreeApi.getTree();
return <m.TreeVisualizer tree={tree} />;
};
return <Component />;
}),
},
});
// Similar for details and text pages...
```
## Technical Benefits
1. **Declarative**: Sub-pages are extensions that can be discovered and manipulated
2. **Type-Safe**: Full TypeScript support with proper type checking
3. **Consistent**: All pages get headers automatically
4. **Flexible**: Supports both simple pages and complex tabbed interfaces
5. **Performant**: Lazy loading of sub-page content
## What's Still Missing
1. **HeaderActionBlueprint Integration**: Needs to be redesigned to work without AppRoutes orchestration
2. **Plugin Display Metadata**: `display: { icon, title }` type support in plugin options
3. **SubRouteRef Support**: Currently only RouteRef is supported for routing
4. **Sidebar Navigation**: The RFC's primary goal for Portal (sidebar navigation API)
5. **Breadcrumbs**: Mentioned in RFC but not implemented
## Testing Recommendations
1. **Test sub-page navigation**: Click through tabs in app-visualizer
2. **Test lazy loading**: Verify sub-pages load on demand
3. **Test routing**: Verify URLs update correctly when switching tabs
4. **Test nested routes**: Verify sub-pages render at correct paths
5. **Test simple pages**: Verify pages with loaders still work
## Migration Path for Other Plugins
To add sub-pages to an existing plugin:
1. **Update parent page** to remove loader:
```typescript
const myPage = PageBlueprint.make({
params: {
path: '/my-plugin',
title: 'My Plugin',
// Remove loader to enable sub-pages
},
});
```
2. **Create sub-pages**:
```typescript
const mySubPage = SubPageBlueprint.make({
attachTo: { id: 'page:my-plugin', input: 'pages' },
name: 'overview',
params: {
path: '/overview',
title: 'Overview',
loader: () => import('./Overview'),
},
});
```
3. **Add to plugin extensions**:
```typescript
export default createFrontendPlugin({
extensions: [
myPage,
mySubPage,
// ... other sub-pages
],
});
```
## Type Safety
All type errors have been resolved:
- ✅ No unused imports
- ✅ Proper type checking for all parameters
- ✅ Correct extension data types
- ✅ Valid blueprint definitions
## Next Steps
1. **Add tests** for SubPageBlueprint and updated PageBlueprint
2. **Implement HeaderActionBlueprint redesign** (perhaps as page-level extensions)
3. **Add plugin display metadata support** in plugin options types
4. **Implement sidebar navigation API** (RFC primary goal)
5. **Add breadcrumb support** for navigation hierarchy
6. **Document the pattern** for community plugin authors
7. **Update existing plugins** to use the new pattern
## Conclusion
The SubPageBlueprint implementation successfully delivers on the core technical requirements from the RFC:
- ✅ Sub-pages are represented as extensions
- ✅ Sub-pages have titles and relative paths
- ✅ Sub-pages attach to parent pages
- ✅ Two levels of navigation (page + sub-pages)
- ✅ Consistent headers across plugins
- ✅ Plugin-relative attachment points
The implementation is type-safe, follows Backstage patterns, and provides a clean API for plugin authors to create rich, multi-page experiences within their plugins.
-337
View File
@@ -1,337 +0,0 @@
# Swappable Component Architecture Refactor
## Problem
The initial implementation had `@backstage/frontend-plugin-api` directly importing `Header` from `@backstage/ui`, which violated the package dependency architecture:
- `@backstage/frontend-plugin-api` is a low-level API package
- `@backstage/ui` is a higher-level UI component library
- API packages should not depend on UI libraries
## Solution
Implemented a **swappable component pattern** that:
1. Provides a default implementation using plain HTML elements in `@backstage/frontend-plugin-api`
2. Ships the `@backstage/ui` Header implementation via the `@backstage/plugin-app` plugin
3. Allows apps to override the component with custom implementations
This follows the same pattern used for other swappable components like `Progress`, `NotFoundErrorPage`, and `ErrorDisplay`.
## Architecture
### 1. Created PageWrapper Swappable Component
**Location:** `packages/frontend-plugin-api/src/components/PageWrapper.tsx`
```typescript
export interface PageWrapperProps {
title?: string;
tabs?: PageTab[];
children?: ReactNode;
}
// Default implementation using plain HTML
function DefaultPageWrapper(props: PageWrapperProps): JSX.Element {
// Uses plain <div>, <header>, <nav>, <a> elements
// No dependency on @backstage/ui
}
// Swappable component that can be overridden
export const PageWrapper = createSwappableComponent<PageWrapperProps>({
id: 'core.page-wrapper',
loader: () => DefaultPageWrapper,
});
```
**Key Features:**
- ✅ No dependencies on `@backstage/ui`
- ✅ Uses only plain HTML elements in default implementation
- ✅ Provides basic styling for usability
- ✅ Can be overridden by apps
### 2. Updated PageBlueprint
**Location:** `packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx`
**Before:**
```typescript
import { Header } from '@backstage/ui'; // ❌ Direct dependency
<Header title={params.title} tabs={tabs} />;
```
**After:**
```typescript
import { PageWrapper } from '../components'; // ✅ Internal swappable component
<PageWrapper title={params.title} tabs={tabs}>
{children}
</PageWrapper>;
```
### 3. Provided @backstage/ui Implementation
**Location:** `plugins/app/src/extensions/components.tsx`
```typescript
export const PageWrapper = SwappableComponentBlueprint.make({
name: 'core-page-wrapper',
params: define =>
define({
component: SwappablePageWrapper,
loader: () => (props: PageWrapperProps) => {
const { title, tabs, children } = props;
return (
<>
<Header title={title} tabs={tabs} />
{children}
</>
);
},
}),
});
```
**Registered in:** `plugins/app/src/plugin.ts`
The app plugin now provides the `@backstage/ui` Header implementation, which will be used instead of the default plain HTML version.
## Component Hierarchy
```
┌─────────────────────────────────────────────────────────────┐
│ @backstage/frontend-plugin-api │
│ │
│ PageWrapper (swappable component) │
│ ├─ Default: Plain HTML implementation │
│ └─ Interface: PageWrapperProps │
└─────────────────────────────────────────────────────────────┘
│ overrides
┌─────────────────────────────────────────────────────────────┐
│ @backstage/plugin-app │
│ │
│ PageWrapper extension │
│ └─ Implementation: @backstage/ui Header │
└─────────────────────────────────────────────────────────────┘
```
## Default Implementation Details
The default `PageWrapper` implementation provides:
### Header Section
- Title display with basic typography
- Tab navigation with links
- Simple border and spacing
### Styling
- Uses inline styles (no external CSS dependencies)
- Minimal but functional design
- Ensures the component is usable without any UI library
### Example Output (Default)
```html
<div data-component="page-wrapper">
<header style="...">
<div style="...">{title}</div>
<nav style="...">
<a href="/tab1">Tab 1</a>
<a href="/tab2">Tab 2</a>
</nav>
</header>
{children}
</div>
```
## Benefits of This Approach
### 1. Clean Dependencies
-`@backstage/frontend-plugin-api` remains dependency-free
- ✅ No circular dependencies
- ✅ Clear separation of concerns
### 2. Flexibility
- Apps can use the default HTML implementation
- Apps can use the `@backstage/ui` implementation (via plugin-app)
- Apps can provide completely custom implementations
### 3. Testability
- Default implementation can be tested without UI library dependencies
- Easy to test in isolation
- No need to mock UI components
### 4. Progressive Enhancement
- Works out of the box with plain HTML
- Enhanced with better UI when `@backstage/plugin-app` is installed
- Graceful degradation if UI library is not available
## How to Override PageWrapper
Apps can provide their own implementation:
```typescript
import { SwappableComponentBlueprint } from '@backstage/plugin-app-react';
import { PageWrapper as SwappablePageWrapper } from '@backstage/frontend-plugin-api';
import { MyCustomHeader } from './components';
const CustomPageWrapper = SwappableComponentBlueprint.make({
name: 'custom-page-wrapper',
params: define =>
define({
component: SwappablePageWrapper,
loader: () => props =>
<MyCustomHeader {...props}>{props.children}</MyCustomHeader>,
}),
});
// Add to app plugin extensions
```
## Files Modified
### Created
1. `packages/frontend-plugin-api/src/components/PageWrapper.tsx` - Swappable component
### Modified
2. `packages/frontend-plugin-api/src/components/index.ts` - Export PageWrapper
3. `packages/frontend-plugin-api/src/blueprints/PageBlueprint.tsx` - Use PageWrapper instead of Header
4. `plugins/app/src/extensions/components.tsx` - Provide @backstage/ui implementation
5. `plugins/app/src/extensions/index.ts` - Export PageWrapper extension
6. `plugins/app/src/plugin.ts` - Register PageWrapper extension
## API Surface
### PageTab Interface
```typescript
export interface PageTab {
id: string;
label: string;
href: string;
matchStrategy?: 'prefix' | 'exact';
}
```
### PageWrapperProps Interface
```typescript
export interface PageWrapperProps {
title?: string;
tabs?: PageTab[];
children?: ReactNode;
}
```
### PageWrapper Component
```typescript
export const PageWrapper: {
(props: PageWrapperProps): JSX.Element | null;
ref: SwappableComponentRef<PageWrapperProps>;
};
```
## Testing Strategy
### Unit Tests
- Test default PageWrapper implementation
- Test PageWrapper with various prop combinations
- Test tab rendering
- Test children rendering
### Integration Tests
- Test PageBlueprint using PageWrapper
- Test swapping PageWrapper implementation
- Test @backstage/ui Header integration via plugin-app
### Visual Tests
- Compare default vs @backstage/ui implementations
- Verify styling consistency
- Test responsive behavior
## Migration Impact
### For Core Backstage
- ✅ No breaking changes
- ✅ Existing apps automatically get @backstage/ui Header via plugin-app
- ✅ New architecture is more maintainable
### For Plugin Authors
- ✅ No changes required
- ✅ PageBlueprint API remains the same
- ✅ SubPageBlueprint API remains the same
### For App Developers
- ✅ Can continue using default setup
- ✅ Can customize PageWrapper if desired
- ✅ Can disable @backstage/ui implementation if needed
## Future Enhancements
### Potential Additions
1. **Breadcrumbs Support**: Add breadcrumbs to PageWrapperProps
2. **Actions Support**: Add header actions to PageWrapperProps
3. **Themes**: Support for theme-aware default styling
4. **Layouts**: Different layout modes (full-width, centered, etc.)
### API Evolution
The swappable component pattern makes it easy to evolve the API:
- Add new optional props without breaking changes
- Provide richer default implementations over time
- Support multiple variants (e.g., PageWrapperCompact, PageWrapperExpanded)
## Comparison to Previous Implementation
### Before
```
PageBlueprint
└─> Directly imports @backstage/ui Header ❌
└─> Creates dependency violation
```
### After
```
PageBlueprint
└─> Uses PageWrapper (swappable) ✅
├─> Default: Plain HTML
└─> App Plugin: @backstage/ui Header
```
## Conclusion
This refactor successfully:
- ✅ Removes the invalid dependency from `@backstage/frontend-plugin-api` to `@backstage/ui`
- ✅ Provides a functional default implementation using plain HTML
- ✅ Maintains the ability to use the rich `@backstage/ui` Header via plugin-app
- ✅ Follows established Backstage patterns for swappable components
- ✅ Enables future customization and extensibility
- ✅ Passes all type checks
The architecture is now cleaner, more flexible, and follows Backstage best practices.