@aaqu/fromcubes-portal-react 0.1.0-alpha.5 → 0.1.0-alpha.7
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/README.md +30 -11
- package/nodes/portal-react.js +12 -3
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,23 +5,26 @@ React portal node for Node-RED. Server-side JSX transpilation via esbuild. Tailw
|
|
|
5
5
|
## How it works
|
|
6
6
|
|
|
7
7
|
```
|
|
8
|
-
┌─ Deploy time (Node-RED server)
|
|
8
|
+
┌─ Deploy time (Node-RED server) ──────────────────────────┐
|
|
9
9
|
│ │
|
|
10
|
-
│ JSX (editor) ──► esbuild transpile ──► cached JS
|
|
10
|
+
│ JSX (editor) ──► esbuild transpile ──► cached JS │
|
|
11
11
|
│ (hash-keyed cache) │
|
|
12
12
|
│ │
|
|
13
|
+
│ Tailwind classes ──► server-side compile ──► CSS file │
|
|
14
|
+
│ (hash-keyed cache) │
|
|
15
|
+
│ │
|
|
13
16
|
│ Unchanged code on redeploy = cache hit, 0ms │
|
|
14
17
|
│ Changed code = retranspile, ~5ms │
|
|
15
18
|
└──────────────────────────────────────────────────────────┘
|
|
16
19
|
|
|
17
|
-
┌─ Runtime (browser)
|
|
20
|
+
┌─ Runtime (browser) ──────────────────────────────────────┐
|
|
18
21
|
│ │
|
|
19
22
|
│ GET /endpoint ──► HTML + pre-compiled JS │
|
|
20
23
|
│ react-19.production.min.js │
|
|
21
|
-
│ Tailwind CSS (
|
|
24
|
+
│ Tailwind CSS (server-compiled) │
|
|
22
25
|
│ NO Babel, NO Sucrase, NO compiler │
|
|
23
26
|
│ │
|
|
24
|
-
│ WebSocket /endpoint/_ws ◄──► Node-RED msg I/O
|
|
27
|
+
│ WebSocket /endpoint/_ws ◄──► Node-RED msg I/O │
|
|
25
28
|
└──────────────────────────────────────────────────────────┘
|
|
26
29
|
```
|
|
27
30
|
|
|
@@ -49,9 +52,8 @@ Dependencies install automatically. No build step needed.
|
|
|
49
52
|
| Field | Purpose |
|
|
50
53
|
|---|---|
|
|
51
54
|
| Endpoint | HTTP path, e.g. `/dashboard` |
|
|
52
|
-
| Title | Browser tab title |
|
|
53
|
-
|
|
|
54
|
-
| Output Schema | JSON — documents emitted `msg.payload` shape |
|
|
55
|
+
| Page Title | Browser tab title |
|
|
56
|
+
| Portal Auth | Enable portal user header extraction |
|
|
55
57
|
| Head HTML | Extra `<head>` tags (CDN, fonts, CSS) |
|
|
56
58
|
| Code Editor | Monaco with JSX — must define `<App />` |
|
|
57
59
|
|
|
@@ -72,13 +74,30 @@ Components are auto-injected into every portal-react page at transpile time.
|
|
|
72
74
|
|
|
73
75
|
```jsx
|
|
74
76
|
function App() {
|
|
75
|
-
const { data, send } = useNodeRed();
|
|
77
|
+
const { data, send, user } = useNodeRed();
|
|
76
78
|
// data = last msg.payload from input wire (reactive)
|
|
77
79
|
// send(payload, topic?) = emit msg on output wire
|
|
80
|
+
// user = portal user object (when Portal Auth enabled), or null
|
|
78
81
|
return <div className="p-4 text-lg">{JSON.stringify(data)}</div>;
|
|
79
82
|
}
|
|
80
83
|
```
|
|
81
84
|
|
|
85
|
+
## Portal Authentication
|
|
86
|
+
|
|
87
|
+
When **Portal Auth** is checked, the node extracts user identity from incoming request headers:
|
|
88
|
+
|
|
89
|
+
| Header | Field |
|
|
90
|
+
|---|---|
|
|
91
|
+
| `x-portal-user-id` | `userId` |
|
|
92
|
+
| `x-portal-user-name` | `userName` |
|
|
93
|
+
| `x-portal-user-username` | `username` |
|
|
94
|
+
| `x-portal-user-email` | `email` |
|
|
95
|
+
| `x-portal-user-role` | `role` |
|
|
96
|
+
| `x-portal-user-groups` | `groups` (JSON array) |
|
|
97
|
+
|
|
98
|
+
- In the browser, `useNodeRed().user` returns the extracted user object (or `null` if auth is disabled or no headers present).
|
|
99
|
+
- On outgoing messages, user info is attached as `msg._client` so downstream nodes can identify the sender.
|
|
100
|
+
|
|
82
101
|
## Deploy lifecycle
|
|
83
102
|
|
|
84
103
|
What happens on each deploy:
|
|
@@ -92,7 +111,7 @@ What happens on each deploy:
|
|
|
92
111
|
- Cache hit → reuse (0ms)
|
|
93
112
|
- Cache miss → esbuild transpile (~5ms)
|
|
94
113
|
6. New HTTP route and WS handler registered
|
|
95
|
-
7. Reconnecting clients
|
|
114
|
+
7. Reconnecting clients soft-reconnect (no page reload) and receive current `lastPayload`
|
|
96
115
|
|
|
97
116
|
Rapid deploys (user clicking deploy repeatedly) are safe:
|
|
98
117
|
- `isClosing` flag prevents accepting new WS connections during teardown
|
|
@@ -110,7 +129,7 @@ Transpile errors:
|
|
|
110
129
|
|---|---|
|
|
111
130
|
| react-19.production.min.js | ~45 KB |
|
|
112
131
|
| Your transpiled JS | ~1-5 KB |
|
|
113
|
-
| Tailwind CSS (
|
|
132
|
+
| Tailwind CSS (server-compiled) | cached per content hash |
|
|
114
133
|
| WebSocket bridge | <1 KB |
|
|
115
134
|
|
|
116
135
|
No Babel, no Sucrase client, no Vue, no Vuetify, no Socket.IO.
|
package/nodes/portal-react.js
CHANGED
|
@@ -291,8 +291,11 @@ module.exports = function (RED) {
|
|
|
291
291
|
})
|
|
292
292
|
: Promise.resolve("");
|
|
293
293
|
|
|
294
|
+
const contentHash = compiled.js ? hash(compiled.js) : "";
|
|
295
|
+
|
|
294
296
|
pageState[endpoint] = {
|
|
295
297
|
compiled,
|
|
298
|
+
contentHash,
|
|
296
299
|
cssHashReady,
|
|
297
300
|
pageTitle,
|
|
298
301
|
wsPath,
|
|
@@ -389,6 +392,10 @@ module.exports = function (RED) {
|
|
|
389
392
|
wsSend(ws, { type: "data", payload: lastPayload });
|
|
390
393
|
}
|
|
391
394
|
|
|
395
|
+
// Send content version for deploy-reload detection
|
|
396
|
+
const contentHash = pageState[endpoint]?.contentHash || "";
|
|
397
|
+
wsSend(ws, { type: "version", hash: contentHash });
|
|
398
|
+
|
|
392
399
|
ws.on("message", (raw) => {
|
|
393
400
|
try {
|
|
394
401
|
const msg = JSON.parse(raw.toString());
|
|
@@ -590,7 +597,7 @@ body{font-family:system-ui,-apple-system,sans-serif;background:#0a0a0a;color:#e0
|
|
|
590
597
|
<div id="__cs" class="err">disconnected</div>
|
|
591
598
|
<script>
|
|
592
599
|
window.__NR={
|
|
593
|
-
_ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,_user:${user ? escScript(JSON.stringify(user)) : 'null'},
|
|
600
|
+
_ws:null,_listeners:new Set(),_lastData:null,_retries:0,_wasConnected:false,_version:null,_user:${user ? escScript(JSON.stringify(user)) : 'null'},
|
|
594
601
|
connect(){
|
|
595
602
|
const p=location.protocol==='https:'?'wss:':'ws:';
|
|
596
603
|
const ws=new WebSocket(p+'//'+location.host+'${wsPath}');
|
|
@@ -601,8 +608,10 @@ window.__NR={
|
|
|
601
608
|
s.textContent='connected';s.className='ok';this._retries=0;this._wasConnected=true;
|
|
602
609
|
};
|
|
603
610
|
ws.onmessage=(e)=>{
|
|
604
|
-
try{const m=JSON.parse(e.data);
|
|
605
|
-
|
|
611
|
+
try{const m=JSON.parse(e.data);
|
|
612
|
+
if(m.type==='version'){if(this._version&&this._version!==m.hash){location.reload();return;}this._version=m.hash;}
|
|
613
|
+
if(m.type==='data'){this._lastData=m.payload;this._listeners.forEach(fn=>fn(m.payload));}
|
|
614
|
+
}catch(err){console.error('WS parse',err);}
|
|
606
615
|
};
|
|
607
616
|
ws.onclose=(e)=>{
|
|
608
617
|
s.textContent='disconnected';s.className='err';
|