@bluealba/platform-cli 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +278 -15
- package/docs/404.mdx +5 -0
- package/docs/architecture/api-explorer.mdx +478 -0
- package/docs/architecture/architecture-diagrams.mdx +12 -0
- package/docs/architecture/authentication-system.mdx +903 -0
- package/docs/architecture/authorization-system.mdx +886 -0
- package/docs/architecture/bootstrap.mdx +1442 -0
- package/docs/architecture/gateway-architecture.mdx +845 -0
- package/docs/architecture/multi-tenancy.mdx +1150 -0
- package/docs/architecture/overview.mdx +776 -0
- package/docs/architecture/scheduler.mdx +818 -0
- package/docs/architecture/shell.mdx +885 -0
- package/docs/architecture/ui-extension-points.mdx +781 -0
- package/docs/architecture/user-states.mdx +794 -0
- package/docs/development/overview.mdx +21 -0
- package/docs/development/workflow.mdx +914 -0
- package/docs/getting-started/core-concepts.mdx +892 -0
- package/docs/getting-started/installation.mdx +780 -0
- package/docs/getting-started/overview.mdx +83 -0
- package/docs/getting-started/quick-start.mdx +940 -0
- package/docs/guides/adding-documentation-sites.mdx +1367 -0
- package/docs/guides/creating-services.mdx +1736 -0
- package/docs/guides/creating-ui-modules.mdx +1860 -0
- package/docs/guides/identity-providers.mdx +1007 -0
- package/docs/guides/mermaid-diagrams.mdx +212 -0
- package/docs/guides/using-feature-flags.mdx +1059 -0
- package/docs/guides/working-with-rooms.mdx +566 -0
- package/docs/index.mdx +57 -0
- package/docs/platform-cli/commands.mdx +604 -0
- package/docs/platform-cli/overview.mdx +195 -0
- package/package.json +5 -2
- package/skills/ba-platform/platform-cli.skill.md +26 -0
- package/skills/ba-platform/platform.skill.md +35 -0
- package/templates/application-monorepo-template/gitignore +95 -0
- package/templates/bootstrap-service-template/Dockerfile.development +1 -1
- package/templates/bootstrap-service-template/gitignore +57 -0
- package/templates/bootstrap-service-template/package.json +1 -1
- package/templates/bootstrap-service-template/src/main.ts +6 -16
- package/templates/customization-ui-module-template/Dockerfile.development +1 -1
- package/templates/customization-ui-module-template/gitignore +73 -0
- package/templates/nestjs-service-module-template/Dockerfile.development +1 -1
- package/templates/nestjs-service-module-template/gitignore +56 -0
- package/templates/platform-init-template/{{platformName}}-core/gitignore +97 -0
- package/templates/platform-init-template/{{platformName}}-core/local/.env.example +1 -1
- package/templates/platform-init-template/{{platformName}}-core/local/platform-docker-compose.yml +1 -1
- package/templates/platform-init-template/{{platformName}}-core/local/{{platformName}}-core-docker-compose.yml +0 -1
- package/templates/react-ui-module-template/Dockerfile +1 -1
- package/templates/react-ui-module-template/Dockerfile.development +1 -3
- package/templates/react-ui-module-template/caddy/Caddyfile +1 -1
- package/templates/react-ui-module-template/gitignore +72 -0
- package/templates/react-ui-module-template/Dockerfile_nginx +0 -11
- package/templates/react-ui-module-template/nginx/default.conf +0 -23
|
@@ -0,0 +1,1860 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Creating UI Modules
|
|
3
|
+
description: Complete guide to creating React-based micro-frontend UI modules for the Blue Alba Platform
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
import { Card, CardGrid, Aside, Tabs, TabItem } from '@astrojs/starlight/components';
|
|
7
|
+
import MermaidDiagram from '~/components/MermaidDiagram.astro';
|
|
8
|
+
|
|
9
|
+
The Blue Alba Platform uses a micro-frontend architecture built on React and single-spa, enabling you to develop, deploy, and integrate independent UI modules that compose into a cohesive application experience. This guide walks you through everything you need to create production-ready UI modules.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
UI modules are self-contained React applications that integrate seamlessly into the platform's shell. Each module runs independently but can communicate with other modules through the platform's shared state, extension points, and event system.
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Architecture
|
|
18
|
+
|
|
19
|
+
The platform's micro-frontend architecture uses single-spa as the orchestration layer, with custom integrations for authorization, routing, and inter-module communication.
|
|
20
|
+
|
|
21
|
+
<MermaidDiagram
|
|
22
|
+
title="Micro-Frontend Architecture"
|
|
23
|
+
code={`graph TB
|
|
24
|
+
A[Platform Shell<br/>pae-shell-ui]:::shell --> B[Application Catalog]:::catalog
|
|
25
|
+
B --> C[UI Module 1<br/>pae-admin-ui]:::module
|
|
26
|
+
B --> D[UI Module 2<br/>Custom App UI]:::module
|
|
27
|
+
B --> E[UI Module N<br/>...]:::module
|
|
28
|
+
|
|
29
|
+
F[pae-ui-react-core<br/>Shared Library]:::lib --> C
|
|
30
|
+
F --> D
|
|
31
|
+
F --> E
|
|
32
|
+
|
|
33
|
+
G[Gateway API]:::gateway --> H[Backend Services]:::services
|
|
34
|
+
C -.HTTP.-> G
|
|
35
|
+
D -.HTTP.-> G
|
|
36
|
+
E -.HTTP.-> G
|
|
37
|
+
|
|
38
|
+
I[single-spa<br/>Orchestrator]:::orchestrator --> A
|
|
39
|
+
I --> C
|
|
40
|
+
I --> D
|
|
41
|
+
I --> E
|
|
42
|
+
|
|
43
|
+
classDef shell fill:#90EE90,color:#333
|
|
44
|
+
classDef catalog fill:#FFD700,color:#333
|
|
45
|
+
classDef module fill:#87CEEB,color:#333
|
|
46
|
+
classDef lib fill:#FFB6C1,color:#333
|
|
47
|
+
classDef gateway fill:#DDA0DD,color:#333
|
|
48
|
+
classDef services fill:#F0E68C,color:#333
|
|
49
|
+
classDef orchestrator fill:#FFA07A,color:#333`}
|
|
50
|
+
/>
|
|
51
|
+
|
|
52
|
+
### Architecture Components
|
|
53
|
+
|
|
54
|
+
- **Platform Shell**: The main container that hosts all UI modules and provides navigation
|
|
55
|
+
- **Application Catalog**: Registry of all available modules and their routes
|
|
56
|
+
- **UI Modules**: Independent React applications loaded dynamically
|
|
57
|
+
- **pae-ui-react-core**: Shared library providing hooks, components, and utilities
|
|
58
|
+
- **single-spa**: Runtime orchestration layer managing module lifecycles
|
|
59
|
+
- **Gateway API**: Unified backend interface for all services
|
|
60
|
+
|
|
61
|
+
---
|
|
62
|
+
|
|
63
|
+
## Quick Start
|
|
64
|
+
|
|
65
|
+
Create your first UI module in five steps:
|
|
66
|
+
|
|
67
|
+
### 1. Create Project Structure
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
mkdir my-app-ui
|
|
71
|
+
cd my-app-ui
|
|
72
|
+
npm init -y
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### 2. Install Dependencies
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Install SDK and core library as dev dependencies
|
|
79
|
+
npm install --save-dev @bluealba/pae-ui-react-sdk
|
|
80
|
+
npm install --save-dev @bluealba/pae-ui-react-core
|
|
81
|
+
|
|
82
|
+
# Install runtime dependencies
|
|
83
|
+
npm install react@18 react-dom@18
|
|
84
|
+
npm install react-router@6 react-router-dom@6
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
### 3. Configure package.json
|
|
88
|
+
|
|
89
|
+
```json
|
|
90
|
+
{
|
|
91
|
+
"name": "@myorg/my-app-ui",
|
|
92
|
+
"version": "1.0.0",
|
|
93
|
+
"displayName": "My Application",
|
|
94
|
+
"scripts": {
|
|
95
|
+
"start": "pae-ui-sdk start",
|
|
96
|
+
"start:dev": "pae-ui-sdk start --dev",
|
|
97
|
+
"build": "pae-ui-sdk build",
|
|
98
|
+
"lint": "pae-ui-sdk lint",
|
|
99
|
+
"lint:fix": "pae-ui-sdk lint --fix"
|
|
100
|
+
},
|
|
101
|
+
"devDependencies": {
|
|
102
|
+
"@bluealba/pae-ui-react-sdk": "*",
|
|
103
|
+
"@bluealba/pae-ui-react-core": "*"
|
|
104
|
+
},
|
|
105
|
+
"dependencies": {
|
|
106
|
+
"@bluealba/pae-core": "*",
|
|
107
|
+
"react": "^18.3.1",
|
|
108
|
+
"react-dom": "^18.3.1",
|
|
109
|
+
"react-router": "^6.26.1",
|
|
110
|
+
"react-router-dom": "^6.26.1"
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
### 4. Create Source Files
|
|
116
|
+
|
|
117
|
+
Create the following file structure:
|
|
118
|
+
|
|
119
|
+
```
|
|
120
|
+
my-app-ui/
|
|
121
|
+
├── package.json
|
|
122
|
+
└── src/
|
|
123
|
+
├── main.tsx # Module entry point
|
|
124
|
+
├── root.component.tsx # Root React component
|
|
125
|
+
├── menu.ts # Navigation menu
|
|
126
|
+
└── Icon.tsx # Application icon
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### 5. Implement Core Files
|
|
130
|
+
|
|
131
|
+
<Tabs>
|
|
132
|
+
<TabItem label="main.tsx">
|
|
133
|
+
|
|
134
|
+
```typescript
|
|
135
|
+
import React from "react";
|
|
136
|
+
import ReactDOMClient from "react-dom/client";
|
|
137
|
+
import { ErrorState, initializeMicroFrontend } from "@bluealba/pae-ui-react-core";
|
|
138
|
+
import Root from "./root.component";
|
|
139
|
+
|
|
140
|
+
export const { bootstrap, mount, unmount } = initializeMicroFrontend({
|
|
141
|
+
React,
|
|
142
|
+
ReactDOMClient,
|
|
143
|
+
rootComponent: Root,
|
|
144
|
+
errorBoundary(err) {
|
|
145
|
+
console.error(err);
|
|
146
|
+
return <ErrorState />
|
|
147
|
+
}
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
// Export menu for shell integration
|
|
151
|
+
export { default as menu } from "./menu";
|
|
152
|
+
|
|
153
|
+
// Export application icon
|
|
154
|
+
export { default as icon } from './Icon';
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
</TabItem>
|
|
158
|
+
|
|
159
|
+
<TabItem label="root.component.tsx">
|
|
160
|
+
|
|
161
|
+
```typescript
|
|
162
|
+
import { BrowserRouter, Routes, Route } from 'react-router-dom';
|
|
163
|
+
|
|
164
|
+
export default function Root() {
|
|
165
|
+
return (
|
|
166
|
+
<BrowserRouter basename="/my-app">
|
|
167
|
+
<Routes>
|
|
168
|
+
<Route path="/" element={<HomePage />} />
|
|
169
|
+
<Route path="/about" element={<AboutPage />} />
|
|
170
|
+
</Routes>
|
|
171
|
+
</BrowserRouter>
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function HomePage() {
|
|
176
|
+
return <h1>Welcome to My App</h1>;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function AboutPage() {
|
|
180
|
+
return <h1>About My App</h1>;
|
|
181
|
+
}
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
</TabItem>
|
|
185
|
+
|
|
186
|
+
<TabItem label="menu.ts">
|
|
187
|
+
|
|
188
|
+
```typescript
|
|
189
|
+
export default [
|
|
190
|
+
{
|
|
191
|
+
label: 'Home',
|
|
192
|
+
path: '/',
|
|
193
|
+
},
|
|
194
|
+
{
|
|
195
|
+
label: 'Features',
|
|
196
|
+
items: [
|
|
197
|
+
{
|
|
198
|
+
label: 'Dashboard',
|
|
199
|
+
path: '/dashboard',
|
|
200
|
+
operations: ['my-app::dashboard::read']
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
label: 'Settings',
|
|
204
|
+
path: '/settings',
|
|
205
|
+
operations: ['my-app::settings::read']
|
|
206
|
+
}
|
|
207
|
+
]
|
|
208
|
+
},
|
|
209
|
+
{
|
|
210
|
+
label: 'About',
|
|
211
|
+
path: '/about',
|
|
212
|
+
}
|
|
213
|
+
];
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
</TabItem>
|
|
217
|
+
|
|
218
|
+
<TabItem label="Icon.tsx">
|
|
219
|
+
|
|
220
|
+
```typescript
|
|
221
|
+
const Icon = () => (
|
|
222
|
+
<svg width="24" height="24" viewBox="0 0 24 24" fill="none">
|
|
223
|
+
<path d="M12 2L2 7v10c0 5.5 4.5 10 10 10s10-4.5 10-10V7L12 2z"
|
|
224
|
+
fill="currentColor"/>
|
|
225
|
+
</svg>
|
|
226
|
+
);
|
|
227
|
+
|
|
228
|
+
export default Icon;
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
</TabItem>
|
|
232
|
+
</Tabs>
|
|
233
|
+
|
|
234
|
+
Now run `npm run start:dev` to start the development server!
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
## Project Structure
|
|
239
|
+
|
|
240
|
+
A typical UI module follows this structure:
|
|
241
|
+
|
|
242
|
+
```
|
|
243
|
+
my-app-ui/
|
|
244
|
+
├── package.json # Project configuration
|
|
245
|
+
├── tsconfig.json # TypeScript configuration (optional)
|
|
246
|
+
├── webpack.config.js # Custom webpack config (optional)
|
|
247
|
+
├── assets/ # Static assets (images, fonts)
|
|
248
|
+
│ └── logo.svg
|
|
249
|
+
└── src/
|
|
250
|
+
├── main.tsx # Module entry point (required)
|
|
251
|
+
├── root.component.tsx # Root component (required)
|
|
252
|
+
├── menu.ts # Navigation menu
|
|
253
|
+
├── Icon.tsx # Application icon
|
|
254
|
+
├── views/ # Page components
|
|
255
|
+
│ ├── HomePage.tsx
|
|
256
|
+
│ ├── DashboardPage.tsx
|
|
257
|
+
│ └── SettingsPage.tsx
|
|
258
|
+
├── components/ # Reusable components
|
|
259
|
+
│ ├── Header.tsx
|
|
260
|
+
│ └── Card.tsx
|
|
261
|
+
├── hooks/ # Custom hooks
|
|
262
|
+
│ └── useMyData.ts
|
|
263
|
+
├── services/ # API service layer
|
|
264
|
+
│ └── api.ts
|
|
265
|
+
├── utils/ # Utility functions
|
|
266
|
+
│ └── helpers.ts
|
|
267
|
+
└── styles/ # Stylesheets
|
|
268
|
+
└── global.css
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Key Files Explained
|
|
272
|
+
|
|
273
|
+
<CardGrid>
|
|
274
|
+
<Card title="main.tsx" icon="seti:javascript">
|
|
275
|
+
Entry point that initializes the micro-frontend using `initializeMicroFrontend()`. Exports lifecycle methods (bootstrap, mount, unmount) and optional menu/icon.
|
|
276
|
+
</Card>
|
|
277
|
+
|
|
278
|
+
<Card title="root.component.tsx" icon="react">
|
|
279
|
+
Root React component containing your application's main structure, typically including routing configuration.
|
|
280
|
+
</Card>
|
|
281
|
+
|
|
282
|
+
<Card title="menu.ts" icon="list">
|
|
283
|
+
Menu configuration exported for shell integration. Defines navigation structure with optional operation-based access control.
|
|
284
|
+
</Card>
|
|
285
|
+
|
|
286
|
+
<Card title="Icon.tsx" icon="seti:image">
|
|
287
|
+
SVG component displayed in the application selector. Should be 24x24px and use `currentColor` for theme support.
|
|
288
|
+
</Card>
|
|
289
|
+
</CardGrid>
|
|
290
|
+
|
|
291
|
+
---
|
|
292
|
+
|
|
293
|
+
## Development Workflow
|
|
294
|
+
|
|
295
|
+
### Running Locally with Docker
|
|
296
|
+
|
|
297
|
+
The recommended way to develop UI modules is using Docker Compose, which provides a consistent development environment and automatic integration with the platform.
|
|
298
|
+
|
|
299
|
+
### Project Structure Setup
|
|
300
|
+
|
|
301
|
+
Your repositories should be organized at the same level:
|
|
302
|
+
|
|
303
|
+
```
|
|
304
|
+
my-projects/
|
|
305
|
+
├── my-app-local/ # Local environment with docker-compose.yml
|
|
306
|
+
│ └── docker-compose.yml
|
|
307
|
+
└── my-app-ui/ # Your UI module repository
|
|
308
|
+
├── Dockerfile.development
|
|
309
|
+
├── package.json
|
|
310
|
+
└── src/
|
|
311
|
+
```
|
|
312
|
+
|
|
313
|
+
<Aside type="note">
|
|
314
|
+
The docker-compose.yml uses `${PWD}/../my-app-ui` to reference the UI repository. This requires both repositories to be siblings in the same parent directory.
|
|
315
|
+
</Aside>
|
|
316
|
+
|
|
317
|
+
### Step 1: Create Development Dockerfile
|
|
318
|
+
|
|
319
|
+
Create a `Dockerfile.development` in your UI module repository:
|
|
320
|
+
|
|
321
|
+
```dockerfile
|
|
322
|
+
FROM node:20-alpine as development
|
|
323
|
+
|
|
324
|
+
ENV NODE_ENV=development
|
|
325
|
+
ENV PORT=80
|
|
326
|
+
|
|
327
|
+
ARG BA_NPM_AUTH_TOKEN
|
|
328
|
+
|
|
329
|
+
WORKDIR /app
|
|
330
|
+
|
|
331
|
+
EXPOSE 80
|
|
332
|
+
|
|
333
|
+
CMD ["npm", "run", "start:dev"]
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
**Key points:**
|
|
337
|
+
- Uses Node 20 Alpine for a lightweight image
|
|
338
|
+
- Sets `NODE_ENV=development` for development mode
|
|
339
|
+
- Exposes port 80 (configurable)
|
|
340
|
+
- Runs `start:dev` command for hot reload
|
|
341
|
+
|
|
342
|
+
### Step 2: Configure Docker Compose
|
|
343
|
+
|
|
344
|
+
Add your UI module to the `docker-compose.yml` in your local environment repository:
|
|
345
|
+
|
|
346
|
+
```yaml
|
|
347
|
+
services:
|
|
348
|
+
my-app-ui:
|
|
349
|
+
build:
|
|
350
|
+
context: ../my-app-ui
|
|
351
|
+
dockerfile: Dockerfile.development
|
|
352
|
+
ports:
|
|
353
|
+
- 9001:80
|
|
354
|
+
volumes:
|
|
355
|
+
- ${PWD}/../my-app-ui:/app
|
|
356
|
+
environment:
|
|
357
|
+
- NODE_ENV=development
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
**Configuration explained:**
|
|
361
|
+
|
|
362
|
+
| Property | Description |
|
|
363
|
+
|----------|-------------|
|
|
364
|
+
| `context: ../my-app-ui` | Path to your UI repository (relative to compose file) |
|
|
365
|
+
| `dockerfile: Dockerfile.development` | Development-specific Dockerfile |
|
|
366
|
+
| `ports: 9001:80` | Maps host port 9001 to container port 80 |
|
|
367
|
+
| `volumes` | Mounts your code for hot reload (changes reflect immediately) |
|
|
368
|
+
| `environment` | Sets environment variables |
|
|
369
|
+
|
|
370
|
+
### Step 3: Start Development Environment
|
|
371
|
+
|
|
372
|
+
From your local environment repository:
|
|
373
|
+
|
|
374
|
+
```bash
|
|
375
|
+
# Start your UI module
|
|
376
|
+
docker-compose up my-app-ui
|
|
377
|
+
|
|
378
|
+
# Or start in detached mode
|
|
379
|
+
docker-compose up -d my-app-ui
|
|
380
|
+
|
|
381
|
+
# View logs
|
|
382
|
+
docker-compose logs -f my-app-ui
|
|
383
|
+
```
|
|
384
|
+
|
|
385
|
+
**What happens:**
|
|
386
|
+
1. Docker builds the development image
|
|
387
|
+
2. Installs npm dependencies inside the container
|
|
388
|
+
3. Starts the dev server with hot reload (`npm run start:dev`)
|
|
389
|
+
4. Your code is mounted as a volume - changes trigger automatic rebuilds
|
|
390
|
+
5. Module is accessible at `http://localhost:9001`
|
|
391
|
+
|
|
392
|
+
---
|
|
393
|
+
|
|
394
|
+
## Core Concepts
|
|
395
|
+
|
|
396
|
+
### Initializing a Micro-Frontend
|
|
397
|
+
|
|
398
|
+
The `initializeMicroFrontend()` function is the foundation of every UI module. It creates a single-spa application with platform integration.
|
|
399
|
+
|
|
400
|
+
```typescript
|
|
401
|
+
import { initializeMicroFrontend } from "@bluealba/pae-ui-react-core";
|
|
402
|
+
|
|
403
|
+
export const { bootstrap, mount, unmount } = initializeMicroFrontend({
|
|
404
|
+
React,
|
|
405
|
+
ReactDOMClient,
|
|
406
|
+
rootComponent: Root,
|
|
407
|
+
errorBoundary(err, info) {
|
|
408
|
+
console.error(err);
|
|
409
|
+
// Return custom error UI
|
|
410
|
+
return <ErrorState error={err} />;
|
|
411
|
+
}
|
|
412
|
+
});
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
**Configuration Options:**
|
|
416
|
+
|
|
417
|
+
| Option | Type | Description | Required |
|
|
418
|
+
|--------|------|-------------|----------|
|
|
419
|
+
| `React` | React | React library instance | Yes |
|
|
420
|
+
| `ReactDOMClient` | ReactDOMClient | React DOM client for rendering | Yes |
|
|
421
|
+
| `rootComponent` | React.Component | Your application's root component | Yes |
|
|
422
|
+
| `errorBoundary` | Function | Error boundary handler returning JSX | Yes |
|
|
423
|
+
|
|
424
|
+
**Lifecycle Methods:**
|
|
425
|
+
|
|
426
|
+
The function returns three methods that single-spa calls:
|
|
427
|
+
|
|
428
|
+
- `bootstrap()`: Called once when module first loads (initialize resources)
|
|
429
|
+
- `mount()`: Called when module becomes active (render to DOM)
|
|
430
|
+
- `unmount()`: Called when module becomes inactive (cleanup)
|
|
431
|
+
|
|
432
|
+
### Navigation and Routing
|
|
433
|
+
|
|
434
|
+
Use React Router for internal navigation and the platform's `navigateTo()` for cross-module navigation.
|
|
435
|
+
|
|
436
|
+
<Tabs>
|
|
437
|
+
<TabItem label="Internal Routing">
|
|
438
|
+
|
|
439
|
+
```typescript
|
|
440
|
+
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom';
|
|
441
|
+
|
|
442
|
+
export default function Root() {
|
|
443
|
+
return (
|
|
444
|
+
<BrowserRouter basename="/my-app">
|
|
445
|
+
<nav>
|
|
446
|
+
<Link to="/">Home</Link>
|
|
447
|
+
<Link to="/dashboard">Dashboard</Link>
|
|
448
|
+
</nav>
|
|
449
|
+
|
|
450
|
+
<Routes>
|
|
451
|
+
<Route path="/" element={<HomePage />} />
|
|
452
|
+
<Route path="/dashboard" element={<DashboardPage />} />
|
|
453
|
+
<Route path="/settings" element={<SettingsPage />} />
|
|
454
|
+
<Route path="*" element={<NotFoundPage />} />
|
|
455
|
+
</Routes>
|
|
456
|
+
</BrowserRouter>
|
|
457
|
+
);
|
|
458
|
+
}
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
<Aside type="note">
|
|
462
|
+
Always set `basename` to match your module's route in the catalog (e.g., `/my-app`).
|
|
463
|
+
</Aside>
|
|
464
|
+
|
|
465
|
+
</TabItem>
|
|
466
|
+
|
|
467
|
+
<TabItem label="Cross-Module Navigation">
|
|
468
|
+
|
|
469
|
+
```typescript
|
|
470
|
+
import { navigateTo } from '@bluealba/pae-ui-react-core';
|
|
471
|
+
|
|
472
|
+
function NavigationExample() {
|
|
473
|
+
const goToAdmin = () => {
|
|
474
|
+
// Navigate to another module
|
|
475
|
+
navigateTo('/admin/users');
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
const goToExternal = () => {
|
|
479
|
+
// Open external link
|
|
480
|
+
window.open('https://example.com', '_blank');
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
return (
|
|
484
|
+
<div>
|
|
485
|
+
<button onClick={goToAdmin}>
|
|
486
|
+
Go to Admin
|
|
487
|
+
</button>
|
|
488
|
+
<button onClick={goToExternal}>
|
|
489
|
+
External Link
|
|
490
|
+
</button>
|
|
491
|
+
</div>
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
```
|
|
495
|
+
|
|
496
|
+
</TabItem>
|
|
497
|
+
|
|
498
|
+
<TabItem label="Programmatic Navigation">
|
|
499
|
+
|
|
500
|
+
```typescript
|
|
501
|
+
import { useNavigate } from 'react-router-dom';
|
|
502
|
+
import { navigateTo } from '@bluealba/pae-ui-react-core';
|
|
503
|
+
|
|
504
|
+
function NavigationComponent() {
|
|
505
|
+
// Internal navigation (within module)
|
|
506
|
+
const navigate = useNavigate();
|
|
507
|
+
|
|
508
|
+
const handleInternalNav = () => {
|
|
509
|
+
navigate('/dashboard');
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
// External navigation (to other modules)
|
|
513
|
+
const handleExternalNav = () => {
|
|
514
|
+
navigateTo('/admin');
|
|
515
|
+
};
|
|
516
|
+
|
|
517
|
+
return (
|
|
518
|
+
<div>
|
|
519
|
+
<button onClick={handleInternalNav}>Dashboard</button>
|
|
520
|
+
<button onClick={handleExternalNav}>Admin Panel</button>
|
|
521
|
+
</div>
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
```
|
|
525
|
+
|
|
526
|
+
</TabItem>
|
|
527
|
+
|
|
528
|
+
<TabItem label="Route Parameters">
|
|
529
|
+
|
|
530
|
+
```typescript
|
|
531
|
+
import { useParams, useSearchParams } from 'react-router-dom';
|
|
532
|
+
|
|
533
|
+
function UserDetailPage() {
|
|
534
|
+
// Path parameters: /users/:userId
|
|
535
|
+
const { userId } = useParams();
|
|
536
|
+
|
|
537
|
+
// Query parameters: /users?tab=profile
|
|
538
|
+
const [searchParams] = useSearchParams();
|
|
539
|
+
const tab = searchParams.get('tab');
|
|
540
|
+
|
|
541
|
+
return (
|
|
542
|
+
<div>
|
|
543
|
+
<h1>User {userId}</h1>
|
|
544
|
+
<p>Current tab: {tab}</p>
|
|
545
|
+
</div>
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
```
|
|
549
|
+
|
|
550
|
+
</TabItem>
|
|
551
|
+
</Tabs>
|
|
552
|
+
|
|
553
|
+
### Authentication and Authorization
|
|
554
|
+
|
|
555
|
+
The platform provides built-in authentication and operation-based authorization.
|
|
556
|
+
|
|
557
|
+
<Tabs>
|
|
558
|
+
<TabItem label="useAuth Hook">
|
|
559
|
+
|
|
560
|
+
```typescript
|
|
561
|
+
import { useAuth } from '@bluealba/pae-ui-react-core';
|
|
562
|
+
|
|
563
|
+
function ProfileComponent() {
|
|
564
|
+
const { authUser, hasAccess } = useAuth();
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<div>
|
|
568
|
+
<h2>Welcome, {authUser.displayName}</h2>
|
|
569
|
+
<p>Username: {authUser.username}</p>
|
|
570
|
+
<p>Email: {authUser.email}</p>
|
|
571
|
+
<p>Initials: {authUser.initials}</p>
|
|
572
|
+
|
|
573
|
+
{hasAccess('admin::users::write') && (
|
|
574
|
+
<button>Edit User</button>
|
|
575
|
+
)}
|
|
576
|
+
</div>
|
|
577
|
+
);
|
|
578
|
+
}
|
|
579
|
+
```
|
|
580
|
+
|
|
581
|
+
**AuthUser Properties:**
|
|
582
|
+
- `displayName`: Full name (e.g., "John Doe")
|
|
583
|
+
- `username`: Username (e.g., "johndoe")
|
|
584
|
+
- `email`: Email address
|
|
585
|
+
- `givenName`: First name
|
|
586
|
+
- `familyName`: Last name
|
|
587
|
+
- `initials`: Initials (e.g., "JD")
|
|
588
|
+
- `operations`: Array of granted operation strings
|
|
589
|
+
|
|
590
|
+
</TabItem>
|
|
591
|
+
|
|
592
|
+
<TabItem label="Authorized Component">
|
|
593
|
+
|
|
594
|
+
```typescript
|
|
595
|
+
import { Authorized } from '@bluealba/pae-ui-react-core';
|
|
596
|
+
|
|
597
|
+
function DashboardPage() {
|
|
598
|
+
return (
|
|
599
|
+
<div>
|
|
600
|
+
<h1>Dashboard</h1>
|
|
601
|
+
|
|
602
|
+
{/* Render only if user has operations */}
|
|
603
|
+
<Authorized operations={['dashboard::read']}>
|
|
604
|
+
<DashboardContent />
|
|
605
|
+
</Authorized>
|
|
606
|
+
|
|
607
|
+
{/* Show alternative content if unauthorized */}
|
|
608
|
+
<Authorized
|
|
609
|
+
operations={['admin::access']}
|
|
610
|
+
forbiddenContent={<div>Admin access required</div>}
|
|
611
|
+
>
|
|
612
|
+
<AdminSection />
|
|
613
|
+
</Authorized>
|
|
614
|
+
|
|
615
|
+
{/* Multiple operations (user needs ALL) */}
|
|
616
|
+
<Authorized operations={['users::read', 'users::write']}>
|
|
617
|
+
<UserManagement />
|
|
618
|
+
</Authorized>
|
|
619
|
+
</div>
|
|
620
|
+
);
|
|
621
|
+
}
|
|
622
|
+
```
|
|
623
|
+
|
|
624
|
+
**forbiddenContent Options:**
|
|
625
|
+
```typescript
|
|
626
|
+
// Render function
|
|
627
|
+
forbiddenContent={({ operations }) => (
|
|
628
|
+
<NoAccessMessage operations={operations} />
|
|
629
|
+
)}
|
|
630
|
+
|
|
631
|
+
// Component reference
|
|
632
|
+
forbiddenContent={NoAccessComponent}
|
|
633
|
+
|
|
634
|
+
// Direct JSX
|
|
635
|
+
forbiddenContent={<div>Access Denied</div>}
|
|
636
|
+
```
|
|
637
|
+
|
|
638
|
+
</TabItem>
|
|
639
|
+
|
|
640
|
+
<TabItem label="Route Protection">
|
|
641
|
+
|
|
642
|
+
```typescript
|
|
643
|
+
import { useAuth } from '@bluealba/pae-ui-react-core';
|
|
644
|
+
import { Navigate } from 'react-router-dom';
|
|
645
|
+
|
|
646
|
+
// Protected route wrapper
|
|
647
|
+
function ProtectedRoute({
|
|
648
|
+
children,
|
|
649
|
+
operations
|
|
650
|
+
}: {
|
|
651
|
+
children: React.ReactNode;
|
|
652
|
+
operations: string[];
|
|
653
|
+
}) {
|
|
654
|
+
const { hasAccess } = useAuth();
|
|
655
|
+
|
|
656
|
+
if (!hasAccess(...operations)) {
|
|
657
|
+
return <Navigate to="/unauthorized" replace />;
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
return <>{children}</>;
|
|
661
|
+
}
|
|
662
|
+
|
|
663
|
+
// Usage in routes
|
|
664
|
+
function Root() {
|
|
665
|
+
return (
|
|
666
|
+
<BrowserRouter basename="/my-app">
|
|
667
|
+
<Routes>
|
|
668
|
+
<Route path="/" element={<HomePage />} />
|
|
669
|
+
|
|
670
|
+
<Route
|
|
671
|
+
path="/admin"
|
|
672
|
+
element={
|
|
673
|
+
<ProtectedRoute operations={['admin::access']}>
|
|
674
|
+
<AdminPage />
|
|
675
|
+
</ProtectedRoute>
|
|
676
|
+
}
|
|
677
|
+
/>
|
|
678
|
+
|
|
679
|
+
<Route path="/unauthorized" element={<UnauthorizedPage />} />
|
|
680
|
+
</Routes>
|
|
681
|
+
</BrowserRouter>
|
|
682
|
+
);
|
|
683
|
+
}
|
|
684
|
+
```
|
|
685
|
+
|
|
686
|
+
</TabItem>
|
|
687
|
+
|
|
688
|
+
<TabItem label="Menu Authorization">
|
|
689
|
+
|
|
690
|
+
```typescript
|
|
691
|
+
// menu.ts
|
|
692
|
+
export default [
|
|
693
|
+
{
|
|
694
|
+
label: 'Home',
|
|
695
|
+
path: '/',
|
|
696
|
+
// No operations = visible to all
|
|
697
|
+
},
|
|
698
|
+
{
|
|
699
|
+
label: 'Dashboard',
|
|
700
|
+
path: '/dashboard',
|
|
701
|
+
operations: ['my-app::dashboard::read']
|
|
702
|
+
},
|
|
703
|
+
{
|
|
704
|
+
label: 'Admin',
|
|
705
|
+
operations: ['my-app::admin::access'],
|
|
706
|
+
items: [
|
|
707
|
+
{
|
|
708
|
+
label: 'Users',
|
|
709
|
+
path: '/admin/users',
|
|
710
|
+
operations: ['my-app::users::read']
|
|
711
|
+
},
|
|
712
|
+
{
|
|
713
|
+
label: 'Settings',
|
|
714
|
+
path: '/admin/settings',
|
|
715
|
+
operations: ['my-app::settings::write']
|
|
716
|
+
}
|
|
717
|
+
]
|
|
718
|
+
}
|
|
719
|
+
];
|
|
720
|
+
```
|
|
721
|
+
|
|
722
|
+
The shell automatically filters menu items based on user operations.
|
|
723
|
+
|
|
724
|
+
</TabItem>
|
|
725
|
+
</Tabs>
|
|
726
|
+
|
|
727
|
+
### Service Communication
|
|
728
|
+
|
|
729
|
+
Communicate with backend services using the `useServiceInvoker` hook.
|
|
730
|
+
|
|
731
|
+
<Tabs>
|
|
732
|
+
<TabItem label="Basic Usage">
|
|
733
|
+
|
|
734
|
+
```typescript
|
|
735
|
+
import { useServiceInvoker } from '@bluealba/pae-ui-react-core';
|
|
736
|
+
|
|
737
|
+
function UsersComponent() {
|
|
738
|
+
const invoke = useServiceInvoker('@myorg/users-service');
|
|
739
|
+
const [users, setUsers] = useState([]);
|
|
740
|
+
|
|
741
|
+
useEffect(() => {
|
|
742
|
+
// GET request
|
|
743
|
+
invoke('/users', { method: 'GET' })
|
|
744
|
+
.then(res => res.json())
|
|
745
|
+
.then(setUsers)
|
|
746
|
+
.catch(console.error);
|
|
747
|
+
}, [invoke]);
|
|
748
|
+
|
|
749
|
+
return (
|
|
750
|
+
<ul>
|
|
751
|
+
{users.map(user => (
|
|
752
|
+
<li key={user.id}>{user.name}</li>
|
|
753
|
+
))}
|
|
754
|
+
</ul>
|
|
755
|
+
);
|
|
756
|
+
}
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
</TabItem>
|
|
760
|
+
|
|
761
|
+
<TabItem label="POST/PUT/DELETE">
|
|
762
|
+
|
|
763
|
+
```typescript
|
|
764
|
+
import { useServiceInvoker } from '@bluealba/pae-ui-react-core';
|
|
765
|
+
|
|
766
|
+
function UserForm() {
|
|
767
|
+
const invoke = useServiceInvoker('@myorg/users-service');
|
|
768
|
+
|
|
769
|
+
const createUser = async (userData: any) => {
|
|
770
|
+
const response = await invoke('/users', {
|
|
771
|
+
method: 'POST',
|
|
772
|
+
headers: {
|
|
773
|
+
'Content-Type': 'application/json',
|
|
774
|
+
},
|
|
775
|
+
body: JSON.stringify(userData)
|
|
776
|
+
});
|
|
777
|
+
|
|
778
|
+
if (!response.ok) {
|
|
779
|
+
throw new Error('Failed to create user');
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
return response.json();
|
|
783
|
+
};
|
|
784
|
+
|
|
785
|
+
const updateUser = async (userId: string, userData: any) => {
|
|
786
|
+
const response = await invoke(`/users/${userId}`, {
|
|
787
|
+
method: 'PUT',
|
|
788
|
+
headers: {
|
|
789
|
+
'Content-Type': 'application/json',
|
|
790
|
+
},
|
|
791
|
+
body: JSON.stringify(userData)
|
|
792
|
+
});
|
|
793
|
+
|
|
794
|
+
return response.json();
|
|
795
|
+
};
|
|
796
|
+
|
|
797
|
+
const deleteUser = async (userId: string) => {
|
|
798
|
+
await invoke(`/users/${userId}`, {
|
|
799
|
+
method: 'DELETE'
|
|
800
|
+
});
|
|
801
|
+
};
|
|
802
|
+
|
|
803
|
+
// ... form implementation
|
|
804
|
+
}
|
|
805
|
+
```
|
|
806
|
+
|
|
807
|
+
</TabItem>
|
|
808
|
+
|
|
809
|
+
<TabItem label="With React Query">
|
|
810
|
+
|
|
811
|
+
```typescript
|
|
812
|
+
import { useServiceInvoker } from '@bluealba/pae-ui-react-core';
|
|
813
|
+
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
|
|
814
|
+
|
|
815
|
+
function UsersManagement() {
|
|
816
|
+
const invoke = useServiceInvoker('@myorg/users-service');
|
|
817
|
+
const queryClient = useQueryClient();
|
|
818
|
+
|
|
819
|
+
// Fetch users
|
|
820
|
+
const { data: users, isLoading } = useQuery({
|
|
821
|
+
queryKey: ['users'],
|
|
822
|
+
queryFn: () => invoke('/users').then(res => res.json())
|
|
823
|
+
});
|
|
824
|
+
|
|
825
|
+
// Create user mutation
|
|
826
|
+
const createMutation = useMutation({
|
|
827
|
+
mutationFn: (userData: any) =>
|
|
828
|
+
invoke('/users', {
|
|
829
|
+
method: 'POST',
|
|
830
|
+
headers: { 'Content-Type': 'application/json' },
|
|
831
|
+
body: JSON.stringify(userData)
|
|
832
|
+
}).then(res => res.json()),
|
|
833
|
+
onSuccess: () => {
|
|
834
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
835
|
+
}
|
|
836
|
+
});
|
|
837
|
+
|
|
838
|
+
// Delete user mutation
|
|
839
|
+
const deleteMutation = useMutation({
|
|
840
|
+
mutationFn: (userId: string) =>
|
|
841
|
+
invoke(`/users/${userId}`, { method: 'DELETE' }),
|
|
842
|
+
onSuccess: () => {
|
|
843
|
+
queryClient.invalidateQueries({ queryKey: ['users'] });
|
|
844
|
+
}
|
|
845
|
+
});
|
|
846
|
+
|
|
847
|
+
if (isLoading) return <div>Loading...</div>;
|
|
848
|
+
|
|
849
|
+
return (
|
|
850
|
+
<div>
|
|
851
|
+
{users?.map(user => (
|
|
852
|
+
<div key={user.id}>
|
|
853
|
+
{user.name}
|
|
854
|
+
<button onClick={() => deleteMutation.mutate(user.id)}>
|
|
855
|
+
Delete
|
|
856
|
+
</button>
|
|
857
|
+
</div>
|
|
858
|
+
))}
|
|
859
|
+
</div>
|
|
860
|
+
);
|
|
861
|
+
}
|
|
862
|
+
```
|
|
863
|
+
|
|
864
|
+
</TabItem>
|
|
865
|
+
|
|
866
|
+
<TabItem label="Error Handling">
|
|
867
|
+
|
|
868
|
+
```typescript
|
|
869
|
+
import { useServiceInvoker } from '@bluealba/pae-ui-react-core';
|
|
870
|
+
|
|
871
|
+
function RobustComponent() {
|
|
872
|
+
const invoke = useServiceInvoker('@myorg/my-service');
|
|
873
|
+
|
|
874
|
+
const fetchData = async () => {
|
|
875
|
+
try {
|
|
876
|
+
const response = await invoke('/data', { method: 'GET' });
|
|
877
|
+
|
|
878
|
+
if (!response.ok) {
|
|
879
|
+
if (response.status === 404) {
|
|
880
|
+
throw new Error('Data not found');
|
|
881
|
+
} else if (response.status === 403) {
|
|
882
|
+
throw new Error('Access denied');
|
|
883
|
+
} else {
|
|
884
|
+
throw new Error(`Request failed: ${response.status}`);
|
|
885
|
+
}
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
const data = await response.json();
|
|
889
|
+
return data;
|
|
890
|
+
|
|
891
|
+
} catch (error) {
|
|
892
|
+
if (error instanceof TypeError) {
|
|
893
|
+
// Network error
|
|
894
|
+
console.error('Network error:', error);
|
|
895
|
+
throw new Error('Cannot connect to server');
|
|
896
|
+
}
|
|
897
|
+
throw error;
|
|
898
|
+
}
|
|
899
|
+
};
|
|
900
|
+
|
|
901
|
+
// ... component implementation
|
|
902
|
+
}
|
|
903
|
+
```
|
|
904
|
+
|
|
905
|
+
</TabItem>
|
|
906
|
+
</Tabs>
|
|
907
|
+
|
|
908
|
+
### Extension Points
|
|
909
|
+
|
|
910
|
+
Extension points allow modules to declare customizable parts that other modules can extend.
|
|
911
|
+
|
|
912
|
+
<Tabs>
|
|
913
|
+
<TabItem label="Declaring Extension Points">
|
|
914
|
+
|
|
915
|
+
**Inline with Component:**
|
|
916
|
+
|
|
917
|
+
```typescript
|
|
918
|
+
import { ExtensionPoint } from '@bluealba/pae-ui-react-core';
|
|
919
|
+
|
|
920
|
+
function DashboardPage() {
|
|
921
|
+
return (
|
|
922
|
+
<div>
|
|
923
|
+
<h1>Dashboard</h1>
|
|
924
|
+
|
|
925
|
+
{/* Declare an extension point */}
|
|
926
|
+
<ExtensionPoint name="DashboardWidgets">
|
|
927
|
+
{/* Default content if not extended */}
|
|
928
|
+
<DefaultWidget />
|
|
929
|
+
</ExtensionPoint>
|
|
930
|
+
|
|
931
|
+
<ExtensionPoint name="DashboardActions" />
|
|
932
|
+
</div>
|
|
933
|
+
);
|
|
934
|
+
}
|
|
935
|
+
```
|
|
936
|
+
|
|
937
|
+
**Whole Component as Extension Point:**
|
|
938
|
+
|
|
939
|
+
```typescript
|
|
940
|
+
import { extensionPoint } from '@bluealba/pae-ui-react-core';
|
|
941
|
+
|
|
942
|
+
const CustomHeader: React.FC = () => {
|
|
943
|
+
return (
|
|
944
|
+
<header>
|
|
945
|
+
<h1>Default Header</h1>
|
|
946
|
+
</header>
|
|
947
|
+
);
|
|
948
|
+
};
|
|
949
|
+
|
|
950
|
+
// Set display name for extension point identification
|
|
951
|
+
CustomHeader.displayName = 'ApplicationHeader';
|
|
952
|
+
|
|
953
|
+
// Make entire component replaceable
|
|
954
|
+
export default extensionPoint(CustomHeader);
|
|
955
|
+
```
|
|
956
|
+
|
|
957
|
+
</TabItem>
|
|
958
|
+
|
|
959
|
+
<TabItem label="Extending Extension Points">
|
|
960
|
+
|
|
961
|
+
**Using Component:**
|
|
962
|
+
|
|
963
|
+
```typescript
|
|
964
|
+
import { ExtendExtensionPoint } from '@bluealba/pae-ui-react-core';
|
|
965
|
+
|
|
966
|
+
function Root() {
|
|
967
|
+
return (
|
|
968
|
+
<div>
|
|
969
|
+
{/* Your app content */}
|
|
970
|
+
<MyAppContent />
|
|
971
|
+
|
|
972
|
+
{/* Extend another module's extension point */}
|
|
973
|
+
<ExtendExtensionPoint
|
|
974
|
+
module="@bluealba/pae-admin-ui"
|
|
975
|
+
point="DashboardWidgets"
|
|
976
|
+
>
|
|
977
|
+
<CustomWidget title="My Custom Widget">
|
|
978
|
+
<p>This content extends the admin dashboard!</p>
|
|
979
|
+
</CustomWidget>
|
|
980
|
+
</ExtendExtensionPoint>
|
|
981
|
+
</div>
|
|
982
|
+
);
|
|
983
|
+
}
|
|
984
|
+
```
|
|
985
|
+
|
|
986
|
+
**Using Hook:**
|
|
987
|
+
|
|
988
|
+
```typescript
|
|
989
|
+
import { useExtendExtensionPoint } from '@bluealba/pae-ui-react-core';
|
|
990
|
+
|
|
991
|
+
const CustomApplicationSelector = () => {
|
|
992
|
+
return <div>Custom App Selector</div>;
|
|
993
|
+
};
|
|
994
|
+
|
|
995
|
+
function Root() {
|
|
996
|
+
// Register extension programmatically
|
|
997
|
+
useExtendExtensionPoint(
|
|
998
|
+
'@bluealba/pae-shell-ui',
|
|
999
|
+
'ApplicationSelector',
|
|
1000
|
+
CustomApplicationSelector
|
|
1001
|
+
);
|
|
1002
|
+
|
|
1003
|
+
return <div>My App Content</div>;
|
|
1004
|
+
}
|
|
1005
|
+
```
|
|
1006
|
+
|
|
1007
|
+
</TabItem>
|
|
1008
|
+
|
|
1009
|
+
<TabItem label="Real-World Example">
|
|
1010
|
+
|
|
1011
|
+
```typescript
|
|
1012
|
+
// In pae-admin-ui: Declare extension point
|
|
1013
|
+
import { ExtensionPoint } from '@bluealba/pae-ui-react-core';
|
|
1014
|
+
|
|
1015
|
+
function SettingsPage() {
|
|
1016
|
+
return (
|
|
1017
|
+
<div>
|
|
1018
|
+
<h1>Settings</h1>
|
|
1019
|
+
|
|
1020
|
+
<section>
|
|
1021
|
+
<h2>Core Settings</h2>
|
|
1022
|
+
{/* Core settings content */}
|
|
1023
|
+
</section>
|
|
1024
|
+
|
|
1025
|
+
{/* Allow other modules to add settings sections */}
|
|
1026
|
+
<ExtensionPoint name="AdditionalSettings" />
|
|
1027
|
+
</div>
|
|
1028
|
+
);
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
// In custom-app-ui: Extend the settings page
|
|
1032
|
+
import { ExtendExtensionPoint } from '@bluealba/pae-ui-react-core';
|
|
1033
|
+
|
|
1034
|
+
function Root() {
|
|
1035
|
+
return (
|
|
1036
|
+
<>
|
|
1037
|
+
<MyAppRoutes />
|
|
1038
|
+
|
|
1039
|
+
<ExtendExtensionPoint
|
|
1040
|
+
module="@bluealba/pae-admin-ui"
|
|
1041
|
+
point="AdditionalSettings"
|
|
1042
|
+
>
|
|
1043
|
+
<section>
|
|
1044
|
+
<h2>My App Settings</h2>
|
|
1045
|
+
<CustomSettings />
|
|
1046
|
+
</section>
|
|
1047
|
+
</ExtendExtensionPoint>
|
|
1048
|
+
</>
|
|
1049
|
+
);
|
|
1050
|
+
}
|
|
1051
|
+
```
|
|
1052
|
+
|
|
1053
|
+
</TabItem>
|
|
1054
|
+
</Tabs>
|
|
1055
|
+
|
|
1056
|
+
---
|
|
1057
|
+
|
|
1058
|
+
## Platform Integration
|
|
1059
|
+
|
|
1060
|
+
### Registering in the Application Catalog
|
|
1061
|
+
|
|
1062
|
+
UI modules must be registered in the catalog to be discovered and loaded by the shell.
|
|
1063
|
+
|
|
1064
|
+
<Tabs>
|
|
1065
|
+
<TabItem label="Via API">
|
|
1066
|
+
|
|
1067
|
+
```bash
|
|
1068
|
+
POST /api/catalog
|
|
1069
|
+
Content-Type: application/json
|
|
1070
|
+
Authorization: Bearer <admin-jwt>
|
|
1071
|
+
|
|
1072
|
+
{
|
|
1073
|
+
"name": "@myorg/my-app-ui",
|
|
1074
|
+
"type": "app",
|
|
1075
|
+
"displayName": "My Application",
|
|
1076
|
+
"baseUrl": "/my-app",
|
|
1077
|
+
"host": "my-app-ui",
|
|
1078
|
+
"port": 80,
|
|
1079
|
+
"activationRoute": "/my-app",
|
|
1080
|
+
"dependsOn": ["@bluealba/pae-shell-ui"]
|
|
1081
|
+
"bundleFile": "my-app-ui.js",
|
|
1082
|
+
"mountAtSelector": "#pae-shell-ui-content",
|
|
1083
|
+
"customProps": {},
|
|
1084
|
+
"applicationName": "my-app-id",
|
|
1085
|
+
"authorization": {
|
|
1086
|
+
"operations": []
|
|
1087
|
+
}
|
|
1088
|
+
}
|
|
1089
|
+
```
|
|
1090
|
+
|
|
1091
|
+
<Aside type="tip">
|
|
1092
|
+
Use `"type": "tool"` instead of `"type": "app"` if your module should appear in the navbar tools section (bottom area) rather than the application selector. This is appropriate for platform utilities and administration modules such as Admin UI or Documentation UI.
|
|
1093
|
+
</Aside>
|
|
1094
|
+
|
|
1095
|
+
</TabItem>
|
|
1096
|
+
|
|
1097
|
+
<TabItem label="Via Bootstrap">
|
|
1098
|
+
|
|
1099
|
+
For automated deployments, include in bootstrap configuration:
|
|
1100
|
+
|
|
1101
|
+
```json
|
|
1102
|
+
{
|
|
1103
|
+
{
|
|
1104
|
+
"name": "@myorg/my-app-ui",
|
|
1105
|
+
"displayName": "My Application",
|
|
1106
|
+
"type": "app",
|
|
1107
|
+
"baseUrl": "/my-app",
|
|
1108
|
+
"service": {
|
|
1109
|
+
"host": "my-app-ui",
|
|
1110
|
+
"port": 80
|
|
1111
|
+
},
|
|
1112
|
+
"ui": {
|
|
1113
|
+
"route": "/my-app",
|
|
1114
|
+
"mountAtSelector": "#pae-shell-ui-content",
|
|
1115
|
+
"bundleFile": "my-app-ui.js",
|
|
1116
|
+
"customProps": {}
|
|
1117
|
+
},
|
|
1118
|
+
"dependsOn": ["@bluealba/pae-shell-ui"]
|
|
1119
|
+
},
|
|
1120
|
+
}
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
</TabItem>
|
|
1124
|
+
|
|
1125
|
+
</Tabs>
|
|
1126
|
+
|
|
1127
|
+
### Menu Integration
|
|
1128
|
+
|
|
1129
|
+
The shell automatically renders your menu in the navigation sidebar.
|
|
1130
|
+
|
|
1131
|
+
<Tabs>
|
|
1132
|
+
<TabItem label="Simple Menu">
|
|
1133
|
+
|
|
1134
|
+
```typescript
|
|
1135
|
+
// menu.ts
|
|
1136
|
+
export default [
|
|
1137
|
+
{
|
|
1138
|
+
label: 'Home',
|
|
1139
|
+
path: '/'
|
|
1140
|
+
},
|
|
1141
|
+
{
|
|
1142
|
+
label: 'Dashboard',
|
|
1143
|
+
path: '/dashboard'
|
|
1144
|
+
},
|
|
1145
|
+
{
|
|
1146
|
+
label: 'Settings',
|
|
1147
|
+
path: '/settings'
|
|
1148
|
+
}
|
|
1149
|
+
];
|
|
1150
|
+
```
|
|
1151
|
+
|
|
1152
|
+
</TabItem>
|
|
1153
|
+
|
|
1154
|
+
<TabItem label="Nested Menu">
|
|
1155
|
+
|
|
1156
|
+
```typescript
|
|
1157
|
+
// menu.ts
|
|
1158
|
+
export default [
|
|
1159
|
+
{
|
|
1160
|
+
label: 'Home',
|
|
1161
|
+
path: '/'
|
|
1162
|
+
},
|
|
1163
|
+
{
|
|
1164
|
+
label: 'Management',
|
|
1165
|
+
items: [
|
|
1166
|
+
{
|
|
1167
|
+
label: 'Users',
|
|
1168
|
+
path: '/users'
|
|
1169
|
+
},
|
|
1170
|
+
{
|
|
1171
|
+
label: 'Roles',
|
|
1172
|
+
path: '/roles'
|
|
1173
|
+
},
|
|
1174
|
+
{
|
|
1175
|
+
label: 'Permissions',
|
|
1176
|
+
path: '/permissions'
|
|
1177
|
+
}
|
|
1178
|
+
]
|
|
1179
|
+
},
|
|
1180
|
+
{
|
|
1181
|
+
label: 'Reports',
|
|
1182
|
+
items: [
|
|
1183
|
+
{
|
|
1184
|
+
label: 'Analytics',
|
|
1185
|
+
path: '/reports/analytics'
|
|
1186
|
+
},
|
|
1187
|
+
{
|
|
1188
|
+
label: 'Audit Log',
|
|
1189
|
+
path: '/reports/audit'
|
|
1190
|
+
}
|
|
1191
|
+
]
|
|
1192
|
+
}
|
|
1193
|
+
];
|
|
1194
|
+
```
|
|
1195
|
+
|
|
1196
|
+
</TabItem>
|
|
1197
|
+
|
|
1198
|
+
<TabItem label="With Operations">
|
|
1199
|
+
|
|
1200
|
+
```typescript
|
|
1201
|
+
// menu.ts
|
|
1202
|
+
export default [
|
|
1203
|
+
{
|
|
1204
|
+
label: 'Home',
|
|
1205
|
+
path: '/',
|
|
1206
|
+
// No operations = visible to all
|
|
1207
|
+
},
|
|
1208
|
+
{
|
|
1209
|
+
label: 'Dashboard',
|
|
1210
|
+
path: '/dashboard',
|
|
1211
|
+
operations: ['my-app::dashboard::read']
|
|
1212
|
+
},
|
|
1213
|
+
{
|
|
1214
|
+
label: 'Admin',
|
|
1215
|
+
// Parent requires operation
|
|
1216
|
+
operations: ['my-app::admin::access'],
|
|
1217
|
+
items: [
|
|
1218
|
+
{
|
|
1219
|
+
label: 'Users',
|
|
1220
|
+
path: '/admin/users',
|
|
1221
|
+
// Child inherits parent operations + own operations
|
|
1222
|
+
operations: ['my-app::users::manage']
|
|
1223
|
+
},
|
|
1224
|
+
{
|
|
1225
|
+
label: 'Settings',
|
|
1226
|
+
path: '/admin/settings',
|
|
1227
|
+
operations: ['my-app::settings::write']
|
|
1228
|
+
}
|
|
1229
|
+
]
|
|
1230
|
+
}
|
|
1231
|
+
];
|
|
1232
|
+
```
|
|
1233
|
+
|
|
1234
|
+
Shell filters menu items based on user operations automatically.
|
|
1235
|
+
|
|
1236
|
+
</TabItem>
|
|
1237
|
+
|
|
1238
|
+
<TabItem label="Dynamic Menu">
|
|
1239
|
+
|
|
1240
|
+
```typescript
|
|
1241
|
+
// menu.ts - can export a function for dynamic menus
|
|
1242
|
+
export default function getMenu(context: any) {
|
|
1243
|
+
const items = [
|
|
1244
|
+
{
|
|
1245
|
+
label: 'Home',
|
|
1246
|
+
path: '/'
|
|
1247
|
+
}
|
|
1248
|
+
];
|
|
1249
|
+
|
|
1250
|
+
// Add items conditionally
|
|
1251
|
+
if (context.user.hasFeature('advanced')) {
|
|
1252
|
+
items.push({
|
|
1253
|
+
label: 'Advanced',
|
|
1254
|
+
path: '/advanced'
|
|
1255
|
+
});
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
return items;
|
|
1259
|
+
}
|
|
1260
|
+
```
|
|
1261
|
+
|
|
1262
|
+
</TabItem>
|
|
1263
|
+
</Tabs>
|
|
1264
|
+
|
|
1265
|
+
### Application Icon
|
|
1266
|
+
|
|
1267
|
+
Provide an icon for the application selector in the shell.
|
|
1268
|
+
|
|
1269
|
+
```typescript
|
|
1270
|
+
// Icon.tsx
|
|
1271
|
+
const Icon = () => (
|
|
1272
|
+
<svg
|
|
1273
|
+
width="24"
|
|
1274
|
+
height="24"
|
|
1275
|
+
viewBox="0 0 24 24"
|
|
1276
|
+
fill="none"
|
|
1277
|
+
>
|
|
1278
|
+
<path
|
|
1279
|
+
d="M12 2L2 7v10c0 5.5 4.5 10 10 10s10-4.5 10-10V7L12 2z"
|
|
1280
|
+
fill="currentColor"
|
|
1281
|
+
/>
|
|
1282
|
+
</svg>
|
|
1283
|
+
);
|
|
1284
|
+
|
|
1285
|
+
export default Icon;
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
**Icon Guidelines:**
|
|
1289
|
+
- Size: 24x24px viewBox
|
|
1290
|
+
- Color: Use `currentColor` for theme support
|
|
1291
|
+
- Format: Inline SVG component
|
|
1292
|
+
- Style: Simple, recognizable at small sizes
|
|
1293
|
+
- Export: Default export from `Icon.tsx`
|
|
1294
|
+
|
|
1295
|
+
---
|
|
1296
|
+
|
|
1297
|
+
## State Management
|
|
1298
|
+
|
|
1299
|
+
Choose a state management solution based on your needs.
|
|
1300
|
+
|
|
1301
|
+
<Tabs>
|
|
1302
|
+
<TabItem label="React Context">
|
|
1303
|
+
|
|
1304
|
+
```typescript
|
|
1305
|
+
// AppContext.tsx
|
|
1306
|
+
import { createContext, useContext, useState } from 'react';
|
|
1307
|
+
|
|
1308
|
+
interface AppState {
|
|
1309
|
+
user: any;
|
|
1310
|
+
settings: any;
|
|
1311
|
+
}
|
|
1312
|
+
|
|
1313
|
+
const AppContext = createContext<{
|
|
1314
|
+
state: AppState;
|
|
1315
|
+
setState: (state: Partial<AppState>) => void;
|
|
1316
|
+
} | null>(null);
|
|
1317
|
+
|
|
1318
|
+
export function AppProvider({ children }: { children: React.ReactNode }) {
|
|
1319
|
+
const [state, setState] = useState<AppState>({
|
|
1320
|
+
user: null,
|
|
1321
|
+
settings: {}
|
|
1322
|
+
});
|
|
1323
|
+
|
|
1324
|
+
const updateState = (updates: Partial<AppState>) => {
|
|
1325
|
+
setState(prev => ({ ...prev, ...updates }));
|
|
1326
|
+
};
|
|
1327
|
+
|
|
1328
|
+
return (
|
|
1329
|
+
<AppContext.Provider value={{ state, setState: updateState }}>
|
|
1330
|
+
{children}
|
|
1331
|
+
</AppContext.Provider>
|
|
1332
|
+
);
|
|
1333
|
+
}
|
|
1334
|
+
|
|
1335
|
+
export function useAppState() {
|
|
1336
|
+
const context = useContext(AppContext);
|
|
1337
|
+
if (!context) throw new Error('useAppState must be used within AppProvider');
|
|
1338
|
+
return context;
|
|
1339
|
+
}
|
|
1340
|
+
```
|
|
1341
|
+
|
|
1342
|
+
</TabItem>
|
|
1343
|
+
|
|
1344
|
+
<TabItem label="Zustand">
|
|
1345
|
+
|
|
1346
|
+
```typescript
|
|
1347
|
+
// store.ts
|
|
1348
|
+
import { create } from 'zustand';
|
|
1349
|
+
|
|
1350
|
+
interface AppStore {
|
|
1351
|
+
user: any;
|
|
1352
|
+
settings: any;
|
|
1353
|
+
setUser: (user: any) => void;
|
|
1354
|
+
setSettings: (settings: any) => void;
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
export const useStore = create<AppStore>((set) => ({
|
|
1358
|
+
user: null,
|
|
1359
|
+
settings: {},
|
|
1360
|
+
setUser: (user) => set({ user }),
|
|
1361
|
+
setSettings: (settings) => set({ settings }),
|
|
1362
|
+
}));
|
|
1363
|
+
|
|
1364
|
+
// Usage
|
|
1365
|
+
function Component() {
|
|
1366
|
+
const user = useStore(state => state.user);
|
|
1367
|
+
const setUser = useStore(state => state.setUser);
|
|
1368
|
+
|
|
1369
|
+
return <div>{user?.name}</div>;
|
|
1370
|
+
}
|
|
1371
|
+
```
|
|
1372
|
+
|
|
1373
|
+
</TabItem>
|
|
1374
|
+
|
|
1375
|
+
<TabItem label="React Query">
|
|
1376
|
+
|
|
1377
|
+
```typescript
|
|
1378
|
+
// Ideal for server state management
|
|
1379
|
+
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
|
|
1380
|
+
|
|
1381
|
+
const queryClient = new QueryClient({
|
|
1382
|
+
defaultOptions: {
|
|
1383
|
+
queries: {
|
|
1384
|
+
refetchOnWindowFocus: false,
|
|
1385
|
+
retry: 1
|
|
1386
|
+
}
|
|
1387
|
+
}
|
|
1388
|
+
});
|
|
1389
|
+
|
|
1390
|
+
function Root() {
|
|
1391
|
+
return (
|
|
1392
|
+
<QueryClientProvider client={queryClient}>
|
|
1393
|
+
<App />
|
|
1394
|
+
</QueryClientProvider>
|
|
1395
|
+
);
|
|
1396
|
+
}
|
|
1397
|
+
```
|
|
1398
|
+
|
|
1399
|
+
</TabItem>
|
|
1400
|
+
|
|
1401
|
+
<TabItem label="Platform State">
|
|
1402
|
+
|
|
1403
|
+
```typescript
|
|
1404
|
+
// Access platform-wide state
|
|
1405
|
+
import { usePAEState } from '@bluealba/pae-ui-react-core';
|
|
1406
|
+
|
|
1407
|
+
function Component() {
|
|
1408
|
+
const paeState = usePAEState();
|
|
1409
|
+
|
|
1410
|
+
// Access catalog
|
|
1411
|
+
console.log(paeState.catalog);
|
|
1412
|
+
|
|
1413
|
+
// Access current user
|
|
1414
|
+
console.log(paeState.user);
|
|
1415
|
+
|
|
1416
|
+
// Access current application
|
|
1417
|
+
console.log(paeState.currentApplication);
|
|
1418
|
+
|
|
1419
|
+
return <div>Component</div>;
|
|
1420
|
+
}
|
|
1421
|
+
```
|
|
1422
|
+
|
|
1423
|
+
</TabItem>
|
|
1424
|
+
</Tabs>
|
|
1425
|
+
|
|
1426
|
+
---
|
|
1427
|
+
|
|
1428
|
+
## Best Practices
|
|
1429
|
+
|
|
1430
|
+
### Component Organization
|
|
1431
|
+
|
|
1432
|
+
<CardGrid>
|
|
1433
|
+
<Card title="Feature-Based Structure" icon="folder">
|
|
1434
|
+
Organize by feature/domain rather than technical type (components, hooks, etc.).
|
|
1435
|
+
</Card>
|
|
1436
|
+
|
|
1437
|
+
<Card title="Colocation" icon="document">
|
|
1438
|
+
Keep related files together: component, styles, tests, and types in the same directory.
|
|
1439
|
+
</Card>
|
|
1440
|
+
|
|
1441
|
+
<Card title="Barrel Exports" icon="seti:javascript">
|
|
1442
|
+
Use index files to create clean import paths and encapsulate internal structure.
|
|
1443
|
+
</Card>
|
|
1444
|
+
|
|
1445
|
+
<Card title="Separation of Concerns" icon="setting">
|
|
1446
|
+
Separate presentational components from container components and business logic.
|
|
1447
|
+
</Card>
|
|
1448
|
+
</CardGrid>
|
|
1449
|
+
|
|
1450
|
+
**Recommended Structure:**
|
|
1451
|
+
|
|
1452
|
+
```
|
|
1453
|
+
src/
|
|
1454
|
+
├── features/
|
|
1455
|
+
│ ├── users/
|
|
1456
|
+
│ │ ├── components/
|
|
1457
|
+
│ │ │ ├── UserList.tsx
|
|
1458
|
+
│ │ │ ├── UserList.test.tsx
|
|
1459
|
+
│ │ │ ├── UserList.module.css
|
|
1460
|
+
│ │ │ └── index.ts
|
|
1461
|
+
│ │ ├── hooks/
|
|
1462
|
+
│ │ │ └── useUsers.ts
|
|
1463
|
+
│ │ ├── services/
|
|
1464
|
+
│ │ │ └── usersApi.ts
|
|
1465
|
+
│ │ ├── types/
|
|
1466
|
+
│ │ │ └── User.ts
|
|
1467
|
+
│ │ └── index.ts
|
|
1468
|
+
│ └── dashboard/
|
|
1469
|
+
├── shared/
|
|
1470
|
+
│ ├── components/
|
|
1471
|
+
│ ├── hooks/
|
|
1472
|
+
│ └── utils/
|
|
1473
|
+
├── main.tsx
|
|
1474
|
+
└── root.component.tsx
|
|
1475
|
+
```
|
|
1476
|
+
|
|
1477
|
+
### Error Handling
|
|
1478
|
+
|
|
1479
|
+
```typescript
|
|
1480
|
+
// Global error boundary
|
|
1481
|
+
import { ErrorBoundary } from 'react-error-boundary';
|
|
1482
|
+
|
|
1483
|
+
function ErrorFallback({ error, resetErrorBoundary }: any) {
|
|
1484
|
+
return (
|
|
1485
|
+
<div role="alert">
|
|
1486
|
+
<h2>Something went wrong</h2>
|
|
1487
|
+
<pre>{error.message}</pre>
|
|
1488
|
+
<button onClick={resetErrorBoundary}>Try again</button>
|
|
1489
|
+
</div>
|
|
1490
|
+
);
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function Root() {
|
|
1494
|
+
return (
|
|
1495
|
+
<ErrorBoundary FallbackComponent={ErrorFallback}>
|
|
1496
|
+
<App />
|
|
1497
|
+
</ErrorBoundary>
|
|
1498
|
+
);
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
// Error handling in async operations
|
|
1502
|
+
async function fetchData() {
|
|
1503
|
+
try {
|
|
1504
|
+
const response = await invoke('/data');
|
|
1505
|
+
if (!response.ok) {
|
|
1506
|
+
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
|
|
1507
|
+
}
|
|
1508
|
+
return await response.json();
|
|
1509
|
+
} catch (error) {
|
|
1510
|
+
console.error('Failed to fetch data:', error);
|
|
1511
|
+
// Show user-friendly error message
|
|
1512
|
+
showNotification('Failed to load data. Please try again.', 'error');
|
|
1513
|
+
throw error;
|
|
1514
|
+
}
|
|
1515
|
+
}
|
|
1516
|
+
```
|
|
1517
|
+
|
|
1518
|
+
### Accessibility
|
|
1519
|
+
|
|
1520
|
+
```typescript
|
|
1521
|
+
// Semantic HTML
|
|
1522
|
+
function AccessibleForm() {
|
|
1523
|
+
return (
|
|
1524
|
+
<form onSubmit={handleSubmit}>
|
|
1525
|
+
<label htmlFor="username">
|
|
1526
|
+
Username
|
|
1527
|
+
<input
|
|
1528
|
+
id="username"
|
|
1529
|
+
type="text"
|
|
1530
|
+
aria-required="true"
|
|
1531
|
+
aria-describedby="username-help"
|
|
1532
|
+
/>
|
|
1533
|
+
</label>
|
|
1534
|
+
<span id="username-help">Enter your username</span>
|
|
1535
|
+
|
|
1536
|
+
<button type="submit">Submit</button>
|
|
1537
|
+
</form>
|
|
1538
|
+
);
|
|
1539
|
+
}
|
|
1540
|
+
|
|
1541
|
+
// Keyboard navigation
|
|
1542
|
+
function AccessibleButton() {
|
|
1543
|
+
return (
|
|
1544
|
+
<button
|
|
1545
|
+
onClick={handleClick}
|
|
1546
|
+
onKeyDown={(e) => {
|
|
1547
|
+
if (e.key === 'Enter' || e.key === ' ') {
|
|
1548
|
+
handleClick();
|
|
1549
|
+
}
|
|
1550
|
+
}}
|
|
1551
|
+
aria-label="Close dialog"
|
|
1552
|
+
>
|
|
1553
|
+
×
|
|
1554
|
+
</button>
|
|
1555
|
+
);
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1558
|
+
// Focus management
|
|
1559
|
+
function Modal({ isOpen, onClose }: { isOpen: boolean; onClose: () => void }) {
|
|
1560
|
+
const modalRef = useRef<HTMLDivElement>(null);
|
|
1561
|
+
|
|
1562
|
+
useEffect(() => {
|
|
1563
|
+
if (isOpen) {
|
|
1564
|
+
modalRef.current?.focus();
|
|
1565
|
+
}
|
|
1566
|
+
}, [isOpen]);
|
|
1567
|
+
|
|
1568
|
+
return (
|
|
1569
|
+
<div
|
|
1570
|
+
ref={modalRef}
|
|
1571
|
+
role="dialog"
|
|
1572
|
+
aria-modal="true"
|
|
1573
|
+
tabIndex={-1}
|
|
1574
|
+
>
|
|
1575
|
+
{/* Modal content */}
|
|
1576
|
+
</div>
|
|
1577
|
+
);
|
|
1578
|
+
}
|
|
1579
|
+
```
|
|
1580
|
+
|
|
1581
|
+
### Testing
|
|
1582
|
+
|
|
1583
|
+
```typescript
|
|
1584
|
+
// Component testing
|
|
1585
|
+
import { render, screen, fireEvent } from '@testing-library/react';
|
|
1586
|
+
import { UserList } from './UserList';
|
|
1587
|
+
|
|
1588
|
+
describe('UserList', () => {
|
|
1589
|
+
it('renders users', () => {
|
|
1590
|
+
const users = [{ id: '1', name: 'John' }];
|
|
1591
|
+
render(<UserList users={users} />);
|
|
1592
|
+
|
|
1593
|
+
expect(screen.getByText('John')).toBeInTheDocument();
|
|
1594
|
+
});
|
|
1595
|
+
|
|
1596
|
+
it('handles user click', () => {
|
|
1597
|
+
const onUserClick = jest.fn();
|
|
1598
|
+
const users = [{ id: '1', name: 'John' }];
|
|
1599
|
+
|
|
1600
|
+
render(<UserList users={users} onUserClick={onUserClick} />);
|
|
1601
|
+
|
|
1602
|
+
fireEvent.click(screen.getByText('John'));
|
|
1603
|
+
expect(onUserClick).toHaveBeenCalledWith(users[0]);
|
|
1604
|
+
});
|
|
1605
|
+
});
|
|
1606
|
+
|
|
1607
|
+
// Hook testing
|
|
1608
|
+
import { renderHook } from '@testing-library/react';
|
|
1609
|
+
import { useAuth } from '@bluealba/pae-ui-react-core';
|
|
1610
|
+
|
|
1611
|
+
describe('useAuth', () => {
|
|
1612
|
+
it('returns auth user', () => {
|
|
1613
|
+
const { result } = renderHook(() => useAuth());
|
|
1614
|
+
|
|
1615
|
+
expect(result.current.authUser).toBeDefined();
|
|
1616
|
+
});
|
|
1617
|
+
});
|
|
1618
|
+
```
|
|
1619
|
+
|
|
1620
|
+
---
|
|
1621
|
+
|
|
1622
|
+
## Troubleshooting
|
|
1623
|
+
|
|
1624
|
+
<Tabs>
|
|
1625
|
+
<TabItem label="Build Failures">
|
|
1626
|
+
|
|
1627
|
+
**Problem: Module not found errors**
|
|
1628
|
+
|
|
1629
|
+
```
|
|
1630
|
+
ERROR in ./src/main.tsx
|
|
1631
|
+
Module not found: Error: Can't resolve '@bluealba/pae-ui-react-core'
|
|
1632
|
+
```
|
|
1633
|
+
|
|
1634
|
+
**Solution:**
|
|
1635
|
+
```bash
|
|
1636
|
+
# Install missing dependencies
|
|
1637
|
+
npm install
|
|
1638
|
+
|
|
1639
|
+
# Clear node_modules and reinstall
|
|
1640
|
+
rm -rf node_modules package-lock.json
|
|
1641
|
+
npm install
|
|
1642
|
+
```
|
|
1643
|
+
|
|
1644
|
+
---
|
|
1645
|
+
|
|
1646
|
+
**Problem: TypeScript errors**
|
|
1647
|
+
|
|
1648
|
+
```
|
|
1649
|
+
TS2307: Cannot find module '@bluealba/pae-ui-react-core'
|
|
1650
|
+
```
|
|
1651
|
+
|
|
1652
|
+
**Solution:**
|
|
1653
|
+
```bash
|
|
1654
|
+
# Ensure types are available
|
|
1655
|
+
npm install --save-dev @types/react @types/react-dom
|
|
1656
|
+
|
|
1657
|
+
# Rebuild
|
|
1658
|
+
npm run build
|
|
1659
|
+
```
|
|
1660
|
+
|
|
1661
|
+
</TabItem>
|
|
1662
|
+
|
|
1663
|
+
<TabItem label="Runtime Errors">
|
|
1664
|
+
|
|
1665
|
+
**Problem: Module doesn't load in shell**
|
|
1666
|
+
|
|
1667
|
+
**Checklist:**
|
|
1668
|
+
1. Verify bundle URL is accessible:
|
|
1669
|
+
```bash
|
|
1670
|
+
curl https://cdn.example.com/my-app/main.js
|
|
1671
|
+
```
|
|
1672
|
+
|
|
1673
|
+
2. Check browser console for errors
|
|
1674
|
+
|
|
1675
|
+
3. Verify catalog registration:
|
|
1676
|
+
```bash
|
|
1677
|
+
curl -H "Authorization: Bearer $TOKEN" \
|
|
1678
|
+
https://platform.example.com/api/catalog
|
|
1679
|
+
```
|
|
1680
|
+
|
|
1681
|
+
4. Check module exports bootstrap, mount, unmount:
|
|
1682
|
+
```typescript
|
|
1683
|
+
export const { bootstrap, mount, unmount } = initializeMicroFrontend(/*...*/);
|
|
1684
|
+
```
|
|
1685
|
+
|
|
1686
|
+
---
|
|
1687
|
+
|
|
1688
|
+
**Problem: Routing doesn't work**
|
|
1689
|
+
|
|
1690
|
+
**Solution:**
|
|
1691
|
+
Ensure `basename` matches catalog route:
|
|
1692
|
+
|
|
1693
|
+
```typescript
|
|
1694
|
+
// Catalog route: /my-app
|
|
1695
|
+
<BrowserRouter basename="/my-app">
|
|
1696
|
+
```
|
|
1697
|
+
|
|
1698
|
+
</TabItem>
|
|
1699
|
+
|
|
1700
|
+
<TabItem label="Authorization Issues">
|
|
1701
|
+
|
|
1702
|
+
**Problem: User can't access module**
|
|
1703
|
+
|
|
1704
|
+
**Causes:**
|
|
1705
|
+
|
|
1706
|
+
1. **Missing operation:**
|
|
1707
|
+
- Check user operations in Admin UI
|
|
1708
|
+
- Grant required operation to user/role
|
|
1709
|
+
|
|
1710
|
+
2. **Wrong route:**
|
|
1711
|
+
- Verify route in catalog matches navigation
|
|
1712
|
+
|
|
1713
|
+
**Debug:**
|
|
1714
|
+
```typescript
|
|
1715
|
+
import { useAuth } from '@bluealba/pae-ui-react-core';
|
|
1716
|
+
|
|
1717
|
+
function Debug() {
|
|
1718
|
+
const { authUser } = useAuth();
|
|
1719
|
+
console.log('User operations:', authUser.operations);
|
|
1720
|
+
return null;
|
|
1721
|
+
}
|
|
1722
|
+
```
|
|
1723
|
+
|
|
1724
|
+
</TabItem>
|
|
1725
|
+
|
|
1726
|
+
<TabItem label="Performance Issues">
|
|
1727
|
+
|
|
1728
|
+
**Problem: Slow initial load**
|
|
1729
|
+
|
|
1730
|
+
**Solutions:**
|
|
1731
|
+
|
|
1732
|
+
1. **Code splitting:**
|
|
1733
|
+
```typescript
|
|
1734
|
+
const HeavyComponent = lazy(() => import('./HeavyComponent'));
|
|
1735
|
+
```
|
|
1736
|
+
|
|
1737
|
+
2. **Bundle analysis:**
|
|
1738
|
+
```bash
|
|
1739
|
+
ANALYZE=true npm run build
|
|
1740
|
+
```
|
|
1741
|
+
|
|
1742
|
+
3. **Optimize dependencies:**
|
|
1743
|
+
```json
|
|
1744
|
+
// Use lighter alternatives
|
|
1745
|
+
// dayjs instead of moment
|
|
1746
|
+
// zustand instead of redux
|
|
1747
|
+
```
|
|
1748
|
+
|
|
1749
|
+
---
|
|
1750
|
+
|
|
1751
|
+
**Problem: Slow navigation**
|
|
1752
|
+
|
|
1753
|
+
**Solution:**
|
|
1754
|
+
Preload routes:
|
|
1755
|
+
```typescript
|
|
1756
|
+
import { Link } from 'react-router-dom';
|
|
1757
|
+
|
|
1758
|
+
<Link
|
|
1759
|
+
to="/dashboard"
|
|
1760
|
+
onMouseEnter={() => {
|
|
1761
|
+
// Preload route component
|
|
1762
|
+
import('./views/DashboardPage');
|
|
1763
|
+
}}
|
|
1764
|
+
>
|
|
1765
|
+
Dashboard
|
|
1766
|
+
</Link>
|
|
1767
|
+
```
|
|
1768
|
+
|
|
1769
|
+
</TabItem>
|
|
1770
|
+
</Tabs>
|
|
1771
|
+
|
|
1772
|
+
---
|
|
1773
|
+
|
|
1774
|
+
## Deployment
|
|
1775
|
+
|
|
1776
|
+
### Building for Production
|
|
1777
|
+
|
|
1778
|
+
```bash
|
|
1779
|
+
# Build optimized bundle
|
|
1780
|
+
npm run build
|
|
1781
|
+
|
|
1782
|
+
# Output: dist/main.js and dist/assets/
|
|
1783
|
+
```
|
|
1784
|
+
|
|
1785
|
+
### Hosting Options
|
|
1786
|
+
|
|
1787
|
+
<Tabs>
|
|
1788
|
+
|
|
1789
|
+
<TabItem label="Docker Container">
|
|
1790
|
+
|
|
1791
|
+
```dockerfile
|
|
1792
|
+
FROM nginxinc/nginx-unprivileged:mainline-alpine-slim
|
|
1793
|
+
|
|
1794
|
+
WORKDIR /usr/share/nginx/html
|
|
1795
|
+
|
|
1796
|
+
COPY ./version.json ./version.json
|
|
1797
|
+
COPY ./dist /usr/share/nginx/html
|
|
1798
|
+
|
|
1799
|
+
COPY ./nginx/default.conf /etc/nginx/conf.d/default.conf
|
|
1800
|
+
|
|
1801
|
+
EXPOSE 8080
|
|
1802
|
+
```
|
|
1803
|
+
|
|
1804
|
+
```nginx
|
|
1805
|
+
# nginx.conf
|
|
1806
|
+
server {
|
|
1807
|
+
listen 8080;
|
|
1808
|
+
listen [::]:8080;
|
|
1809
|
+
server_name localhost;
|
|
1810
|
+
|
|
1811
|
+
location / {
|
|
1812
|
+
root /usr/share/nginx/html;
|
|
1813
|
+
index index.html index.htm;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
location /version {
|
|
1817
|
+
default_type application/json;
|
|
1818
|
+
alias /usr/share/nginx/html/version.json;
|
|
1819
|
+
}
|
|
1820
|
+
|
|
1821
|
+
error_page 500 502 503 504 /50x.html;
|
|
1822
|
+
location = /50x.html {
|
|
1823
|
+
root /usr/share/nginx/html;
|
|
1824
|
+
}
|
|
1825
|
+
}
|
|
1826
|
+
```
|
|
1827
|
+
|
|
1828
|
+
Deploy:
|
|
1829
|
+
```bash
|
|
1830
|
+
docker build -t my-app-ui:1.0.0 .
|
|
1831
|
+
docker push registry.example.com/my-app-ui:1.0.0
|
|
1832
|
+
```
|
|
1833
|
+
|
|
1834
|
+
</TabItem>
|
|
1835
|
+
</Tabs>
|
|
1836
|
+
|
|
1837
|
+
### Versioning Strategy
|
|
1838
|
+
|
|
1839
|
+
```bash
|
|
1840
|
+
# Semantic versioning
|
|
1841
|
+
v1.0.0 - Initial release
|
|
1842
|
+
v1.1.0 - New features (backward compatible)
|
|
1843
|
+
v1.1.1 - Bug fixes
|
|
1844
|
+
v2.0.0 - Breaking changes
|
|
1845
|
+
|
|
1846
|
+
```
|
|
1847
|
+
|
|
1848
|
+
---
|
|
1849
|
+
|
|
1850
|
+
## Related Documentation
|
|
1851
|
+
|
|
1852
|
+
- **[Architecture Overview](../architecture/overview)** - Understand platform architecture
|
|
1853
|
+
- **[Gateway Architecture](../architecture/gateway-architecture)** - Learn about the API gateway
|
|
1854
|
+
- **[Shell Architecture](../architecture/shell)** - Deep dive into the platform shell
|
|
1855
|
+
- **[UI Extension Points](../architecture/ui-extension-points)** - Master extension point patterns
|
|
1856
|
+
- **[Authorization System](../architecture/authorization-system)** - Understand operations and RBAC
|
|
1857
|
+
- **[Adding Documentation Sites](./adding-documentation-sites)** - Document your UI module
|
|
1858
|
+
- **[Using Feature Flags](./using-feature-flags)** - Implement feature toggles
|
|
1859
|
+
|
|
1860
|
+
---
|