@aaqu/fromcubes-react 0.1.0 → 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/README.md +40 -10
- package/examples/PixiJS Sprites.json +3 -3
- package/examples/Three.js Scene.json +3 -3
- package/nodes/editor/workspace.css +7 -0
- package/nodes/lib/providers.js +53 -8
- package/package.json +4 -7
package/README.md
CHANGED
|
@@ -6,20 +6,19 @@
|
|
|
6
6
|
[](https://nodered.org)
|
|
7
7
|
[](./LICENSE)
|
|
8
8
|
[](https://discord.gg/KYzsfKkYxX)
|
|
9
|
+
[](https://github.com/sponsors/Aaqu)
|
|
9
10
|
|
|
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.
|
|
11
12
|
|
|
12
13
|
[](https://aaqu.github.io/fromcubes-modules/)
|
|
13
14
|
|
|
14
|
-
> **⚠️ Alpha Module** — This project is in early development. Expect breaking changes. Test on a clean Node-RED instance.
|
|
15
|
-
|
|
16
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.
|
|
17
16
|
|
|
18
17
|
For internals, plugin authoring, and the deploy pipeline see [README-DEV.md](./README-DEV.md).
|
|
19
18
|
|
|
20
19
|
**💬 Questions, feedback, showcase?** Join the [fromcubes Discord](https://discord.gg/KYzsfKkYxX).
|
|
21
20
|
|
|
22
|
-
[](https://github.com/sponsors/Aaqu)
|
|
23
22
|
|
|
24
23
|
## Prerequisites
|
|
25
24
|
|
|
@@ -41,12 +40,17 @@ npm install @aaqu/fromcubes-react
|
|
|
41
40
|
# restart Node-RED
|
|
42
41
|
```
|
|
43
42
|
|
|
44
|
-
That's it. No build step. Any npm packages you list in the node config (e.g. `d3, three`) are installed automatically on deploy.
|
|
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.
|
|
45
44
|
|
|
46
45
|
### Add-ons
|
|
47
46
|
|
|
48
|
-
|
|
49
|
-
|
|
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
|
+
```
|
|
50
54
|
|
|
51
55
|
| Package | What it adds |
|
|
52
56
|
|---|---|
|
|
@@ -54,10 +58,29 @@ installs them too:
|
|
|
54
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) |
|
|
55
59
|
| [`@aaqu/fromcubes-assets`](https://www.npmjs.com/package/@aaqu/fromcubes-assets) | Static files for portals, managed from a sidebar tab |
|
|
56
60
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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.
|
|
61
84
|
|
|
62
85
|
## Your first portal
|
|
63
86
|
|
|
@@ -401,6 +424,13 @@ behind a reverse proxy. Highlights:
|
|
|
401
424
|
inbound identity headers before injecting verified ones, and Node-RED must
|
|
402
425
|
not be reachable except through that proxy.
|
|
403
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
|
+
|
|
404
434
|
## License
|
|
405
435
|
|
|
406
436
|
Apache-2.0
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"id": "fc-pixi-comp-statusbar",
|
|
30
30
|
"type": "fc-portal-component",
|
|
31
31
|
"z": "fc-pixi-flow",
|
|
32
|
-
"compName": "
|
|
33
|
-
"compCode": "function
|
|
32
|
+
"compName": "PixiStatusBar",
|
|
33
|
+
"compCode": "function PixiStatusBar({ spriteCount, totalClicks, lastClicked }) {\n return (\n <div className=\"px-6 py-4 bg-zinc-900/50 border-t border-zinc-800/50 flex items-center gap-6 flex-wrap\">\n <Stat label=\"Sprites\">\n <span className=\"text-sm font-mono text-cyan-400\">{spriteCount}</span>\n </Stat>\n <Stat label=\"Total clicks\">\n <span className=\"text-sm font-mono text-emerald-400\">{totalClicks}</span>\n </Stat>\n <Stat label=\"Last clicked\">\n <span className=\"text-sm font-mono text-zinc-300\">{lastClicked || '\\u2014'}</span>\n </Stat>\n </div>\n );\n}",
|
|
34
34
|
"x": 360,
|
|
35
35
|
"y": 300,
|
|
36
36
|
"wires": []
|
|
@@ -91,7 +91,7 @@
|
|
|
91
91
|
"var": "PixiReact"
|
|
92
92
|
}
|
|
93
93
|
],
|
|
94
|
-
"componentCode": "import { Sprite } from 'pixi.js';\nimport { Application, extend } from '@pixi/react';\nextend({ Sprite });\n\nfunction App() {\n const { data, send } = useNodeRed();\n const [sprites, setSprites] = useState([]);\n const [totalClicks, setTotalClicks] = useState(0);\n const [lastClicked, setLastClicked] = useState(null);\n const idRef = useRef(0);\n\n useEffect(() => {\n if (!data) return;\n setSprites(prev => {\n const next = [...prev, {\n id: idRef.current++,\n x: 40 + Math.random() * 520,\n y: 40 + Math.random() * 320,\n tint: data.tint || 0x22d3ee\n }];\n return next.length > 12 ? next.slice(-12) : next;\n });\n }, [data]);\n\n const handleClick = useCallback((info) => {\n setTotalClicks(c => c + 1);\n setLastClicked('#' + info.id);\n send({\n action: 'clicked',\n spriteId: info.id,\n x: Math.round(info.x),\n y: Math.round(info.y),\n spriteClicks: info.clicks,\n timestamp: Date.now()\n }, 'pixi/click');\n }, [send]);\n\n return (\n <Page>\n <Header title=\"fromcubes Pixi\" subtitle=\"click the bunnies\" />\n <div className=\"flex-1 flex items-center justify-center p-6\">\n <div className=\"rounded-xl overflow-hidden border border-zinc-800\">\n <Application width={600} height={400} background={0x18181b} antialias>\n {sprites.map(s => (\n <ClickableSprite key={s.id} {...s} onClick={handleClick} />\n ))}\n </Application>\n </div>\n </div>\n <
|
|
94
|
+
"componentCode": "import { Sprite } from 'pixi.js';\nimport { Application, extend } from '@pixi/react';\nextend({ Sprite });\n\nfunction App() {\n const { data, send } = useNodeRed();\n const [sprites, setSprites] = useState([]);\n const [totalClicks, setTotalClicks] = useState(0);\n const [lastClicked, setLastClicked] = useState(null);\n const idRef = useRef(0);\n\n useEffect(() => {\n if (!data) return;\n setSprites(prev => {\n const next = [...prev, {\n id: idRef.current++,\n x: 40 + Math.random() * 520,\n y: 40 + Math.random() * 320,\n tint: data.tint || 0x22d3ee\n }];\n return next.length > 12 ? next.slice(-12) : next;\n });\n }, [data]);\n\n const handleClick = useCallback((info) => {\n setTotalClicks(c => c + 1);\n setLastClicked('#' + info.id);\n send({\n action: 'clicked',\n spriteId: info.id,\n x: Math.round(info.x),\n y: Math.round(info.y),\n spriteClicks: info.clicks,\n timestamp: Date.now()\n }, 'pixi/click');\n }, [send]);\n\n return (\n <Page>\n <Header title=\"fromcubes Pixi\" subtitle=\"click the bunnies\" />\n <div className=\"flex-1 flex items-center justify-center p-6\">\n <div className=\"rounded-xl overflow-hidden border border-zinc-800\">\n <Application width={600} height={400} background={0x18181b} antialias>\n {sprites.map(s => (\n <ClickableSprite key={s.id} {...s} onClick={handleClick} />\n ))}\n </Application>\n </div>\n </div>\n <PixiStatusBar spriteCount={sprites.length} totalClicks={totalClicks} lastClicked={lastClicked} />\n </Page>\n );\n}",
|
|
95
95
|
"x": 560,
|
|
96
96
|
"y": 160,
|
|
97
97
|
"wires": [
|
|
@@ -29,8 +29,8 @@
|
|
|
29
29
|
"id": "fc-three-comp-statusbar",
|
|
30
30
|
"type": "fc-portal-component",
|
|
31
31
|
"z": "fc-three-flow",
|
|
32
|
-
"compName": "
|
|
33
|
-
"compCode": "function
|
|
32
|
+
"compName": "ThreeStatusBar",
|
|
33
|
+
"compCode": "function ThreeStatusBar({ leftColor, rightColor, clickCount }) {\n return (\n <div className=\"px-6 py-4 bg-zinc-900/50 border-t border-zinc-800/50 flex items-center gap-6 flex-wrap\">\n <Stat label=\"Left box\">\n <span className=\"w-4 h-4 rounded-full border border-zinc-700\" style={{ backgroundColor: leftColor }} />\n </Stat>\n <Stat label=\"Right box\">\n <span className=\"w-4 h-4 rounded-full border border-zinc-700\" style={{ backgroundColor: rightColor }} />\n </Stat>\n <Stat label=\"Clicks\">\n <span className=\"text-sm font-mono text-cyan-400\">{clickCount}</span>\n </Stat>\n </div>\n );\n}",
|
|
34
34
|
"x": 360,
|
|
35
35
|
"y": 300,
|
|
36
36
|
"wires": []
|
|
@@ -95,7 +95,7 @@
|
|
|
95
95
|
"var": "Drei"
|
|
96
96
|
}
|
|
97
97
|
],
|
|
98
|
-
"componentCode": "import { Canvas } from '@react-three/fiber';\nimport { OrbitControls } from '@react-three/drei';\n\nfunction App() {\n const { data, send } = useNodeRed();\n const colors = data || { leftColor: 'orange', rightColor: 'cyan' };\n const [clickCount, setClickCount] = useState(0);\n\n const handleToggle = useCallback((clicked) => {\n setClickCount(prev => prev + 1);\n send({ action: clicked ? 'clicked' : 'unclicked', clicks: clickCount + 1 }, '3d/interact');\n }, [send, clickCount]);\n\n return (\n <div className=\"h-screen bg-zinc-950 text-zinc-100 flex flex-col\">\n <Header title=\"fromcubes 3D\" subtitle=\"React Three Fiber + live Node-RED data\" />\n <div className=\"flex-1 overflow-hidden\">\n <Canvas style={{ width: '100%', height: '100%' }}>\n <ambientLight intensity={Math.PI / 2} />\n <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />\n <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />\n <Box position={[-1.2, 0, 0]} color={colors.leftColor} onToggle={handleToggle} />\n <Box position={[1.2, 0, 0]} color={colors.rightColor} onToggle={handleToggle} />\n <OrbitControls />\n </Canvas>\n </div>\n <
|
|
98
|
+
"componentCode": "import { Canvas } from '@react-three/fiber';\nimport { OrbitControls } from '@react-three/drei';\n\nfunction App() {\n const { data, send } = useNodeRed();\n const colors = data || { leftColor: 'orange', rightColor: 'cyan' };\n const [clickCount, setClickCount] = useState(0);\n\n const handleToggle = useCallback((clicked) => {\n setClickCount(prev => prev + 1);\n send({ action: clicked ? 'clicked' : 'unclicked', clicks: clickCount + 1 }, '3d/interact');\n }, [send, clickCount]);\n\n return (\n <div className=\"h-screen bg-zinc-950 text-zinc-100 flex flex-col\">\n <Header title=\"fromcubes 3D\" subtitle=\"React Three Fiber + live Node-RED data\" />\n <div className=\"flex-1 overflow-hidden\">\n <Canvas style={{ width: '100%', height: '100%' }}>\n <ambientLight intensity={Math.PI / 2} />\n <spotLight position={[10, 10, 10]} angle={0.15} penumbra={1} decay={0} intensity={Math.PI} />\n <pointLight position={[-10, -10, -10]} decay={0} intensity={Math.PI} />\n <Box position={[-1.2, 0, 0]} color={colors.leftColor} onToggle={handleToggle} />\n <Box position={[1.2, 0, 0]} color={colors.rightColor} onToggle={handleToggle} />\n <OrbitControls />\n </Canvas>\n </div>\n <ThreeStatusBar leftColor={colors.leftColor} rightColor={colors.rightColor} clickCount={clickCount} />\n </div>\n );\n}",
|
|
99
99
|
"x": 560,
|
|
100
100
|
"y": 160,
|
|
101
101
|
"wires": [
|
|
@@ -630,6 +630,13 @@ body.fc-ws-open #red-ui-notifications {
|
|
|
630
630
|
/* ── Settings → Fromcubes (provider-settings.js) ─────────────────
|
|
631
631
|
Lives in Node-RED's own Settings tray, not in the overlay, so it uses
|
|
632
632
|
Node-RED's theme variables instead of the vs-dark palette above. */
|
|
633
|
+
/* Same box as Node-RED's own View pane (#red-ui-settings-tab-view): fills
|
|
634
|
+
the content area, scrolls on its own, 20px side padding. */
|
|
635
|
+
.fc-ps {
|
|
636
|
+
position: absolute; top: 0; right: 0; bottom: 0; left: 0;
|
|
637
|
+
padding: 8px 20px 20px;
|
|
638
|
+
overflow-y: auto;
|
|
639
|
+
}
|
|
633
640
|
.fc-ps .fc-ps-section { margin-bottom: 24px; }
|
|
634
641
|
.fc-ps .fc-ps-hint { color: var(--red-ui-secondary-text-color); margin: 0 0 8px; }
|
|
635
642
|
.fc-ps .fc-ps-note,
|
package/nodes/lib/providers.js
CHANGED
|
@@ -30,9 +30,13 @@
|
|
|
30
30
|
* — the administrator's word; the editor shows it locked.
|
|
31
31
|
* 2. The choice made in the editor (Settings → Fromcubes), persisted in
|
|
32
32
|
* `<userDir>/fromcubes/providers.json`.
|
|
33
|
-
* 3. The
|
|
34
|
-
*
|
|
35
|
-
*
|
|
33
|
+
* 3. The default: the official add-on when it is installed — `headers`
|
|
34
|
+
* from @aaqu/fromcubes-auth-headers, `tailwind` from
|
|
35
|
+
* @aaqu/fromcubes-tailwind — and `none` when it is not. The core works
|
|
36
|
+
* on its own; installing the add-on is what turns the feature on. Only
|
|
37
|
+
* the official package counts here: another package registering a
|
|
38
|
+
* plugin called `headers` never becomes the default identity provider
|
|
39
|
+
* (it can still be chosen explicitly, in 1 or 2).
|
|
36
40
|
* Installing a plugin never switches anything on its own — for identity that
|
|
37
41
|
* would let a package change who counts as logged in.
|
|
38
42
|
*
|
|
@@ -50,6 +54,7 @@ const API_VERSION = 1;
|
|
|
50
54
|
|
|
51
55
|
const DEFAULT_IDENTITY = "headers";
|
|
52
56
|
const DEFAULT_CSS = "tailwind";
|
|
57
|
+
const NONE = "none";
|
|
53
58
|
|
|
54
59
|
// Bounds on what a provider may hand us. Generous enough that anything the
|
|
55
60
|
// built-in header parser produces passes untouched (its groups header is
|
|
@@ -68,8 +73,18 @@ const MAX_EDITOR_CLASS_LEN = 200;
|
|
|
68
73
|
const USER_STRING_FIELDS = ["login", "displayName", "email", "role"];
|
|
69
74
|
|
|
70
75
|
const KINDS = {
|
|
71
|
-
identity: {
|
|
72
|
-
|
|
76
|
+
identity: {
|
|
77
|
+
type: IDENTITY_TYPE,
|
|
78
|
+
setting: "identityProvider",
|
|
79
|
+
fallback: DEFAULT_IDENTITY,
|
|
80
|
+
fallbackModule: "@aaqu/fromcubes-auth-headers",
|
|
81
|
+
},
|
|
82
|
+
css: {
|
|
83
|
+
type: CSS_TYPE,
|
|
84
|
+
setting: "cssEngine",
|
|
85
|
+
fallback: DEFAULT_CSS,
|
|
86
|
+
fallbackModule: "@aaqu/fromcubes-tailwind",
|
|
87
|
+
},
|
|
73
88
|
};
|
|
74
89
|
|
|
75
90
|
// Provider ids travel through the settings file, the admin API and the
|
|
@@ -261,7 +276,24 @@ function createProviders(RED, opts = {}) {
|
|
|
261
276
|
return { id: fromSettings, source: "settings" };
|
|
262
277
|
}
|
|
263
278
|
if (chosen[kind]) return { id: chosen[kind], source: "editor" };
|
|
264
|
-
|
|
279
|
+
const official = pluginsOf(k.type).some(
|
|
280
|
+
(p) => p.id === k.fallback && p.module === k.fallbackModule,
|
|
281
|
+
);
|
|
282
|
+
return { id: official ? k.fallback : NONE, source: "default", addonMissing: !official };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Log once, at info level, why a kind runs on `none` by default — so a
|
|
287
|
+
* core installed on its own explains itself instead of failing quietly.
|
|
288
|
+
* @param {string} kind
|
|
289
|
+
* @param {string} msg
|
|
290
|
+
* @returns {void}
|
|
291
|
+
*/
|
|
292
|
+
function explainNone(kind, msg) {
|
|
293
|
+
const key = "none:" + kind;
|
|
294
|
+
if (logged.has(key)) return;
|
|
295
|
+
logged.add(key);
|
|
296
|
+
if (RED.log.info) RED.log.info("[portal-react] " + msg);
|
|
265
297
|
}
|
|
266
298
|
|
|
267
299
|
/**
|
|
@@ -428,7 +460,15 @@ function createProviders(RED, opts = {}) {
|
|
|
428
460
|
* @returns {Promise<Object|null>}
|
|
429
461
|
*/
|
|
430
462
|
async function resolveUser(req) {
|
|
431
|
-
const
|
|
463
|
+
const cur = current("identity");
|
|
464
|
+
const id = cur.id;
|
|
465
|
+
// Called only for portals with Portal Auth on — so this is someone who
|
|
466
|
+
// expects users and gets none.
|
|
467
|
+
if (cur.addonMissing) {
|
|
468
|
+
explainNone("identity", "Portal Auth is on but no identity provider is installed, so every " +
|
|
469
|
+
"session is anonymous. For users from x-portal-user-* headers install " +
|
|
470
|
+
"@aaqu/fromcubes-auth-headers, or choose a provider in Settings → Fromcubes.");
|
|
471
|
+
}
|
|
432
472
|
const { provider, error } = lookup(IDENTITY_TYPE, builtins.identity, id);
|
|
433
473
|
if (!provider) {
|
|
434
474
|
logOnce(error + " — every session is anonymous");
|
|
@@ -466,7 +506,12 @@ function createProviders(RED, opts = {}) {
|
|
|
466
506
|
* @returns {Promise<{css: string}>}
|
|
467
507
|
*/
|
|
468
508
|
async function generateCss(source) {
|
|
469
|
-
const
|
|
509
|
+
const cur = current("css");
|
|
510
|
+
const id = cur.id;
|
|
511
|
+
if (cur.addonMissing) {
|
|
512
|
+
explainNone("css", "No CSS engine is installed, so portals are served without a stylesheet. " +
|
|
513
|
+
"For Tailwind CSS install @aaqu/fromcubes-tailwind.");
|
|
514
|
+
}
|
|
470
515
|
const { provider, error } = lookup(CSS_TYPE, builtins.css, id);
|
|
471
516
|
if (!provider) throw new Error(error);
|
|
472
517
|
if (typeof provider.generate !== "function") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaqu/fromcubes-react",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Fromcubes React - React portals for Node-RED, built at deploy, with live WebSocket data binding",
|
|
5
5
|
"type": "commonjs",
|
|
6
6
|
"keywords": [
|
|
@@ -41,10 +41,7 @@
|
|
|
41
41
|
"monaco-editor": "^0.56.0",
|
|
42
42
|
"prettier": "^3.9.8",
|
|
43
43
|
"react": "^19.3.0",
|
|
44
|
-
"react-dom": "^19.3.0"
|
|
45
|
-
"@aaqu/fromcubes-assets": "^0.1.0",
|
|
46
|
-
"@aaqu/fromcubes-auth-headers": "^0.1.0",
|
|
47
|
-
"@aaqu/fromcubes-tailwind": "^0.1.0"
|
|
44
|
+
"react-dom": "^19.3.0"
|
|
48
45
|
},
|
|
49
46
|
"peerDependencies": {
|
|
50
47
|
"ws": "^8.21.0"
|
|
@@ -64,8 +61,8 @@
|
|
|
64
61
|
},
|
|
65
62
|
"homepage": "https://aaqu.github.io/fromcubes-modules/",
|
|
66
63
|
"funding": {
|
|
67
|
-
"type": "
|
|
68
|
-
"url": "https://
|
|
64
|
+
"type": "github",
|
|
65
|
+
"url": "https://github.com/sponsors/Aaqu"
|
|
69
66
|
},
|
|
70
67
|
"license": "Apache-2.0",
|
|
71
68
|
"author": {
|