remove planning docs
Signed-off-by: Patrik Oldsberg <poldsberg@gmail.com>
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user