4bnode 3.5.3 → 4.0.2
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 +144 -109
- package/add-api.js +288 -156
- package/add-crud.js +226 -0
- package/add-login.js +152 -319
- package/add-mongo-delete.js +67 -138
- package/add-mongo-insert.js +87 -204
- package/add-mongo-read.js +100 -216
- package/add-mongo-schema.js +431 -199
- package/add-mongo-update.js +89 -202
- package/add-mongo.js +125 -218
- package/add-serialport.js +53 -98
- package/add-socket.js +43 -66
- package/add-websocket.js +68 -140
- package/index.js +341 -156
- package/lib/env.js +18 -0
- package/lib/indexFile.js +134 -0
- package/lib/names.js +21 -0
- package/lib/project.js +122 -0
- package/lib/routeFile.js +56 -0
- package/lib/ui.js +161 -0
- package/package.json +9 -16
- package/skeleton/.4bnode/assets/fonts/NotoSans-Bold.woff2 +0 -0
- package/skeleton/.4bnode/assets/fonts/NotoSans-Light.woff2 +0 -0
- package/skeleton/.4bnode/assets/fonts/NotoSans-Medium.woff2 +0 -0
- package/skeleton/.4bnode/assets/fonts/NotoSans-Regular.woff2 +0 -0
- package/skeleton/.4bnode/assets/fonts/NotoSans-SemiBold.woff2 +0 -0
- package/skeleton/.4bnode/assets/images/mongo.png +0 -0
- package/skeleton/.4bnode/assets/images/mysql.png +0 -0
- package/skeleton/.4bnode/assets/images/postgresql.png +0 -0
- package/skeleton/.4bnode/assets/images/sqlite.png +0 -0
- package/skeleton/.4bnode/assets/js/App.js +115 -0
- package/skeleton/.4bnode/assets/js/Root.js +44 -0
- package/skeleton/.4bnode/assets/js/components/Modal.js +52 -0
- package/skeleton/.4bnode/assets/js/components/Sidebar.js +68 -0
- package/skeleton/.4bnode/assets/js/components/ToastContainer.js +15 -0
- package/skeleton/.4bnode/assets/js/context.js +7 -0
- package/skeleton/.4bnode/assets/js/lib.js +83 -0
- package/skeleton/.4bnode/assets/js/main.js +5 -0
- package/skeleton/.4bnode/assets/js/pages/ApiTesterPage.js +1594 -0
- package/skeleton/.4bnode/assets/js/pages/DashboardPage.js +435 -0
- package/skeleton/.4bnode/assets/js/pages/DatabasePage.js +461 -0
- package/skeleton/.4bnode/assets/js/pages/EnvironmentPage.js +141 -0
- package/skeleton/.4bnode/assets/js/pages/RealtimePage.js +186 -0
- package/skeleton/.4bnode/assets/js/pages/RoutesPage.js +1656 -0
- package/skeleton/.4bnode/assets/js/pages/SchemasPage.js +901 -0
- package/skeleton/.4bnode/assets/js/pages/SerialPortPage.js +170 -0
- package/skeleton/.4bnode/assets/js/screens/PasskeyScreen.js +105 -0
- package/skeleton/.4bnode/assets/js/screens/SplashScreen.js +66 -0
- package/skeleton/.4bnode/dev-api.js +2930 -0
- package/skeleton/.4bnode/ui.html +1556 -0
- package/skeleton/index.js +70 -17
- package/skeleton/package.json +9 -5
- package/skeleton/public/index.html +288 -86
- package/README_4bnode.md +0 -197
- package/src/auth/passport.js +0 -21
- package/src/db.js +0 -13
- package/test-server/.env.development +0 -1
- package/test-server/.env.production +0 -1
- package/test-server/index.js +0 -82
- package/test-server/package-lock.json +0 -1267
- package/test-server/package.json +0 -20
- package/test-server/public/index.html +0 -90
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import React, { useState, useEffect } from "react";
|
|
2
|
+
import { AnimatePresence } from "framer-motion";
|
|
3
|
+
import { motion } from "framer-motion";
|
|
4
|
+
import { html, API, getStoredPasskey } from "./lib.js";
|
|
5
|
+
import { App } from "./App.js";
|
|
6
|
+
import { SplashScreen } from "./screens/SplashScreen.js";
|
|
7
|
+
import { PasskeyScreen } from "./screens/PasskeyScreen.js";
|
|
8
|
+
|
|
9
|
+
export function Root() {
|
|
10
|
+
const [showSplash, setShowSplash] = useState(true);
|
|
11
|
+
const [authed, setAuthed] = useState(false);
|
|
12
|
+
const [checking, setChecking] = useState(true);
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
(async () => {
|
|
16
|
+
try {
|
|
17
|
+
const stored = getStoredPasskey();
|
|
18
|
+
const res = await fetch(API + '/auth', {
|
|
19
|
+
method: 'POST',
|
|
20
|
+
headers: { 'Content-Type': 'application/json' },
|
|
21
|
+
body: JSON.stringify({ passkey: stored }),
|
|
22
|
+
});
|
|
23
|
+
if (res.ok) setAuthed(true);
|
|
24
|
+
} catch {}
|
|
25
|
+
setChecking(false);
|
|
26
|
+
})();
|
|
27
|
+
}, []);
|
|
28
|
+
|
|
29
|
+
if (checking) return null;
|
|
30
|
+
|
|
31
|
+
return html`<${AnimatePresence} mode="wait">
|
|
32
|
+
${showSplash
|
|
33
|
+
? html`<${SplashScreen} key="splash" onDone=${() => setShowSplash(false)} />`
|
|
34
|
+
: !authed
|
|
35
|
+
? html`<${PasskeyScreen} key="passkey" onSuccess=${() => setAuthed(true)} />`
|
|
36
|
+
: html`<${motion.div} key="app"
|
|
37
|
+
style=${{ display: 'flex', height: '100vh', width: '100%' }}
|
|
38
|
+
initial=${{ opacity: 0 }}
|
|
39
|
+
animate=${{ opacity: 1 }}
|
|
40
|
+
transition=${{ duration: 0.4 }}>
|
|
41
|
+
<${App} />
|
|
42
|
+
<//>`}
|
|
43
|
+
<//>`;
|
|
44
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { motion, AnimatePresence } from "framer-motion";
|
|
3
|
+
import { LuTrash2, LuZap } from "react-icons/lu";
|
|
4
|
+
import { html } from "../lib.js";
|
|
5
|
+
|
|
6
|
+
export function Modal({ title, children, onClose, actions }) {
|
|
7
|
+
return html`<${AnimatePresence}><${motion.div}
|
|
8
|
+
className="modal-overlay"
|
|
9
|
+
onClick=${(e) => e.target === e.currentTarget && onClose()}
|
|
10
|
+
initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
|
|
11
|
+
transition=${{ duration: 0.15 }}
|
|
12
|
+
>
|
|
13
|
+
<${motion.div} className="modal"
|
|
14
|
+
initial=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
15
|
+
animate=${{ opacity: 1, scale: 1, y: 0 }}
|
|
16
|
+
exit=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
17
|
+
transition=${{ duration: 0.15 }}>
|
|
18
|
+
<div className="modal-title">${title}</div>
|
|
19
|
+
${children}
|
|
20
|
+
${actions && html`<div className="modal-actions">${actions}</div>`}
|
|
21
|
+
<//>
|
|
22
|
+
<//><//>`;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function ConfirmDialog({ message, onConfirm, onCancel, confirmLabel, danger }) {
|
|
26
|
+
return html`<${AnimatePresence}><${motion.div}
|
|
27
|
+
className="modal-overlay"
|
|
28
|
+
onClick=${onCancel}
|
|
29
|
+
initial=${{ opacity: 0 }} animate=${{ opacity: 1 }} exit=${{ opacity: 0 }}
|
|
30
|
+
transition=${{ duration: 0.15 }}
|
|
31
|
+
>
|
|
32
|
+
<${motion.div} className="modal" onClick=${e => e.stopPropagation()}
|
|
33
|
+
style=${{ minWidth: '380px', maxWidth: '440px' }}
|
|
34
|
+
initial=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
35
|
+
animate=${{ opacity: 1, scale: 1, y: 0 }}
|
|
36
|
+
exit=${{ opacity: 0, scale: 0.95, y: 10 }}
|
|
37
|
+
transition=${{ duration: 0.15 }}>
|
|
38
|
+
<div className="flex items-center gap-3" style=${{ marginBottom: '16px' }}>
|
|
39
|
+
<div style=${{ width: '40px', height: '40px', borderRadius: '10px', display: 'flex', alignItems: 'center', justifyContent: 'center', flexShrink: 0,
|
|
40
|
+
background: danger ? 'var(--error-glow)' : 'var(--warning-glow)' }}>
|
|
41
|
+
<${danger ? LuTrash2 : LuZap} size=${18} color=${danger ? 'var(--error)' : 'var(--warning)'} />
|
|
42
|
+
</div>
|
|
43
|
+
<div style=${{ fontSize: '15px', fontWeight: 600 }}>Confirm Action</div>
|
|
44
|
+
</div>
|
|
45
|
+
<div style=${{ fontSize: '13px', color: 'var(--text-secondary)', lineHeight: '1.6', marginBottom: '20px' }}>${message}</div>
|
|
46
|
+
<div className="modal-actions">
|
|
47
|
+
<button className="btn btn-ghost" onClick=${onCancel}>Cancel</button>
|
|
48
|
+
<button className=${`btn ${danger ? 'btn-danger' : 'btn-primary'}`} onClick=${onConfirm}>${confirmLabel || 'Confirm'}</button>
|
|
49
|
+
</div>
|
|
50
|
+
<//>
|
|
51
|
+
<//><//>`;
|
|
52
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import { html, Icons } from "../lib.js";
|
|
3
|
+
import { useApp } from "../context.js";
|
|
4
|
+
|
|
5
|
+
export function Sidebar() {
|
|
6
|
+
const { page, setPage, data } = useApp();
|
|
7
|
+
const dbOk = data.mongoConfigured;
|
|
8
|
+
|
|
9
|
+
const navItems = [
|
|
10
|
+
{
|
|
11
|
+
id: "dashboard",
|
|
12
|
+
label: "Dashboard",
|
|
13
|
+
icon: Icons.dashboard,
|
|
14
|
+
section: "Overview",
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
id: "database",
|
|
18
|
+
label: "Database",
|
|
19
|
+
icon: Icons.database,
|
|
20
|
+
section: "Setup",
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
id: "schemas",
|
|
24
|
+
label: "Schemas",
|
|
25
|
+
icon: Icons.schema,
|
|
26
|
+
locked: !dbOk,
|
|
27
|
+
lockMsg: "Requires database",
|
|
28
|
+
},
|
|
29
|
+
{ id: "routes", label: "API Routes", icon: Icons.route },
|
|
30
|
+
{ id: "api-tester", label: "API Tester", icon: Icons.apiTester },
|
|
31
|
+
{
|
|
32
|
+
id: "realtime",
|
|
33
|
+
label: "Real-time",
|
|
34
|
+
icon: Icons.realtime,
|
|
35
|
+
section: "Features",
|
|
36
|
+
},
|
|
37
|
+
{ id: "serialport", label: "SerialPort", icon: Icons.serial },
|
|
38
|
+
{ id: "env", label: "Environment", icon: Icons.env, section: "Settings" },
|
|
39
|
+
];
|
|
40
|
+
|
|
41
|
+
let currentSection = null;
|
|
42
|
+
return html`<div className="sidebar">
|
|
43
|
+
<div className="sidebar-brand">
|
|
44
|
+
<pre>${`██╗ ██╗██████╗ ███╗ ██╗ ██████╗ ██████╗ ███████╗\n██║ ██║██╔══██╗████╗ ██║██╔═══██╗██╔══██╗██╔════╝\n███████║██████╔╝██╔██╗ ██║██║ ██║██║ ██║█████╗\n╚════██║██╔══██╗██║╚██╗██║██║ ██║██║ ██║██╔══╝\n ██║██████╔╝██║ ╚████║╚██████╔╝██████╔╝███████╗\n ╚═╝╚═════╝ ╚═╝ ╚═══╝ ╚═════╝ ╚═════╝ ╚══════╝`}</pre>
|
|
45
|
+
<span>Project Dashboard</span>
|
|
46
|
+
</div>
|
|
47
|
+
${navItems.map((item) => {
|
|
48
|
+
const sectionHeader =
|
|
49
|
+
item.section && item.section !== currentSection
|
|
50
|
+
? ((currentSection = item.section),
|
|
51
|
+
html`<div className="sidebar-section">${item.section}</div>`)
|
|
52
|
+
: null;
|
|
53
|
+
return html`<${React.Fragment} key=${item.id}>
|
|
54
|
+
${sectionHeader}
|
|
55
|
+
<div
|
|
56
|
+
className=${`sidebar-item ${page === item.id ? "active" : ""} ${item.locked ? "locked" : ""}`}
|
|
57
|
+
onClick=${() => !item.locked && setPage(item.id)}
|
|
58
|
+
title=${item.locked ? item.lockMsg : item.label}
|
|
59
|
+
>
|
|
60
|
+
<span className="sidebar-icon">${item.icon}</span>
|
|
61
|
+
${item.label}
|
|
62
|
+
${item.locked &&
|
|
63
|
+
html`<span className="lock-icon">${Icons.lock}</span>`}
|
|
64
|
+
</div>
|
|
65
|
+
<//>`;
|
|
66
|
+
})}
|
|
67
|
+
</div>`;
|
|
68
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { html } from "../lib.js";
|
|
2
|
+
import { useApp } from "../context.js";
|
|
3
|
+
|
|
4
|
+
export function ToastContainer() {
|
|
5
|
+
const { toasts } = useApp();
|
|
6
|
+
if (toasts.length === 0) return null;
|
|
7
|
+
return html`<div className="toast-container">
|
|
8
|
+
${toasts.map(
|
|
9
|
+
(t) =>
|
|
10
|
+
html`<div key=${t.id} className=${`toast ${t.type}`}>
|
|
11
|
+
${t.message}
|
|
12
|
+
</div>`,
|
|
13
|
+
)}
|
|
14
|
+
</div>`;
|
|
15
|
+
}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import React from "react";
|
|
2
|
+
import htm from "htm";
|
|
3
|
+
import {
|
|
4
|
+
LuLayoutDashboard,
|
|
5
|
+
LuDatabase,
|
|
6
|
+
LuFileText,
|
|
7
|
+
LuRadio,
|
|
8
|
+
LuPlus,
|
|
9
|
+
LuTrash2,
|
|
10
|
+
LuCheck,
|
|
11
|
+
LuLock,
|
|
12
|
+
LuArrowRight,
|
|
13
|
+
LuPencil,
|
|
14
|
+
LuCable,
|
|
15
|
+
LuActivity,
|
|
16
|
+
LuZap,
|
|
17
|
+
LuShield,
|
|
18
|
+
LuBox,
|
|
19
|
+
LuFlaskConical,
|
|
20
|
+
LuSend,
|
|
21
|
+
LuSettings,
|
|
22
|
+
} from "react-icons/lu";
|
|
23
|
+
|
|
24
|
+
export const html = htm.bind(React.createElement);
|
|
25
|
+
export const API = "/_dev/api";
|
|
26
|
+
|
|
27
|
+
export function getStoredPasskey() {
|
|
28
|
+
return sessionStorage.getItem("_dev_passkey") || "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export async function api(path, options = {}) {
|
|
32
|
+
const passkey = getStoredPasskey();
|
|
33
|
+
const headers = { "Content-Type": "application/json" };
|
|
34
|
+
if (passkey) headers["x-passkey"] = passkey;
|
|
35
|
+
const res = await fetch(API + path, {
|
|
36
|
+
headers,
|
|
37
|
+
...options,
|
|
38
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
39
|
+
});
|
|
40
|
+
const text = await res.text();
|
|
41
|
+
let data;
|
|
42
|
+
try {
|
|
43
|
+
data = JSON.parse(text);
|
|
44
|
+
} catch {
|
|
45
|
+
if (res.ok) return { message: "Done" };
|
|
46
|
+
throw new Error("Server restarting, please wait...");
|
|
47
|
+
}
|
|
48
|
+
if (!res.ok)
|
|
49
|
+
throw new Error(data.error || data.message || "Request failed");
|
|
50
|
+
return data;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export const Icons = {
|
|
54
|
+
dashboard: html`<${LuLayoutDashboard} size=${16} />`,
|
|
55
|
+
database: html`<${LuDatabase} size=${16} />`,
|
|
56
|
+
schema: html`<${LuFileText} size=${16} />`,
|
|
57
|
+
route: html`<${LuRadio} size=${16} />`,
|
|
58
|
+
realtime: html`<${LuActivity} size=${16} />`,
|
|
59
|
+
env: html`<${LuSettings} size=${16} />`,
|
|
60
|
+
plus: html`<${LuPlus} size=${16} />`,
|
|
61
|
+
trash: html`<${LuTrash2} size=${16} />`,
|
|
62
|
+
check: html`<${LuCheck} size=${16} />`,
|
|
63
|
+
lock: html`<${LuLock} size=${12} />`,
|
|
64
|
+
arrow: html`<${LuArrowRight} size=${14} />`,
|
|
65
|
+
edit: html`<${LuPencil} size=${16} />`,
|
|
66
|
+
serial: html`<${LuCable} size=${16} />`,
|
|
67
|
+
zap: html`<${LuZap} size=${16} />`,
|
|
68
|
+
shield: html`<${LuShield} size=${16} />`,
|
|
69
|
+
box: html`<${LuBox} size=${16} />`,
|
|
70
|
+
apiTester: html`<${LuFlaskConical} size=${16} />`,
|
|
71
|
+
send: html`<${LuSend} size=${16} />`,
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const PAGE_TITLES = {
|
|
75
|
+
dashboard: "Dashboard",
|
|
76
|
+
database: "Database",
|
|
77
|
+
schemas: "Schemas",
|
|
78
|
+
routes: "API Routes",
|
|
79
|
+
"api-tester": "API Tester",
|
|
80
|
+
realtime: "Real-time",
|
|
81
|
+
serialport: "SerialPort",
|
|
82
|
+
env: "Environment",
|
|
83
|
+
};
|