@aaqu/fromcubes-react 0.0.0-stage → 0.2.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/LICENSE +190 -0
- package/README.md +435 -2
- package/THIRD-PARTY-NOTICES.md +164 -0
- package/examples/D3 Poland Map.json +90 -0
- package/examples/Live Chart.json +101 -0
- package/examples/PixiJS Sprites.json +114 -0
- package/examples/Sensor Portal.json +93 -0
- package/examples/Shared Components.json +68 -0
- package/examples/Three.js Scene.json +118 -0
- package/examples/Utility Hooks.json +94 -0
- package/examples/WebGPU Shader.json +110 -0
- package/nodes/editor/ark-pixel.LICENSE.txt +96 -0
- package/nodes/editor/ark-pixel.woff2 +0 -0
- package/nodes/editor/file-tree.js +403 -0
- package/nodes/editor/format.js +182 -0
- package/nodes/editor/icons.svg +46 -0
- package/nodes/editor/mark.svg +5 -0
- package/nodes/editor/provider-settings.js +192 -0
- package/nodes/editor/workspace.css +660 -0
- package/nodes/editor/workspace.js +1565 -0
- package/nodes/icons/fromcubes-component.svg +27 -0
- package/nodes/icons/fromcubes-react.svg +25 -0
- package/nodes/icons/fromcubes-utility.svg +21 -0
- package/nodes/lib/admin-api.js +238 -0
- package/nodes/lib/admin-security.js +161 -0
- package/nodes/lib/api.js +29 -0
- package/nodes/lib/helpers.js +990 -0
- package/nodes/lib/hooks.js +145 -0
- package/nodes/lib/page-builder.js +499 -0
- package/nodes/lib/portal-page-route.js +131 -0
- package/nodes/lib/providers.js +581 -0
- package/nodes/lib/registry-nodes.js +331 -0
- package/nodes/lib/router.js +103 -0
- package/nodes/lib/ws-heartbeat.js +58 -0
- package/nodes/portal-react.html +2539 -0
- package/nodes/portal-react.js +1652 -0
- package/package.json +73 -4
package/LICENSE
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Apache License
|
|
2
|
+
Version 2.0, January 2004
|
|
3
|
+
http://www.apache.org/licenses/
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
|
6
|
+
|
|
7
|
+
1. Definitions.
|
|
8
|
+
|
|
9
|
+
"License" shall mean the terms and conditions for use, reproduction,
|
|
10
|
+
and distribution as defined by Sections 1 through 9 of this document.
|
|
11
|
+
|
|
12
|
+
"Licensor" shall mean the copyright owner or entity authorized by
|
|
13
|
+
the copyright owner that is granting the License.
|
|
14
|
+
|
|
15
|
+
"Legal Entity" shall mean the union of the acting entity and all
|
|
16
|
+
other entities that control, are controlled by, or are under common
|
|
17
|
+
control with that entity. For the purposes of this definition,
|
|
18
|
+
"control" means (i) the power, direct or indirect, to cause the
|
|
19
|
+
direction or management of such entity, whether by contract or
|
|
20
|
+
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
|
21
|
+
outstanding shares, or (iii) beneficial ownership of such entity.
|
|
22
|
+
|
|
23
|
+
"You" (or "Your") shall mean an individual or Legal Entity
|
|
24
|
+
exercising permissions granted by this License.
|
|
25
|
+
|
|
26
|
+
"Source" form shall mean the preferred form for making modifications,
|
|
27
|
+
including but not limited to software source code, documentation
|
|
28
|
+
source, and configuration files.
|
|
29
|
+
|
|
30
|
+
"Object" form shall mean any form resulting from mechanical
|
|
31
|
+
transformation or translation of a Source form, including but
|
|
32
|
+
not limited to compiled object code, generated documentation,
|
|
33
|
+
and conversions to other media types.
|
|
34
|
+
|
|
35
|
+
"Work" shall mean the work of authorship, whether in Source or
|
|
36
|
+
Object form, made available under the License, as indicated by a
|
|
37
|
+
copyright notice that is included in or attached to the work
|
|
38
|
+
(an example is provided in the Appendix below).
|
|
39
|
+
|
|
40
|
+
"Derivative Works" shall mean any work, whether in Source or Object
|
|
41
|
+
form, that is based on (or derived from) the Work and for which the
|
|
42
|
+
editorial revisions, annotations, elaborations, or other modifications
|
|
43
|
+
represent, as a whole, an original work of authorship. For the purposes
|
|
44
|
+
of this License, Derivative Works shall not include works that remain
|
|
45
|
+
separable from, or merely link (or bind by name) to the interfaces of,
|
|
46
|
+
the Work and Derivative Works thereof.
|
|
47
|
+
|
|
48
|
+
"Contribution" shall mean any work of authorship, including
|
|
49
|
+
the original version of the Work and any modifications or additions
|
|
50
|
+
to that Work or Derivative Works thereof, that is intentionally
|
|
51
|
+
submitted to the Licensor for inclusion in the Work by the copyright owner
|
|
52
|
+
or by an individual or Legal Entity authorized to submit on behalf of
|
|
53
|
+
the copyright owner. For the purposes of this definition, "submitted"
|
|
54
|
+
means any form of electronic, verbal, or written communication sent
|
|
55
|
+
to the Licensor or its representatives, including but not limited to
|
|
56
|
+
communication on electronic mailing lists, source code control systems,
|
|
57
|
+
and issue tracking systems that are managed by, or on behalf of, the
|
|
58
|
+
Licensor for the purpose of discussing and improving the Work, but
|
|
59
|
+
excluding communication that is conspicuously marked or otherwise
|
|
60
|
+
designated in writing by the copyright owner as "Not a Contribution."
|
|
61
|
+
|
|
62
|
+
"Contributor" shall mean Licensor and any individual or Legal Entity
|
|
63
|
+
on behalf of whom a Contribution has been received by the Licensor and
|
|
64
|
+
subsequently incorporated within the Work.
|
|
65
|
+
|
|
66
|
+
2. Grant of Copyright License. Subject to the terms and conditions of
|
|
67
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
68
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
69
|
+
copyright license to reproduce, prepare Derivative Works of,
|
|
70
|
+
publicly display, publicly perform, sublicense, and distribute the
|
|
71
|
+
Work and such Derivative Works in Source or Object form.
|
|
72
|
+
|
|
73
|
+
3. Grant of Patent License. Subject to the terms and conditions of
|
|
74
|
+
this License, each Contributor hereby grants to You a perpetual,
|
|
75
|
+
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
|
76
|
+
(except as stated in this section) patent license to make, have made,
|
|
77
|
+
use, offer to sell, sell, import, and otherwise transfer the Work,
|
|
78
|
+
where such license applies only to those patent claims licensable
|
|
79
|
+
by such Contributor that are necessarily infringed by their
|
|
80
|
+
Contribution(s) alone or by combination of their Contribution(s)
|
|
81
|
+
with the Work to which such Contribution(s) was submitted. If You
|
|
82
|
+
institute patent litigation against any entity (including a
|
|
83
|
+
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
|
84
|
+
or a Contribution incorporated within the Work constitutes direct
|
|
85
|
+
or contributory patent infringement, then any patent licenses
|
|
86
|
+
granted to You under this License for that Work shall terminate
|
|
87
|
+
as of the date such litigation is filed.
|
|
88
|
+
|
|
89
|
+
4. Redistribution. You may reproduce and distribute copies of the
|
|
90
|
+
Work or Derivative Works thereof in any medium, with or without
|
|
91
|
+
modifications, and in Source or Object form, provided that You
|
|
92
|
+
meet the following conditions:
|
|
93
|
+
|
|
94
|
+
(a) You must give any other recipients of the Work or
|
|
95
|
+
Derivative Works a copy of this License; and
|
|
96
|
+
|
|
97
|
+
(b) You must cause any modified files to carry prominent notices
|
|
98
|
+
stating that You changed the files; and
|
|
99
|
+
|
|
100
|
+
(c) You must retain, in the Source form of any Derivative Works
|
|
101
|
+
that You distribute, all copyright, patent, trademark, and
|
|
102
|
+
attribution notices from the Source form of the Work,
|
|
103
|
+
excluding those notices that do not pertain to any part of
|
|
104
|
+
the Derivative Works; and
|
|
105
|
+
|
|
106
|
+
(d) If the Work includes a "NOTICE" text file as part of its
|
|
107
|
+
distribution, then any Derivative Works that You distribute must
|
|
108
|
+
include a readable copy of the attribution notices contained
|
|
109
|
+
within such NOTICE file, excluding any notices that do not
|
|
110
|
+
pertain to any part of the Derivative Works, in at least one
|
|
111
|
+
of the following places: within a NOTICE text file distributed
|
|
112
|
+
as part of the Derivative Works; within the Source form or
|
|
113
|
+
documentation, if provided along with the Derivative Works; or,
|
|
114
|
+
within a display generated by the Derivative Works, if and
|
|
115
|
+
wherever such third-party notices normally appear. The contents
|
|
116
|
+
of the NOTICE file are for informational purposes only and
|
|
117
|
+
do not modify the License. You may add Your own attribution
|
|
118
|
+
notices within Derivative Works that You distribute, alongside
|
|
119
|
+
or as an addendum to the NOTICE text from the Work, provided
|
|
120
|
+
that such additional attribution notices cannot be construed
|
|
121
|
+
as modifying the License.
|
|
122
|
+
|
|
123
|
+
You may add Your own copyright statement to Your modifications and
|
|
124
|
+
may provide additional or different license terms and conditions
|
|
125
|
+
for use, reproduction, or distribution of Your modifications, or
|
|
126
|
+
for any such Derivative Works as a whole, provided Your use,
|
|
127
|
+
reproduction, and distribution of the Work otherwise complies with
|
|
128
|
+
the conditions stated in this License.
|
|
129
|
+
|
|
130
|
+
5. Submission of Contributions. Unless You explicitly state otherwise,
|
|
131
|
+
any Contribution intentionally submitted for inclusion in the Work
|
|
132
|
+
by You to the Licensor shall be under the terms and conditions of
|
|
133
|
+
this License, without any additional terms or conditions.
|
|
134
|
+
Notwithstanding the above, nothing herein shall supersede or modify
|
|
135
|
+
the terms of any separate license agreement you may have executed
|
|
136
|
+
with Licensor regarding such Contributions.
|
|
137
|
+
|
|
138
|
+
6. Trademarks. This License does not grant permission to use the trade
|
|
139
|
+
names, trademarks, service marks, or product names of the Licensor,
|
|
140
|
+
except as required for reasonable and customary use in describing the
|
|
141
|
+
origin of the Work and reproducing the content of the NOTICE file.
|
|
142
|
+
|
|
143
|
+
7. Disclaimer of Warranty. Unless required by applicable law or
|
|
144
|
+
agreed to in writing, Licensor provides the Work (and each
|
|
145
|
+
Contributor provides its Contributions) on an "AS IS" BASIS,
|
|
146
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
|
147
|
+
implied, including, without limitation, any warranties or conditions
|
|
148
|
+
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
|
149
|
+
PARTICULAR PURPOSE. You are solely responsible for determining the
|
|
150
|
+
appropriateness of using or redistributing the Work and assume any
|
|
151
|
+
risks associated with Your exercise of permissions under this License.
|
|
152
|
+
|
|
153
|
+
8. Limitation of Liability. In no event and under no legal theory,
|
|
154
|
+
whether in tort (including negligence), contract, or otherwise,
|
|
155
|
+
unless required by applicable law (such as deliberate and grossly
|
|
156
|
+
negligent acts) or agreed to in writing, shall any Contributor be
|
|
157
|
+
liable to You for damages, including any direct, indirect, special,
|
|
158
|
+
incidental, or consequential damages of any character arising as a
|
|
159
|
+
result of this License or out of the use or inability to use the
|
|
160
|
+
Work (including but not limited to damages for loss of goodwill,
|
|
161
|
+
work stoppage, computer failure or malfunction, or any and all
|
|
162
|
+
other commercial damages or losses), even if such Contributor
|
|
163
|
+
has been advised of the possibility of such damages.
|
|
164
|
+
|
|
165
|
+
9. Accepting Warranty or Additional Liability. While redistributing
|
|
166
|
+
the Work or Derivative Works thereof, You may choose to offer,
|
|
167
|
+
and charge a fee for, acceptance of support, warranty, indemnity,
|
|
168
|
+
or other liability obligations and/or rights consistent with this
|
|
169
|
+
License. However, in accepting such obligations, You may act only
|
|
170
|
+
on Your own behalf and on Your sole responsibility, not on behalf
|
|
171
|
+
of any other Contributor, and only if You agree to indemnify,
|
|
172
|
+
defend, and hold each Contributor harmless for any liability
|
|
173
|
+
incurred by, or claims asserted against, such Contributor by reason
|
|
174
|
+
of your accepting any such warranty or additional liability.
|
|
175
|
+
|
|
176
|
+
END OF TERMS AND CONDITIONS
|
|
177
|
+
|
|
178
|
+
Copyright 2026 Mazur Albert Fromcubes
|
|
179
|
+
|
|
180
|
+
Licensed under the Apache License, Version 2.0 (the "License");
|
|
181
|
+
you may not use this file except in compliance with the License.
|
|
182
|
+
You may obtain a copy of the License at
|
|
183
|
+
|
|
184
|
+
http://www.apache.org/licenses/LICENSE-2.0
|
|
185
|
+
|
|
186
|
+
Unless required by applicable law or agreed to in writing, software
|
|
187
|
+
distributed under the License is distributed on an "AS IS" BASIS,
|
|
188
|
+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
189
|
+
See the License for the specific language governing permissions and
|
|
190
|
+
limitations under the License.
|
package/README.md
CHANGED
|
@@ -1,3 +1,436 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @aaqu/fromcubes-react
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
[](https://www.npmjs.com/package/@aaqu/fromcubes-react)
|
|
4
|
+
[](https://www.npmjs.com/package/@aaqu/fromcubes-react)
|
|
5
|
+
[](https://www.npmjs.com/package/@aaqu/fromcubes-react)
|
|
6
|
+
[](https://nodered.org)
|
|
7
|
+
[](./LICENSE)
|
|
8
|
+
[](https://discord.gg/KYzsfKkYxX)
|
|
9
|
+
[](https://github.com/sponsors/Aaqu)
|
|
10
|
+
|
|
11
|
+
**📖 Documentation — step-by-step guide with screenshots:** [aaqu.github.io/fromcubes-modules](https://aaqu.github.io/fromcubes-modules/) — install → import → edit JSX → live dashboard, on one example.
|
|
12
|
+
|
|
13
|
+
[](https://aaqu.github.io/fromcubes-modules/)
|
|
14
|
+
|
|
15
|
+
A Node-RED node that turns any `/fromcubes/<sub-path>` URL into a React page. Write JSX in the editor, deploy, open the URL — your component talks to the flow over WebSocket. No build step, no browser compiler. All portal pages are served under the hardcoded `/fromcubes/` prefix so every node cleanly coexists under one URL tree.
|
|
16
|
+
|
|
17
|
+
For internals, plugin authoring, and the deploy pipeline see [README-DEV.md](./README-DEV.md).
|
|
18
|
+
|
|
19
|
+
**💬 Questions, feedback, showcase?** Join the [fromcubes Discord](https://discord.gg/KYzsfKkYxX).
|
|
20
|
+
|
|
21
|
+
**❤️ Support the project:** [](https://github.com/sponsors/Aaqu)
|
|
22
|
+
|
|
23
|
+
## Prerequisites
|
|
24
|
+
|
|
25
|
+
- **Node-RED ≥ 4.0** — declared in `package.json`'s `node-red.version` and
|
|
26
|
+
enforced by the editor's Palette Manager.
|
|
27
|
+
- **Node.js ≥ 18.5** — Node-RED 4.x's effective minimum.
|
|
28
|
+
- **npm** — only required when you use the `libs` config field to install
|
|
29
|
+
user packages at deploy time. The portal itself does not need `npm`
|
|
30
|
+
available at runtime.
|
|
31
|
+
- A WebSocket-capable reverse proxy or direct connection — the portal
|
|
32
|
+
upgrades `/fromcubes/<sub-path>/_ws`. If you front Node-RED with nginx /
|
|
33
|
+
Traefik / Caddy, enable `Upgrade: websocket` headers on that path.
|
|
34
|
+
|
|
35
|
+
## Install
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
cd ~/.node-red
|
|
39
|
+
npm install @aaqu/fromcubes-react
|
|
40
|
+
# restart Node-RED
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
That's it — this is the whole core: the nodes, deploy-time bundling, the WebSocket bridge and the React Editor. No build step. Any npm packages you list in the node config (e.g. `d3, three`) are installed automatically on deploy.
|
|
44
|
+
|
|
45
|
+
### Add-ons
|
|
46
|
+
|
|
47
|
+
Everything else is an add-on you install only if you need it:
|
|
48
|
+
|
|
49
|
+
```bash
|
|
50
|
+
cd ~/.node-red
|
|
51
|
+
npm install @aaqu/fromcubes-tailwind # and/or -assets, -auth-headers
|
|
52
|
+
# restart Node-RED
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
| Package | What it adds |
|
|
56
|
+
|---|---|
|
|
57
|
+
| [`@aaqu/fromcubes-tailwind`](https://www.npmjs.com/package/@aaqu/fromcubes-tailwind) | Tailwind CSS 4 for portals, plus class completion in the editor |
|
|
58
|
+
| [`@aaqu/fromcubes-auth-headers`](https://www.npmjs.com/package/@aaqu/fromcubes-auth-headers) | Users from `x-portal-user-*` headers set by a reverse proxy (Portal Auth) |
|
|
59
|
+
| [`@aaqu/fromcubes-assets`](https://www.npmjs.com/package/@aaqu/fromcubes-assets) | Static files for portals, managed from a sidebar tab |
|
|
60
|
+
|
|
61
|
+
Without an add-on its feature is simply off, and the editor says so once in
|
|
62
|
+
the log: no Tailwind means portals get no generated stylesheet (style them
|
|
63
|
+
with a `<link>`/`<style>` in **Head HTML**, or inline), no auth-headers means
|
|
64
|
+
Portal Auth sees every session as anonymous, no assets means no sidebar and no
|
|
65
|
+
`/fromcubes/public/`. Install an add-on later and it switches on by itself —
|
|
66
|
+
**Menu → Settings → Fromcubes** shows what is in use.
|
|
67
|
+
|
|
68
|
+
The shipped examples are styled with Tailwind; install
|
|
69
|
+
`@aaqu/fromcubes-tailwind` to see them as intended.
|
|
70
|
+
|
|
71
|
+
Each add-on is a Node-RED plugin built on the same API anyone can use — see
|
|
72
|
+
[Plugins](#plugins-identity-providers-and-css-engines).
|
|
73
|
+
|
|
74
|
+
**Coming from `@aaqu/fromcubes-portal-react`?** It contained all of the above.
|
|
75
|
+
Uninstall it first (both register the same nodes), then install the core
|
|
76
|
+
**and the add-ons you used** — for the same features as before:
|
|
77
|
+
|
|
78
|
+
```bash
|
|
79
|
+
npm uninstall @aaqu/fromcubes-portal-react
|
|
80
|
+
npm install @aaqu/fromcubes-react @aaqu/fromcubes-tailwind @aaqu/fromcubes-assets @aaqu/fromcubes-auth-headers
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
Your flows keep working unchanged.
|
|
84
|
+
|
|
85
|
+
## Your first portal
|
|
86
|
+
|
|
87
|
+
1. Drop a **portal-react** node onto a flow.
|
|
88
|
+
2. Set **Sub-path** to e.g. `hello` (the node will serve at `/fromcubes/hello`; the `/fromcubes/` prefix is fixed).
|
|
89
|
+
3. Open the code editor and paste:
|
|
90
|
+
```jsx
|
|
91
|
+
function App() {
|
|
92
|
+
const { data, send } = useNodeRed();
|
|
93
|
+
return (
|
|
94
|
+
<div className="p-4">
|
|
95
|
+
<p>From flow: {JSON.stringify(data)}</p>
|
|
96
|
+
<button
|
|
97
|
+
className="px-3 py-1 bg-blue-500 text-white rounded"
|
|
98
|
+
onClick={() => send({ clicked: Date.now() })}
|
|
99
|
+
>
|
|
100
|
+
click me
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
```
|
|
106
|
+
4. Deploy. Open `http://localhost:1880/fromcubes/hello`.
|
|
107
|
+
5. Wire an **inject** node into the portal-react input → see `data` update live. Wire its output → see button clicks arrive in your flow.
|
|
108
|
+
|
|
109
|
+
## The `useNodeRed()` hook
|
|
110
|
+
|
|
111
|
+
Everything your component needs from the flow lives in one hook:
|
|
112
|
+
|
|
113
|
+
```jsx
|
|
114
|
+
const {
|
|
115
|
+
data, // last broadcast msg.payload from input wire (reactive)
|
|
116
|
+
send, // send(payload, topic?) — emit msg on output wire
|
|
117
|
+
user, // portal user object (when Portal Auth is enabled), or null
|
|
118
|
+
portalClient, // unique session/tab ID assigned by server on connect
|
|
119
|
+
authRequired, // true when this session is anonymous on an authenticated-only portal
|
|
120
|
+
} = useNodeRed();
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
### Initial data on connect
|
|
124
|
+
|
|
125
|
+
The server does **not** cache payloads — a freshly-loaded page has `data === null` until the flow pushes the next message. For periodic streams that's at most one interval of blankness. For event-driven or per-user data, request the initial state from the component:
|
|
126
|
+
|
|
127
|
+
```jsx
|
|
128
|
+
const { data, send } = useNodeRed();
|
|
129
|
+
useEffect(() => { send({}, "init"); }, []);
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
In the flow, catch `topic === "init"`, look up the state (per-user via `msg._client.userId` if needed) and reply — `msg._client` is already set on the inbound message, so the response returns as a unicast to exactly that tab.
|
|
133
|
+
|
|
134
|
+
## Node configuration
|
|
135
|
+
|
|
136
|
+
| Field | Purpose |
|
|
137
|
+
|---|---|
|
|
138
|
+
| Sub-path | Part after `/fromcubes/`, e.g. `page1` → served at `/fromcubes/page1`. Required. Nesting allowed (`team/alpha`). Reserved: `public`, `_ws`. |
|
|
139
|
+
| Page Title | Browser tab title |
|
|
140
|
+
| npm Packages | Comma-separated, e.g. `d3, three, @react-three/fiber` |
|
|
141
|
+
| Portal Auth | Enable portal user header extraction (see Multi-user) |
|
|
142
|
+
| Authenticated-only delivery | Untargeted msgs (no `msg._client`) go only to sessions with portal identity headers; anonymous sessions get an `auth_required` frame on connect (see Multi-user). Off by default. |
|
|
143
|
+
| Show WS status | Small "fromcubes • connected/disconnected" badge in the page's bottom-right corner (off by default) |
|
|
144
|
+
| Head HTML | Extra trusted-author `<head>` tags (CDN, fonts, CSS, scripts). Runs in the public portal page. |
|
|
145
|
+
| Code Editor | Monaco with JSX — must define `<App />` |
|
|
146
|
+
|
|
147
|
+
There is also a config node, **fc-portal-component**, that lets you define reusable React components once and reference them by name from any portal-react node. Referenced components (and their transitive dependencies) are injected at transpile time, so unused ones add nothing to the bundle.
|
|
148
|
+
|
|
149
|
+
For shared **non-component** code (helpers, custom hooks, constants), use **fc-portal-utility** — a sibling config node. Unlike component nodes (which export exactly one symbol via an IIFE wrapper), a utility node is injected raw at top level so it can declare any number of `function` / `const` / `let` / `class` symbols. Selective inclusion: a utility node lands in a portal's bundle only when the portal's JSX or any of its referenced library components mentions at least one of the symbols declared in that utility.
|
|
150
|
+
|
|
151
|
+
```jsx
|
|
152
|
+
// fc-portal-utility, Module name = mathHelpers
|
|
153
|
+
const PI2 = Math.PI * 2;
|
|
154
|
+
function clamp(n, min, max) { return Math.max(min, Math.min(max, n)); }
|
|
155
|
+
function useDebounce(value, ms = 300) {
|
|
156
|
+
const [v, setV] = React.useState(value);
|
|
157
|
+
React.useEffect(() => {
|
|
158
|
+
const t = setTimeout(() => setV(value), ms);
|
|
159
|
+
return () => clearTimeout(t);
|
|
160
|
+
}, [value, ms]);
|
|
161
|
+
return v;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// portal-react, JSX tab
|
|
165
|
+
function App() {
|
|
166
|
+
const { data } = useNodeRed();
|
|
167
|
+
const slow = useDebounce(data?.value);
|
|
168
|
+
return <div>{clamp(slow ?? 0, 0, 100)}</div>;
|
|
169
|
+
}
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
## Editor features
|
|
173
|
+
|
|
174
|
+
- **Monaco** with full JSX support and `useNodeRed()` type declarations
|
|
175
|
+
- **Tailwind CSS autocompletion** inside `className="..."` (~19k utility classes). Classes must be complete literals in a string — dynamic `bg-${tone}-500` generates no CSS (use a map of full classes). Arbitrary values incl. `w-[calc(100%-2rem)]`, `grid-cols-[repeat(2,1fr)]`, `mt-0!` and `@md:flex` are supported.
|
|
176
|
+
- **JSX tag completion** — type tag name, Tab to expand
|
|
177
|
+
- **Self-close collapse** — type `/` inside empty `<tag></tag>` to convert to `<tag />`
|
|
178
|
+
- **Ctrl+D / Cmd+D** — duplicate the current line or selection (in every Monaco editor of the package)
|
|
179
|
+
- **Ctrl+/ / Cmd+/** — JSX-aware comment toggle. Style follows the first selected line (VS Code behavior): selecting a whole `return ( … )` block gives plain `//` on every line, while lines between live JSX tags are wrapped in `{/* … */}` (a plain `//` there is literal text — JSX renders it on the page)
|
|
180
|
+
- **Component completion** — registry components + any PascalCase word
|
|
181
|
+
- **Utility-symbol completion** — top-level identifiers from any `fc-portal-utility` node, suggested in JS context
|
|
182
|
+
- **Components / Utilities dialogs** — buttons in the JSX tab; Components inserts `<Tag></Tag>`, Utilities expands to the symbols declared in each node and inserts the bare identifier on click
|
|
183
|
+
- **Portal Assets sidebar** — file manager for static assets (GLB, textures, fonts…)
|
|
184
|
+
|
|
185
|
+
## Multi-user / Multi-tenancy
|
|
186
|
+
|
|
187
|
+
Portal-react has four routing modes — broadcast, auth-cast (every authenticated session), user-cast (every tab of one user), and unicast (one specific tab). Everything works without authentication too — user-cast and auth-cast just degrade gracefully when there is no user.
|
|
188
|
+
|
|
189
|
+
### Identity
|
|
190
|
+
|
|
191
|
+
| Identifier | Scope | Persistence | Source |
|
|
192
|
+
|---|---|---|---|
|
|
193
|
+
| `portalClient` | Single WS session (one tab) | Lost on reconnect — server assigns a new UUID | Generated server-side on connect |
|
|
194
|
+
| `userId` / `login` | All sessions of a user | Survives reconnect and new tabs | `x-portal-user-*` headers (when **Portal Auth** is enabled) |
|
|
195
|
+
|
|
196
|
+
**Portal Auth header contract** — injected by an upstream reverse proxy (the [fromcubes-portal](https://github.com/Aaqu/fromcubes-portal) platform sets them from Traefik forward-auth). Every value is percent-encoded UTF-8 (`%C5%81ukasz` → `Łukasz`); the same field names appear in `useNodeRed().user` and `msg._client`:
|
|
197
|
+
|
|
198
|
+
| Header | Field |
|
|
199
|
+
|---|---|
|
|
200
|
+
| `x-portal-user-id` | `userId` — required; without it the session is anonymous |
|
|
201
|
+
| `x-portal-user-login` | `login` |
|
|
202
|
+
| `x-portal-user-display-name` | `displayName` |
|
|
203
|
+
| `x-portal-user-email` | `email` |
|
|
204
|
+
| `x-portal-user-role` | `role` (single value) |
|
|
205
|
+
| `x-portal-user-groups` | `groups` — comma-separated, each element encoded on its own (`a%2Cb` is one group `a,b`); always an array, `[]` when absent |
|
|
206
|
+
|
|
207
|
+
A value with a malformed escape is skipped (in `groups`, only that element). Without `x-portal-user-id` the session is anonymous and `user` is `null`, whatever other headers arrive. The pre-contract `x-portal-user-username` / `x-portal-user-name` headers are ignored.
|
|
208
|
+
|
|
209
|
+
**The proxy must strip all six headers from the client's request** before injecting its own. The package trusts them as they arrive; one header let through is enough to impersonate any user.
|
|
210
|
+
|
|
211
|
+
If Portal Auth is disabled or headers are absent, `user` is `null` and user-scoped features are silently skipped — broadcast and per-session features still work.
|
|
212
|
+
|
|
213
|
+
### Routing modes
|
|
214
|
+
|
|
215
|
+
Every inbound `msg` arrives in your flow with `msg._client` already filled in by the server. The flow then decides where the response should go by setting (or clearing) `msg._client` on the outgoing `msg`:
|
|
216
|
+
|
|
217
|
+
```javascript
|
|
218
|
+
// BROADCAST — everyone connected to this endpoint
|
|
219
|
+
delete msg._client;
|
|
220
|
+
return msg;
|
|
221
|
+
|
|
222
|
+
// UNICAST — only the tab that sent the original msg (echo)
|
|
223
|
+
// Keep msg._client as-is; it already carries portalClient.
|
|
224
|
+
return msg;
|
|
225
|
+
|
|
226
|
+
// UNICAST — a specific tab whose ID you know
|
|
227
|
+
msg._client = { portalClient: "a1b2c3d4-..." };
|
|
228
|
+
return msg;
|
|
229
|
+
|
|
230
|
+
// USER-CAST — every tab of a specific user (even ones that just opened)
|
|
231
|
+
msg._client = { userId: "alice" };
|
|
232
|
+
return msg;
|
|
233
|
+
|
|
234
|
+
// AUTH-CAST — every session that arrived with x-portal-user-* identity;
|
|
235
|
+
// anonymous sessions (no proxy headers) are skipped
|
|
236
|
+
msg._client = { authenticated: true };
|
|
237
|
+
return msg;
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
**Auth-cast vs broadcast.** Broadcast reaches every connected client, including anonymous ones. Auth-cast reaches only sessions that arrived with `x-portal-user-*` identity headers. Use auth-cast for data that any logged-in user may see; use broadcast only for data that is genuinely public. Nothing is cached server-side — each mode delivers to the clients connected at send time.
|
|
241
|
+
|
|
242
|
+
### Authenticated-only delivery (fail-safe default)
|
|
243
|
+
|
|
244
|
+
With the **Authenticated-only delivery** checkbox enabled on the node, an untargeted `msg` (no `msg._client`) is delivered as an **auth-cast** instead of a broadcast — a forgotten `_client` then narrows delivery to logged-in sessions rather than leaking to anonymous ones. A true broadcast stays available explicitly:
|
|
245
|
+
|
|
246
|
+
```javascript
|
|
247
|
+
msg._client = { authenticated: false }; // deliberate broadcast, anonymous included
|
|
248
|
+
return msg;
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
Anonymous sessions are told up front: right after `hello` the server sends a single `auth_required` frame (once per connection — never per skipped message, so unauthenticated clients learn nothing about traffic volume or timing). The hook exposes it so the page can render a login hint instead of staying silently empty:
|
|
252
|
+
|
|
253
|
+
```jsx
|
|
254
|
+
const { data, authRequired } = useNodeRed();
|
|
255
|
+
if (authRequired) return <p className="p-4 text-zinc-500">Log in to see this data.</p>;
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
**Anti-spoof guarantee.** On every inbound message the server overwrites `msg._client` from scratch using the socket's own `portalClient` and the user data captured at connect. A browser cannot forge `_client` — whatever it puts there is discarded.
|
|
259
|
+
|
|
260
|
+
**User-cast uses an O(1) index** so a message to `{userId: "alice"}` reaches every tab of Alice with a single lookup, not a scan.
|
|
261
|
+
|
|
262
|
+
### Without a user (anonymous mode)
|
|
263
|
+
|
|
264
|
+
If **Portal Auth** is off (or no proxy headers arrive), everything still works:
|
|
265
|
+
|
|
266
|
+
- `broadcast` and `portalClient` unicast: unchanged
|
|
267
|
+
- `user`-cast: gracefully skipped (no `userId` to target)
|
|
268
|
+
- `auth`-cast: delivers to nobody (no session has identity headers)
|
|
269
|
+
- `useNodeRed().user` is `null`
|
|
270
|
+
|
|
271
|
+
Same model as dashboard 2 — no auth required to use the node.
|
|
272
|
+
|
|
273
|
+
## Plugins: identity providers and CSS engines
|
|
274
|
+
|
|
275
|
+
Who the user is and how CSS is generated are both pluggable. The defaults,
|
|
276
|
+
`headers` (the contract above) and `tailwind`, are themselves plugins shipped
|
|
277
|
+
in this package and use exactly this API. A separate npm package can register its own, e.g. for a different
|
|
278
|
+
auth proxy or a different CSS tool:
|
|
279
|
+
|
|
280
|
+
```js
|
|
281
|
+
// package.json: "node-red": { "plugins": { "my-providers": "plugin.js" } }
|
|
282
|
+
module.exports = function (RED) {
|
|
283
|
+
RED.plugins.registerPlugin("my-sso", {
|
|
284
|
+
type: "fromcubes-identity",
|
|
285
|
+
apiVersion: 1,
|
|
286
|
+
async resolveUser(req) { // HTTP request or WS upgrade request
|
|
287
|
+
const claims = await verify(req.headers.authorization);
|
|
288
|
+
return claims ? { userId: claims.sub, login: claims.preferred_username, groups: claims.groups } : null;
|
|
289
|
+
},
|
|
290
|
+
});
|
|
291
|
+
RED.plugins.registerPlugin("my-css", {
|
|
292
|
+
type: "fromcubes-css",
|
|
293
|
+
apiVersion: 1,
|
|
294
|
+
async generate(source) { // the portal's full source, components included
|
|
295
|
+
return { css: await compileMyCss(source) };
|
|
296
|
+
},
|
|
297
|
+
editor: { // optional: className="…" completion
|
|
298
|
+
classes() { return ["card", "btn", "btn-primary"]; },
|
|
299
|
+
},
|
|
300
|
+
});
|
|
301
|
+
};
|
|
302
|
+
```
|
|
303
|
+
|
|
304
|
+
Installing a plugin does not activate it. Choose one per kind in the editor,
|
|
305
|
+
**Menu → Settings → Fromcubes**: every installed provider is listed with the
|
|
306
|
+
package it comes from, and exactly one is selected. The choice is saved in
|
|
307
|
+
`<userDir>/fromcubes/providers.json` and applies at once — a new CSS engine
|
|
308
|
+
rebuilds every portal; a new identity provider asks for confirmation, then
|
|
309
|
+
reloads every open portal tab so page and connection both use it.
|
|
310
|
+
|
|
311
|
+
An administrator can pin either choice in `settings.js`; the editor then shows
|
|
312
|
+
it locked:
|
|
313
|
+
|
|
314
|
+
```js
|
|
315
|
+
portalReact: {
|
|
316
|
+
identityProvider: "my-sso", // default "headers"; "none" = everyone anonymous
|
|
317
|
+
cssEngine: "my-css", // default "tailwind"; "none" serves no stylesheet
|
|
318
|
+
},
|
|
319
|
+
```
|
|
320
|
+
|
|
321
|
+
- `resolveUser` returns a user or `null`. Only the known fields
|
|
322
|
+
(`userId`, `login`, `displayName`, `email`, `role`, `groups`) are kept; a
|
|
323
|
+
throw, a rejection or 5 s without an answer means anonymous.
|
|
324
|
+
- `generate` returns `{ css }`. A failure shows the yellow `css-fail` status
|
|
325
|
+
and the page is served unstyled.
|
|
326
|
+
- `editor.classes()` (optional, may be async) feeds the class completion in
|
|
327
|
+
every code editor; it is asked once per installed plugin. Without it the
|
|
328
|
+
engine just gets no completion. Strings only, no whitespace, ≤ 200 chars,
|
|
329
|
+
at most 50 000.
|
|
330
|
+
- An unknown provider name is logged once; for identity every session is then
|
|
331
|
+
anonymous. The settings pane flags a selected provider whose package is gone.
|
|
332
|
+
- Optional `label: "…"` on the plugin is shown next to its id in the settings
|
|
333
|
+
pane.
|
|
334
|
+
- `require("@aaqu/fromcubes-react/api")` gives the type names and
|
|
335
|
+
`API_VERSION`, plus `createAdminSecurity(RED)` — the permission, CSRF and
|
|
336
|
+
rate-limit gates the core puts on its own admin routes — for a plugin that
|
|
337
|
+
adds routes. Declare `@aaqu/fromcubes-react` as a peer dependency. The
|
|
338
|
+
three add-ons above are complete examples.
|
|
339
|
+
|
|
340
|
+
## Portal Assets
|
|
341
|
+
|
|
342
|
+
Static files (3D models, textures, fonts, etc.) can be uploaded and served from a public endpoint.
|
|
343
|
+
|
|
344
|
+
- Open the **Portal Assets** tab in the Node-RED sidebar
|
|
345
|
+
- Upload files via button or drag & drop
|
|
346
|
+
- Organize in folders (create, rename, move between folders)
|
|
347
|
+
- Copy public path with one click — use in JSX: `/fromcubes/public/models/scene.glb`
|
|
348
|
+
- Download and delete files from the context menu
|
|
349
|
+
|
|
350
|
+
All uploads require Node-RED admin authentication. Files are served publicly at `/fromcubes/public/`.
|
|
351
|
+
|
|
352
|
+
Limits: 100 MB per file, 500 MB total, 1000 files max.
|
|
353
|
+
|
|
354
|
+
## Examples
|
|
355
|
+
|
|
356
|
+
Available via **Menu → Import → Examples → @aaqu/fromcubes-react**.
|
|
357
|
+
Each flow includes a comment node with what to expect and the URL to open.
|
|
358
|
+
|
|
359
|
+
Import **Shared Components** first — it provides the UI building blocks (Page, Header, Stat, Button, ValueBadge) referenced by the others.
|
|
360
|
+
|
|
361
|
+
| Example | npm packages | Description |
|
|
362
|
+
|---|---|---|
|
|
363
|
+
| **Shared Components** | — | Reusable components: Page, Header, Stat, Button, ValueBadge |
|
|
364
|
+
| **Sensor Portal** | — | Sensor gauge with live WebSocket data |
|
|
365
|
+
| **Live Chart** | `chart.js/auto` | Live updating Chart.js charts |
|
|
366
|
+
| **D3 Poland Map** | `d3` | Interactive SVG map of Poland (simulated data) |
|
|
367
|
+
| **Three.js Scene** | `three`, `@react-three/fiber`, `@react-three/drei` | 3D scene with Three.js |
|
|
368
|
+
| **PixiJS Sprites** | `pixi.js`, `@pixi/react` | Clickable bunny sprites with PixiJS |
|
|
369
|
+
| **WebGPU Shader** | `three` | WebGPU renderer + TSL animated shaders |
|
|
370
|
+
| **Utility Hooks** | — | `fc-portal-utility` demo: `useDebounce` custom hook + `clamp` helper |
|
|
371
|
+
|
|
372
|
+
## Troubleshooting
|
|
373
|
+
|
|
374
|
+
| Symptom | Likely cause |
|
|
375
|
+
|---|---|
|
|
376
|
+
| Red status "transpile error" + error page on the endpoint | JSX syntax error — fix code, redeploy (cache invalidates automatically) |
|
|
377
|
+
| Red status "legacy endpoint" on deploy | Flow was saved before the `/fromcubes/` prefix became hardcoded. Open the node, set **Sub-path**, redeploy. Automatic migration is disabled to avoid silent URL changes. |
|
|
378
|
+
| Red status "bad sub-path" | Sub-path is empty or violates the rules (no leading `/`, no whitespace, no `..`, segments must start alphanumerically, `public`/`_ws` reserved). |
|
|
379
|
+
| Yellow status "css-fail" | Tailwind generation failed (usually an invalid class in JSX). Page still loads but unstyled. Fix the class, redeploy — the status clears on the next successful build. |
|
|
380
|
+
| Code editor stays blank / `fromcubes-react/vs/loader.js` 404s | Monaco is served live from the package's own `node_modules` at `/fromcubes-react/vs` (no postinstall copy step). The 404 means `RED.httpAdmin` is mounted under a non-root `httpAdminRoot` the editor didn't account for, or `monaco-editor` failed to install. Reinstall the package and hard-refresh the editor (`Cmd/Ctrl-Shift-R`). |
|
|
381
|
+
| `npm install @aaqu/fromcubes-react` ends with `EACCES` | The Node-RED `userDir/node_modules` install path is not writable by the user running Node-RED. Re-run `npm install` from a shell with the right ownership. |
|
|
382
|
+
| Browser request `/fromcubes/css/<hash>.css` returns 404 | The portal's deploy hasn't produced a CSS bundle yet — open the editor, redeploy. If the URL is bookmarked from before a deploy, the hash is stale; reload the portal page itself. With a custom `httpNodeRoot`, prepend it to this path. |
|
|
383
|
+
| WebSocket reconnects in an endless loop | Reverse-proxy is not forwarding `Upgrade: websocket` on `/fromcubes/<sub-path>/_ws`. Check the proxy config — nginx needs `proxy_set_header Upgrade $http_upgrade`, Traefik needs the `websocket` middleware. |
|
|
384
|
+
| `libs` packages fail to install on deploy | The user-installed npm packages declared in **Libs** install via Node-RED's `dynamicModuleList` mechanism, which needs network access from `userDir` and a writable `node_modules`. Behind a corporate proxy set `npm config set proxy …` for the Node-RED user. |
|
|
385
|
+
| Page loads but `data` stays `null` | Nothing has been pushed since the page connected — payloads are not cached server-side. Push periodically, or request initial state from the component (`send({}, "init")` → flow replies) |
|
|
386
|
+
| `user` is `null` even with Portal Auth on | Upstream proxy is not injecting `x-portal-user-id` (required — the other headers alone count as anonymous), or sends a malformed percent-escape in it |
|
|
387
|
+
| Page reloads on every deploy | Expected for code changes; clients soft-reconnect with exponential backoff |
|
|
388
|
+
|
|
389
|
+
## Architecture: source AND sink
|
|
390
|
+
|
|
391
|
+
The Node-RED guidelines suggest a node should "sit at the beginning, middle
|
|
392
|
+
or end of a flow — not all at once". `portal-react` intentionally violates
|
|
393
|
+
this: every node is both a **sink** (`msg → render`) and a **source** (UI
|
|
394
|
+
event → outbound `msg`). Trade-offs:
|
|
395
|
+
|
|
396
|
+
- **Pro**: one wire per portal is conceptually simpler — the same node owns
|
|
397
|
+
the page lifecycle, its WebSocket, and the routing rules.
|
|
398
|
+
- **Con**: flow-error tracking only follows the `input → output` path.
|
|
399
|
+
Errors originating in the browser (e.g. a runtime exception inside a UI
|
|
400
|
+
handler) cross the WebSocket as a source-event and are **not** routed to
|
|
401
|
+
a Catch node on the same tab — they surface as a red node status and a
|
|
402
|
+
log line. This matches the Node-RED guidance for source nodes: "produces
|
|
403
|
+
messages in response to external events".
|
|
404
|
+
|
|
405
|
+
If you need explicit Catch-node visibility for UI-originated errors, wire
|
|
406
|
+
the portal's output through a Switch / Catch chain — a UI event landing on
|
|
407
|
+
`msg.payload` is then a regular wire-level message that Catch can observe.
|
|
408
|
+
|
|
409
|
+
## Security
|
|
410
|
+
|
|
411
|
+
This module is meant to run behind a trusted Node-RED instance, ideally
|
|
412
|
+
behind a reverse proxy. Highlights:
|
|
413
|
+
|
|
414
|
+
- Admin write endpoints require the `Node-RED-API-Version` header (CSRF
|
|
415
|
+
protection) and a `portal-react.write` permission when `adminAuth` is
|
|
416
|
+
configured.
|
|
417
|
+
- Token-bucket rate-limit (default 60 burst / 1 req/s steady) on every
|
|
418
|
+
write endpoint, tunable via `RED.settings.portalReact.rateLimit`.
|
|
419
|
+
- JSON bodies capped by Node-RED's own `apiMaxLength` (settings.js, default
|
|
420
|
+
5 MB), 100 MB asset-upload cap, 500 MB / 1000 files total
|
|
421
|
+
assets quota, 1 MB WebSocket frame cap.
|
|
422
|
+
- `x-portal-user-*` identity headers are trusted unconditionally — production
|
|
423
|
+
deployments **must** terminate auth at a reverse proxy that strips all six
|
|
424
|
+
inbound identity headers before injecting verified ones, and Node-RED must
|
|
425
|
+
not be reachable except through that proxy.
|
|
426
|
+
|
|
427
|
+
## Support
|
|
428
|
+
|
|
429
|
+
fromcubes is built and maintained by one person. If it saves you time,
|
|
430
|
+
consider sponsoring its development:
|
|
431
|
+
|
|
432
|
+
[](https://github.com/sponsors/Aaqu)
|
|
433
|
+
|
|
434
|
+
## License
|
|
435
|
+
|
|
436
|
+
Apache-2.0
|