@aaqu/fromcubes-react 0.1.0 → 0.2.1
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/file-tree.js +1 -1
- package/nodes/editor/format.js +1 -1
- package/nodes/editor/provider-settings.js +8 -8
- package/nodes/editor/workspace.css +7 -0
- package/nodes/editor/workspace.js +5 -3
- package/nodes/lib/admin-api.js +33 -22
- package/nodes/lib/providers.js +53 -8
- package/nodes/portal-react.html +24 -8
- package/nodes/portal-react.js +6 -1
- 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": [
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* module is a pair of pure functions so it can be unit-tested under vitest and
|
|
8
8
|
* still be loaded as a plain browser script by the Node-RED editor.
|
|
9
9
|
*
|
|
10
|
-
* Loaded in the browser
|
|
10
|
+
* Loaded in the browser by the asset loader in portal-react.html (`<base>/editor/file-tree.js`)
|
|
11
11
|
* (exposes `window.__fcFileTree`) and in tests via `require()`.
|
|
12
12
|
*/
|
|
13
13
|
(function (root, factory) {
|
package/nodes/editor/format.js
CHANGED
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"use strict";
|
|
14
14
|
|
|
15
15
|
const PREFIX = "[FC-Format]";
|
|
16
|
-
const BASE = "fromcubes-react/prettier/";
|
|
16
|
+
const BASE = (window.__fcAssetBase || "fromcubes-react/") + "prettier/";
|
|
17
17
|
// UMD builds: each sets window.prettier / window.prettierPlugins.
|
|
18
18
|
const SCRIPTS = [
|
|
19
19
|
"standalone.js",
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Settings → Fromcubes: choose the
|
|
2
|
+
* Settings → Fromcubes: choose the CSS engine and the identity provider.
|
|
3
3
|
*
|
|
4
4
|
* Exactly one provider per kind, shown as a radio list, so two can never be
|
|
5
5
|
* active at once. Reads and writes `/fromcubes-react/providers`; the server
|
|
6
6
|
* validates, persists to <userDir>/fromcubes/providers.json, and applies the
|
|
7
|
-
* change (
|
|
7
|
+
* change (css: every portal rebuilds; identity: every portal session reloads).
|
|
8
8
|
* A kind set in settings.js is shown locked.
|
|
9
9
|
*
|
|
10
10
|
* Node-RED shows a pane by the element id `red-ui-settings-tab-<pane id>`,
|
|
@@ -18,18 +18,18 @@
|
|
|
18
18
|
var URL = "fromcubes-react/providers";
|
|
19
19
|
|
|
20
20
|
var KINDS = [
|
|
21
|
-
{
|
|
22
|
-
kind: "identity",
|
|
23
|
-
title: "Identity provider",
|
|
24
|
-
hint: "Who is behind a portal session — used by Portal Auth, auth-cast and per-user delivery.",
|
|
25
|
-
setting: "identityProvider",
|
|
26
|
-
},
|
|
27
21
|
{
|
|
28
22
|
kind: "css",
|
|
29
23
|
title: "CSS engine",
|
|
30
24
|
hint: "Generates each portal's stylesheet at deploy. Changing it rebuilds every portal.",
|
|
31
25
|
setting: "cssEngine",
|
|
32
26
|
},
|
|
27
|
+
{
|
|
28
|
+
kind: "identity",
|
|
29
|
+
title: "Identity provider",
|
|
30
|
+
hint: "Who is behind a portal session — used by Portal Auth, auth-cast and per-user delivery.",
|
|
31
|
+
setting: "identityProvider",
|
|
32
|
+
},
|
|
33
33
|
];
|
|
34
34
|
|
|
35
35
|
function errorText(xhr) {
|
|
@@ -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,
|
|
@@ -19,6 +19,8 @@
|
|
|
19
19
|
const EDITOR_NAME = "React Editor";
|
|
20
20
|
const FLUSH_DEBOUNCE_MS = 400;
|
|
21
21
|
const MIN_TREE_WIDTH = 150;
|
|
22
|
+
// Versioned asset path set by portal-react.html (see ASSET_BASE in lib/admin-api.js).
|
|
23
|
+
const ASSET_BASE = window.__fcAssetBase || "fromcubes-react/";
|
|
22
24
|
|
|
23
25
|
// ── Module state ────────────────────────────────────────────
|
|
24
26
|
|
|
@@ -636,7 +638,7 @@
|
|
|
636
638
|
* @returns {void}
|
|
637
639
|
*/
|
|
638
640
|
function injectIconSprite() {
|
|
639
|
-
$.ajax({ url: "
|
|
641
|
+
$.ajax({ url: ASSET_BASE + "editor/icons.svg", dataType: "text" })
|
|
640
642
|
.done(function (markup) {
|
|
641
643
|
$("<div>")
|
|
642
644
|
.attr("id", "fc-ws-icon-sprite")
|
|
@@ -1107,7 +1109,7 @@
|
|
|
1107
1109
|
const brand = $("<div>").addClass("fc-ws-brand").appendTo(header);
|
|
1108
1110
|
$("<img>")
|
|
1109
1111
|
.addClass("fc-ws-mark-logo")
|
|
1110
|
-
.attr("src", "
|
|
1112
|
+
.attr("src", ASSET_BASE + "editor/mark.svg")
|
|
1111
1113
|
.attr("alt", "fromcubes")
|
|
1112
1114
|
.appendTo(brand);
|
|
1113
1115
|
// One word split by colour — wrapped so the lockup's gap sits between the
|
|
@@ -1471,7 +1473,7 @@
|
|
|
1471
1473
|
});
|
|
1472
1474
|
$("<img>")
|
|
1473
1475
|
.addClass("fc-ws-statusbar-icon")
|
|
1474
|
-
.attr("src", "
|
|
1476
|
+
.attr("src", ASSET_BASE + "icons/fromcubes-react.svg")
|
|
1475
1477
|
.attr("alt", "")
|
|
1476
1478
|
.appendTo(el);
|
|
1477
1479
|
$("<span>").text(EDITOR_NAME).appendTo(el);
|
package/nodes/lib/admin-api.js
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
const path = require("path");
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Editor assets are also served under a path carrying the core's version
|
|
5
|
+
* (`fromcubes-react/0.2.0/editor/…`), and the editor template loads them from
|
|
6
|
+
* there. Every cache in between — the browser, a CDN, a reverse proxy — keys
|
|
7
|
+
* on the path, so a new release can never be handed an old file, nor a 404
|
|
8
|
+
* remembered from a moment the route did not exist yet. Query strings are not
|
|
9
|
+
* enough: some caches ignore them. The unversioned paths stay for anything
|
|
10
|
+
* that still points at them.
|
|
11
|
+
*/
|
|
12
|
+
const ASSET_VERSION = String(require("../../package.json").version).replace(
|
|
13
|
+
/[^0-9A-Za-z.+-]/g,
|
|
14
|
+
"",
|
|
15
|
+
);
|
|
16
|
+
/** Relative to the admin root, as the editor needs it (no leading slash). */
|
|
17
|
+
const ASSET_BASE = "fromcubes-react/" + ASSET_VERSION + "/";
|
|
18
|
+
|
|
3
19
|
/**
|
|
4
20
|
* Register editor/admin/public support routes for portal-react.
|
|
5
21
|
*
|
|
@@ -37,41 +53,36 @@ function registerAdminApi(RED, deps) {
|
|
|
37
53
|
onProviderChange,
|
|
38
54
|
} = deps;
|
|
39
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Mount one static directory at both its plain and its versioned path.
|
|
58
|
+
* @param {string} name
|
|
59
|
+
* @param {string} dir
|
|
60
|
+
* @returns {void}
|
|
61
|
+
*/
|
|
62
|
+
function serveStatic(name, dir) {
|
|
63
|
+
const files = express.static(dir);
|
|
64
|
+
RED.httpAdmin.use("/fromcubes-react/" + name, permRead, files);
|
|
65
|
+
RED.httpAdmin.use("/" + ASSET_BASE + name, permRead, files);
|
|
66
|
+
}
|
|
67
|
+
|
|
40
68
|
// Since 0.56 monaco-editor has an `exports` map that hides package.json;
|
|
41
69
|
// its CommonJS entry is min/vs/index.js, so the AMD build (loader.js and
|
|
42
70
|
// editor/editor.main.js) sits in the same directory.
|
|
43
|
-
|
|
44
|
-
RED.httpAdmin.use(
|
|
45
|
-
"/fromcubes-react/vs",
|
|
46
|
-
permRead,
|
|
47
|
-
express.static(monacoVsPath),
|
|
48
|
-
);
|
|
71
|
+
serveStatic("vs", path.dirname(require.resolve("monaco-editor")));
|
|
49
72
|
|
|
50
73
|
// Editor-side workspace plugin (tree + full-screen Monaco overlay). Served
|
|
51
74
|
// as static files instead of being inlined into portal-react.html so the
|
|
52
75
|
// node's editor template stays readable.
|
|
53
|
-
|
|
54
|
-
"/fromcubes-react/editor",
|
|
55
|
-
permRead,
|
|
56
|
-
express.static(path.join(__dirname, "..", "editor")),
|
|
57
|
-
);
|
|
76
|
+
serveStatic("editor", path.join(__dirname, "..", "editor"));
|
|
58
77
|
|
|
59
78
|
// The node icon doubles as the editor's status-bar mark. Serving the icons
|
|
60
79
|
// directory here keeps one copy on disk and spares the client from having to
|
|
61
80
|
// know the package name that Node-RED's own /icons route is keyed by.
|
|
62
|
-
|
|
63
|
-
"/fromcubes-react/icons",
|
|
64
|
-
permRead,
|
|
65
|
-
express.static(path.join(__dirname, "..", "icons")),
|
|
66
|
-
);
|
|
81
|
+
serveStatic("icons", path.join(__dirname, "..", "icons"));
|
|
67
82
|
|
|
68
83
|
// Prettier's browser builds, straight from node_modules — same arrangement
|
|
69
84
|
// as Monaco at /fromcubes-react/vs. Nothing is fetched until someone formats.
|
|
70
|
-
|
|
71
|
-
"/fromcubes-react/prettier",
|
|
72
|
-
permRead,
|
|
73
|
-
express.static(path.dirname(require.resolve("prettier/package.json"))),
|
|
74
|
-
);
|
|
85
|
+
serveStatic("prettier", path.dirname(require.resolve("prettier/package.json")));
|
|
75
86
|
|
|
76
87
|
const CSS_HASH_RE = /^[a-f0-9]{1,64}$/;
|
|
77
88
|
function findCssByHash(reqHash) {
|
|
@@ -235,4 +246,4 @@ function registerProviderRoutes(RED, deps) {
|
|
|
235
246
|
);
|
|
236
247
|
}
|
|
237
248
|
|
|
238
|
-
module.exports = { registerAdminApi };
|
|
249
|
+
module.exports = { registerAdminApi, ASSET_BASE };
|
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/nodes/portal-react.html
CHANGED
|
@@ -1,12 +1,27 @@
|
|
|
1
1
|
<!-- ============================================================
|
|
2
2
|
Portal Code Workspace — sidebar tab + full-screen editor overlay.
|
|
3
|
-
Served from nodes/editor/ via the /fromcubes-react
|
|
3
|
+
Served from nodes/editor/ via the /fromcubes-react/<version>/editor admin route.
|
|
4
4
|
============================================================ -->
|
|
5
|
-
<
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
5
|
+
<script type="text/javascript">
|
|
6
|
+
// Loaded from the versioned asset path the runtime exports (ASSET_BASE in
|
|
7
|
+
// lib/admin-api.js), so no cache — browser, CDN or proxy — can serve this
|
|
8
|
+
// template an older file, or a 404 it remembered. Execution order follows
|
|
9
|
+
// insertion order (async = false); every script waits for what it needs.
|
|
10
|
+
(function () {
|
|
11
|
+
const base = (RED.settings && RED.settings.portalReactAssetBase) || "fromcubes-react/";
|
|
12
|
+
window.__fcAssetBase = base;
|
|
13
|
+
const link = document.createElement("link");
|
|
14
|
+
link.rel = "stylesheet";
|
|
15
|
+
link.href = base + "editor/workspace.css";
|
|
16
|
+
document.head.appendChild(link);
|
|
17
|
+
["file-tree.js", "workspace.js", "format.js", "provider-settings.js"].forEach(function (file) {
|
|
18
|
+
const s = document.createElement("script");
|
|
19
|
+
s.src = base + "editor/" + file;
|
|
20
|
+
s.async = false;
|
|
21
|
+
document.head.appendChild(s);
|
|
22
|
+
});
|
|
23
|
+
})();
|
|
24
|
+
</script>
|
|
10
25
|
|
|
11
26
|
<!-- ============================================================
|
|
12
27
|
Shared: Monaco loader (used by both node editors)
|
|
@@ -1041,10 +1056,11 @@
|
|
|
1041
1056
|
return;
|
|
1042
1057
|
}
|
|
1043
1058
|
const s = document.createElement("script");
|
|
1044
|
-
|
|
1059
|
+
const base = window.__fcAssetBase || "fromcubes-react/";
|
|
1060
|
+
s.src = base + "vs/loader.js";
|
|
1045
1061
|
s.onload = function () {
|
|
1046
1062
|
console.log(PREFIX, "loader.js loaded, configuring require paths");
|
|
1047
|
-
require.config({ paths: { vs: "
|
|
1063
|
+
require.config({ paths: { vs: base + "vs" } });
|
|
1048
1064
|
require(["vs/editor/editor.main"], function () {
|
|
1049
1065
|
console.log(PREFIX, "editor.main loaded");
|
|
1050
1066
|
ensureJsxSetup();
|
package/nodes/portal-react.js
CHANGED
|
@@ -1629,12 +1629,17 @@ module.exports = function (RED) {
|
|
|
1629
1629
|
}); // end setImmediate
|
|
1630
1630
|
}
|
|
1631
1631
|
|
|
1632
|
+
const { registerAdminApi, ASSET_BASE } = require("./lib/admin-api");
|
|
1632
1633
|
RED.nodes.registerType("portal-react", PortalReactNode, {
|
|
1633
1634
|
dynamicModuleList: "libs",
|
|
1635
|
+
// Read by portal-react.html to load the editor assets from their
|
|
1636
|
+
// versioned path (see ASSET_BASE in lib/admin-api.js).
|
|
1637
|
+
settings: {
|
|
1638
|
+
portalReactAssetBase: { value: ASSET_BASE, exportable: true },
|
|
1639
|
+
},
|
|
1634
1640
|
});
|
|
1635
1641
|
|
|
1636
1642
|
const express = loadExpress();
|
|
1637
|
-
const { registerAdminApi } = require("./lib/admin-api");
|
|
1638
1643
|
registerAdminApi(RED, {
|
|
1639
1644
|
express,
|
|
1640
1645
|
permRead: PERM_READ,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aaqu/fromcubes-react",
|
|
3
|
-
"version": "0.1
|
|
3
|
+
"version": "0.2.1",
|
|
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": {
|